feat(scheduling): Registry Scheduling MVP - #1092
Conversation
7ed0254 to
7be7edc
Compare
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9674e86e93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
4bf86bc to
34deb3e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e590e5cc66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8491a37f4a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68e3060fbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
68e3060 to
0aecb1a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0aecb1a8e5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 046e23ce6d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Casework clocks and Scheduling openings evaluate the same mathematical object: working days, holiday revisions, and timezone-correct local instants. Move the evaluator into a new registry-platform-calendar crate so both products share one implementation, and add the weekly-opening expansion Scheduling needs: bounded patterns become concrete half-open UTC intervals, daylight-saving gaps and folds are explicit errors rather than silent shifts, and exception layering makes a blocking closure win over an opening while an authorized reopening adds time back only inside the closure it names. registry-casework-core re-exports the platform crate through its flat glob, so its public surface is unchanged. CalendarEvaluationError keeps every original variant verbatim and gains the weekly diagnostics; it loses Copy because the new variants carry identifiers. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
GrantBounds gains a closed `scheduling` tag whose permissions bind one
service and location pair to a bounded action vocabulary, mirroring the
existing BREG bounds shape. Existing evidence and breg accessors return
None for it, and a scheduling_permissions accessor joins them.
Validation:
- Threat: grant bounds carry the cross-product authorization payload
inside a signed task grant, so a new variant is where semantics could
leak from one product's runtime into another's. The variant stays a
closed union member: a verifier that does not know the tag rejects it
as a malformed bound at authentication and never interprets it, and
the Evidence and BREG consumers read bounds only through their
accessors, which return None for scheduling grants.
- Wire format: inside the signed registry_grant_bounds claim,
{"type":"scheduling","permissions":[{"service":...,"location":...,
"actions":[...]}]}. No existing claim changes shape; the union gains a
tag and nothing else.
- Compatibility: issuers on older code cannot mint the tag and verifiers
on older code refuse it as malformed, which is the intended fail-closed
behavior; no token signed before this change carries scheduling bounds.
- Limits: 64 permissions of 32 actions, service and location values
<=512 bytes with no whitespace or wildcard, actions in the existing
lowercase operation grammar, (service, location) pairs unique; Debug
redacts service, location, and action values.
- Tests: registry-platform-oidc 81 passed (variant distinctness,
cardinality at and over the limits, wildcard, whitespace, and duplicate
refusal, scheduling Debug redaction); cargo fmt/check/clippy
--workspace --all-targets -D warnings clean; cargo test --workspace
green with BREG_TEST_DATABASE_URL set on a disposable PostGIS instance
(registry-breg: 53 suites, 373 passed, 0 failed; the casework clock
PostgreSQL journey also passes against the extracted calendar);
cargo deny check ok for advisories, bans, licenses, and sources.
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A reopening restored the closure it named even when that closure reached outside the pattern's own time, so an operator could extend published hours through the exception layer rather than through the reviewed pattern. A reopening now restores only pattern time its closure removed: it must sit inside the pattern's hours on its date, on a pattern weekday, inside the effective range, and on a non-holiday date, or the evaluation refuses with the reopening's identifier. Dated local intervals gain their own conversion with dedicated error variants: local_interval turns one wall-clock interval on a date into a half-open UTC interval in a named timezone, refusing gap and fold instants explicitly, and InvalidIntervalDate and InvalidIntervalTimes replace the exception-scoped variants a placeholder id used to force. Review notes: the reopening tightening changes what an authorized exception may publish, so the tests pin every refused shape (outside pattern hours, reaching past pattern end, holiday date, non-pattern weekday, out of effective range) beside the restored ones (identity reopening of a whole closure, adjacency to an unrelated closure), plus the fold-spanning interval that keeps real elapsed time. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
GrantBounds::Scheduling shipped with SchedulingPermission private to the crate, so no relying product could name the permission it verifies. Export it through the crate root and pin the wire form: the scheduling tag round-trips through serde, refuses unknown members at the union and the permission-value level alike, and enforces its value boundaries. The review record for the scheduling bounds moves from the M0b commit message into the crate README, where the BREG and Evidence bounds records already live, so a reviewer of a later change finds the limits and redaction decisions beside the code. Review notes: authorization-relevant but behavior-neutral on the wire. No claim shape changes and no verifier changes; the export makes an existing closed type nameable, and the round-trip test holds the tag spelling so a write-side regression fails before validation runs. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The scheduling bounds are a closed union member the BREG binding must never interpret: a task grant carrying them is refused as a malformed bound at authentication, the same fail-closed path as every bound the verifier does not know. Pin that refusal so a future accessor addition cannot silently admit scheduling grants to BREG actions. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Registry Scheduling Milestone 1, the offline checkpoint: everything that decides an admission and everything that proves it, with no runtime yet. registry-scheduling-core holds the source-neutral model and the pure evaluators. The authored policy declares services, offerings, opening patterns, holiday sets, published windows, and the hold policy; the environment facts (locations, pools, dated exceptions) stay runtime records the policy references but never embeds, so one published policy governs every environment that resolves its identifiers. The evaluators are total functions of resolved policy, facts, ledger snapshot, request, and observed instant: no clock, no socket, no database. The runtime will call them inside its capacity transaction, the explain endpoint will call them again for an authorized caller, and fixture replay calls them with synthetic facts, so the three cannot disagree about what admits. Every refusal carries two problem codes from the closed 26-code vocabulary: the public one every caller may see and the detailed one only the authorized explain path may see. Exactly one code, resource.unavailable, is detailed-only, because a specific resource's absence can disclose another person's booking or a private staff reason. authorization.refused is an audit reason, never a problem code. Interval arithmetic that leaves representable time refuses as a horizon answer, never a mislabeled capacity refusal and never a clamped buffer that undercounts occupancy; window allocation saturates instead of wrapping past u32. Fixtures replay offline: synthetic facts, a starting ledger, ordered cases with expected outcomes. Replay runs the same evaluators, records admitted cases into the ledger later cases contend against, and refuses a fixture naming a code outside the vocabulary or a detailed-only code before any case runs. Structural accesses are total: a policy whose check did not pass fails the fixture with a typed error naming the case, never a panic. schedulingctl authors and proves: init writes a complete starter project from one of two templates, check validates offline, test replays every fixture, explain publishes what the runtime would serve. check exits zero with findings by default and gains --deny-findings for CI, matching caseworkctl; a fixture run with failing cases always exits nonzero. Reports render in text or JSON with path-addressed diagnostics. products/scheduling carries the dependency-direction gate holding the boundary: the core never references runtime, protocol, or storage types, and no product term leaks into a platform crate. Tests: registry-scheduling-core 71 passed, registry-schedulingctl 22 passed, the template projects init/check/test/explain green end to end; cargo fmt, check, and clippy -D warnings clean across the workspace; cargo test --workspace green with the disposable PostGIS databases set; cargo deny check ok; the scheduling dependency-direction gate passes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
INT-02 makes reminders configurable at the service-design layer and binds each intent to an appointment revision, and AT-08 asserts a reschedule leaves no active reminder for the old revision. Both need an authored reminder schedule to exist, or the invariant is only vacuously true, so the policy gains one reminder offset type on each offering: minutes_before and a review because, validated like every other authored choice (zero, duplicate distances, and blank reasons are findings). Message transport stays external: the runtime mints revision-bound intents from these offsets, and an adopter with no consumer reads them back. The standalone-exact-time template authors two offsets on the counter offering so the runtime demo can show a real intent being suppressed. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The HTTP documents both the runtime and every client speak: the catalogue (service, offering, window, reminder), availability entries as a kind-tagged union of grid slots and windows, pages with cursors, holds, appointments and their requests, history entries, and the explain document with its public and detailed problem codes side by side. They live in the source-neutral core beside the model they project, the way the Casework DTOs live in registry-casework-core, so the client crate depends on core and never on the runtime. Every document is camelCase and refuses unknown fields. The projections are deliberate: because reasons never publish, callers see displayed times rather than occupied intervals, and the detailed code that names unavailability appears only on the separately authorized explain path. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Every code in the closed vocabulary now carries its HTTP status, title, and value-free remediation detail beside the code string itself, so the runtime emits and every client validates one pinned mapping rather than each re-deriving its own. The map is part of the wire contract: a caller can enumerate every problem this product can return and know the status it arrives with. Statuses follow the acceptance reasoning: recoverable conflicts answer 409, holds and idempotency receipts that no longer exist answer 410, stale revisions answer 412, a missing precondition answer 428, requests the published policy could never serve answer 422, and checks that could not run answer 503. Transport statuses (404, 405, 413, 415) are deliberately absent: edge rejections belong to the platform's own problem namespace, never to the domain vocabulary. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The route paths, the idempotency-key header, the cursor and limit query parameters, and the idempotency key bound are contract the runtime and every client share, so they live in core beside the artifact names and are pinned by the same style of test. The resource and location listings gain their documents: a resource is a concrete pool member, never an independent counter, and a location carries the IANA timezone its openings expand in. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The relying-party client for Registry Scheduling: fourteen methods over the pinned exact-time HTTP contract, each taking a borrowed bearer token by value and answering one completed call with the response document and its validated trace identifier. Every failure lands in one of four matchable shapes: a caller-side configuration or request defect validated before any network input or output, a transport failure, a protocol failure naming the stage where the wire stopped matching the pinned contract, and an exactly validated product problem surfaced as the core's closed ProblemCode. A problem document only becomes a domain problem when its status, type URI, title, detail, code, and trace identifier all equal the pinned definition; any mismatch, and any platform-owned transport problem, is edge talk. Mutating calls carry a caller-supplied idempotency key validated against the pinned bound. Route identifiers are validated as one path segment before interpolation, so an opaque id can never silently detour to a different route. Bodies are read under a bounded byte ceiling and the core documents refuse unknown fields, so a response is exactly the contract or a protocol failure. The client never retains a token, never follows a redirect, and never retries a mutation. Boundary: the client depends on registry-scheduling-core and the shared registry-platform-httpsec and registry-platform-httputil primitives only, never on the Scheduling runtime or its operator tooling, and adds no scheduling semantics of its own. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The service owns the three decisions the store cannot. Authorization: every mutating call must carry a task grant whose scheduling permissions name the offering's service, location, and the operation, and the store re-checks the grant's expiry inside the capacity transaction. Disclosure: every refusal projects to its public problem code, and only the separately authorized explain path sees the detailed one; an admitted start carries no codes at all, because there is no refusal to explain. Attributability: the commitment's audit record carries pseudonymized principal, client, grant, and approver references, and a refused commitment records its receipt under the caller's idempotency key so a replay answers exactly as the first attempt did. Availability is bounded: exact-time offerings walk their authored grid inside the published openings, clear of closures, past lead time, inside the horizon, and only list a slot at least one available member can still serve; arrival windows list their remaining units. A stored success receipt carries the minted claim itself, so a replayed response is the original response through the same projection, not a re-derivation from live state. The policy-to-context resolution moves from the replay fixtures into the core beside the evaluators, so the offline replay and the live runtime expand the same openings and closures and cannot disagree about what a location published. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
records apply performs the one attributable operator write of a deployment's live environment records: the document is parsed and validated offline in its own terms (duplicates, timezone, undeclared exception locations, strict dates and times) before any connection opens, then lands through the store's single replace transaction with its operator audit row. A PostgreSQL integration test behind the postgres-test feature proves the wholesale swap and both audit rows on a disposable schema. package writes the deployment identity the runtime verifies: the manifest lands beside the authored policy, an existing manifest is refused rather than replaced, and the written manifest is proven to verify through the runtime's own verifier before success is reported. Runtime-configuration and store failures now map to their own diagnostics instead of the generic authoring refusal, and the store binds exception dates as text cast to date, the pattern the rest of the workspace uses for typed columns. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The request-edge rejections (400/404/405/413/415/422) join the closed problem vocabulary under the product's own prefix, exactly as Casework carries them: no shared platform problem prefix exists in the Registry Stack identifier catalog. The client now types the product's own request-edge documents and treats only foreign dialects as edge talk. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The database-test isolation gate now reports a maintained database suite whose required-features gate disappeared, and a dependency entry that asks for a feature whose expansion enables a sibling's gate, not only the gate's own name. The known suites are named so losing a gate fails the checkpoint rather than passing silently. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The contracts validator now asks each cited test's own runner for its inventory instead of matching names in source text: cargo lists the selected target's tests under the citation's features, unittest discovers the Python module, and neither executes a test body. The one citation living behind a feature gate names the feature in the matrix and the traceability file. The database-suite gate reads Cargo's resolved default features, so aliasing or forwarding postgres-test into a default build fails the checkpoint, and an ordinary ungated integration test is no longer mistaken for a database suite. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
SCHEDULING-SEC-14 is recorded as enforced and promises an authorization audit record for every refusal the ledger decides. The guard selecting which outcomes to record matched only an admission refusal, the hold ceiling, and a lapsed grant, so a stale observed revision and a cancellation past its cutoff were answered as refusals with nothing written behind them. Both are decisions about a named appointment under a grant the service had already matched, which is exactly what the invariant exists to attribute. Replace the wildcard match with an exhaustive one, so a new CommitError variant has to state whether it is a decision or a fault instead of inheriting silence. Faults decide nothing and stay unaudited; an idempotency key refusal stays unaudited because the attempt receipt already carries it and the key says nothing about what a grant reaches. Narrow the invariant's enforcement point to name the outcomes on each side, so the contract asserts what the code performs rather than a looser claim it did not meet. Security review notes: this changes audit integrity. It only adds records; no existing record changes shape, reason, or ordering, and the hash chain is untouched. The added records carry the same pseudonymized principal and grant as the refusals already audited beside them, so no caller identifier reaches the journal in the clear. The new test asserts both the added rows and the absence of the raw principal. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The install block advertised scheduling-install.sh and a SCHEDULING_INSTALL_DIR override, but the release candidate's payload inventory carries no scheduling binary and no scheduling installer, and that inventory is checked for exact equality. Both links would answer 404 on the next release, and adding the assets to a manifest without first extending the inventory would be refused as unexpected payloads. Say what is true today: Scheduling publishes a container image, which the release model does carry, and has no released binary or installer yet. Wiring the binaries and the installer into the release pipeline touches release provenance and belongs in its own reviewed change. Move the CLI publication review date to the day the combined command content was last reviewed against this branch. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
046e23c to
84c301b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84c301bbbc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…variants The canonical validation of a policy against the published window records it governs runs only in the authoring tooling. Neither database write repeats it: policy publication fences offerings carrying active claims but never reads the window records, and records replacement never reads the deployed policy. An operator bypassing the authoring tooling can back an exact-time offering with a pool that already staffs a published window, and the two modes then draw the same staffing through separate anchor rows. No matrix row claimed this was enforced, so the security contract was accurate but silent. Record it as SCHEDULING-DEF-07 with the authoring refusal as its compensating control, restate it in the review notes the row points at, and name the limit in the changelog beside the ledger promise it qualifies. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e807da43f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Rebase failed
Draft, opened to get CI running against the branch and to give the work a home for review. Not ready to merge.
Size
101 commits, 122 files, 42717 insertions, 145 deletions. 81 files added, 39 modified, one rename, one copy.
What it adds
Five new workspace crates, all wired into the root
Cargo.tomlat0.32.0:registry-scheduling-core, the source-neutral model, admission rules, policy, diagnostics, problem catalog, and fixturesregistry-scheduling, the runtime and theschedulingbinaryregistry-schedulingctl, adopter and operator tooling and theschedulingctlbinaryregistry-scheduling-client, the Rust clientregistry-platform-calendar, shared calendar primitivesAGENTS.mdis updated to describe Scheduling as a fifth independent runtime product, with the boundary stated explicitly: Scheduling owns its capacity ledger absolutely, a hold or appointment is created, moved, or released only inside the Scheduling runtime's own capacity transaction, and no other product may write to that ledger directly or through a shared database.Commit scopes, complete: 30
fix(scheduling), 20feat(scheduling), 11docs(scheduling), 6test(scheduling), 6fix(schedulingctl), 4docs, 3feat(schedulingctl), 2feat(platform), 2chore(scheduling), 1 each oftest(release),test(breg),refactor(casework),fix(scheduling-client),fix(platform),fix(platform-calendar),fix(ci),feat(scheduling-core),feat(auth),docs(site),docs(schedulingctl),ci(scheduling),ci,chore(schedulingctl),chore(identifiers),chore,test(schedulingctl).Beyond the new crates it touches
products/scheduling(29 files),docs(18),release(7),.github(4),products/identifiers(2).Worth a reviewer's attention
Three things reach outside the Scheduling crates and are the parts most worth arguing about:
crates/registry-casework-core/src/calendar.rsmoves tocrates/registry-platform-calendar/src/working_day.rs(85% similarity). Calendar logic is lifted out of Casework into a shared platform crate so Scheduling can use it without depending on Casework. The Casework public API is preserved by re-exporting the moved items fromregistry-casework-core. That is the right direction given the product boundaries, but it changes a Casework-owned file and deserves a second opinion on the seam.crates/registry-platform-oidcgains aSchedulingvariant onGrantBoundsplus its bounded permission type (3 files). The change is additive, but the crate is shared by every product, so it is worth checking nothing narrows or widens for the existing callers.crates/registry-breg/src/task_grant/tests.rsandcrates/registry-cli-docsare each touched lightly. Scheduling verifies a task grant, so the BReg test change is expected, but it is a cross-product edit.State
mainand force-pushed. The rebase was clean: the branch and the three commits it was behind touch disjoint file sets.docs/site/src/data/cli-reference.yamlcarries a fresh v3 publication record for0.32.0, reviewed against a generated catalog diff rather than stamped blind. The only catalog delta againstmainis the two added binariesschedulingandschedulingctl; every other command tree is byte-identical.Cargo.lockgains only the five new workspace crates. No third-party version moved and nothing was removed, so every other product builds against an identical dependency graph.What the first CI run found
One real defect, now fixed on the branch:
intents_postgresseeded its base time straight from the clock and compared the value PostgreSQL returned against the nanosecond-precision value still held in memory.timestamptzresolves to microseconds, so the tail was dropped on write. This failed on every Linux run and passed on every macOS one, because the two platforms' realtime clocks differ in resolution, which is why it reached CI at all. The fix truncates the seeded instant to microseconds.One unrelated gate flaked:
Base Registry Engine product contracts (postgres)refused acaseworkctl devsession with a genericruntime_dependencydiagnostic, then passed on a re-run of the same commit. That test starts a dev session with a stock issuer, a database, and four ports, so it is load sensitive. Not caused by this branch: the branch touches neither the Casework runtime, norcaseworkctl, nor that test, nor that job.