Conversation
a6d6ea7 to
086d918
Compare
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-free checkpoint runs the dependency-direction gate, both starter templates through the whole offline authoring journey, the package manifest write, and a drift check holding the committed example to exactly what init writes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Six defects found reviewing the committed M2 half against the milestone contracts, each with its own verification: - Exact-time capacity locked member ids as supply anchors, but the publication anchors pool ids only: every commitment met a missing anchor row and answered corrupt instead of booking. The transaction now locks the pool and snapshots the members (store.rs). - The eagerly constructed internal errors on four lookup paths logged a phantom error line on every successful call. They now construct only on failure (service.rs). - Holding an intent local stamped a delivery instant its own comment said never happened. No instant is stamped (store.rs). - A reminder destination URL carrying a query or fragment passed the configuration check and was then refused by the frozen transport at startup. The check refuses it first (config.rs). - The postgres-test feature compiles the plaintext-database refusal out of the binary and compiled the pinning test's only use out with it. The use is now gated to match the refusal (config.rs). - An opening spanning more days than the weekly calendar expansion serves passed authoring and failed only at evaluation. The policy check now refuses the span at exactly the runtime boundary (registry-scheduling-core). The store also gains adopt(), the provisioning verb that claims the empty deployment identity the migration seeds, so a fresh database can reach a servable state; the runtime command wiring follows in the next commit. Review notes for the security-adjacent changes: the pool-anchor fix changes what the capacity transaction locks, not what it authorizes; the adopt verb writes the deployment identity under FOR UPDATE and refuses a mismatched claim, never overwriting one. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The scheduling binary becomes a service: migrate and serve. serve assembles the deployment the contracts describe and refuses to start half-provisioned. It verifies the policy package identity, refuses an empty or mismatched deployment identity before writing anything, keys the audit journal from one master secret through independently HKDF-derived chain and identifier sub-keys (AUDIT-03), and publishes the policy only when the digest moved. Four supervised workers run beside the listener — hold expiry, reminder dispatch, retention sweeps, and audit publication — and a worker that stops stops the process, so the runtime never keeps selling capacity its clocks no longer guard. The HTTP edge (http.rs) serves the fifteen pinned routes over the naming constants, types every refusal as a problem document with a trace id, and replays stored attempt receipts verbatim under their original status. Authentication (auth.rs) has no unauthenticated mode: a read requires the reads scope, explain its own scope, and every mutation a task grant whose scheduling bounds cover the offering's service, location, and action, re-checked inside the capacity transaction. Reminder dispatch is the one outbound hop: each due intent renders as one canonical CloudEvents 1.0 event and is POSTed to the frozen operator-configured destination, with no destination meaning intents stay recorded and local. The audit outbox publishes to the keyed chain with a crash-gap reconciliation that never duplicates an envelope. The operator configuration schema is generated from the strict types (schema.rs) and committed with a drift test, the same contract the other products hold. The PostgreSQL suite (tests/) drives the six commitments, availability, cursors, replay, the edge refusals, the unanchored-supply refusal, and deployment-identity adoption through the served router against a real database. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Scheduling joins the Registry Stack identifier catalog: the 32 problem codes publish under https://id.registrystack.org/problems/registry-scheduling/, exported by a core example beside the casework and BReg exporters, and the runtime configuration schema registers as an active v1alpha1 schema source generated from crates/registry-scheduling/src/schema.rs. The generated catalog is reproduced by products/identifiers/scripts/generate.py --write and verified by its check. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Scheduling becomes one of the five named runtime products, with its four crates on the repository map, its boundary paragraph beside the other products', and its gates on the verify-your-change list: the checkpoint script, the PostgreSQL suite under postgres-test, the identifier reference closure, and the schema generator command. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…erence The scheduling and schedulingctl command trees join the generated CLI reference: the catalog crate registers both, the generator knows their group, and the sidebar builder seats their pages. The review record moves to the catalog that includes them. The calendar extraction had also left two Casework evidence anchors citing the removed casework-core calendar module; both now cite the platform calendar file the evaluation moved into. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
run.sh builds the real scheduling and schedulingctl binaries, stands up a disposable TLS-enabled PostgreSQL (its own throwaway PKI, since the released runtime refuses plaintext database links), publishes the standalone-exact-time policy with demo environment records, and drives four acceptance scenarios against the served contract under real authentication: the two-caller race for the last station (AT-01), hold expiry (AT-05), lost-confirmation replay (AT-06), and the daylight-saving fold grid (AT-19). Tokens are real RFC 9068 access tokens signed by a throwaway RSA key the runtime verifies through its static JWKS source; mutations carry complete task grants the runtime re-checks inside the capacity transaction. The support module's pure pieces (JWK derivation, token minting, grant claims, records, runtime config, pinned scenario facts) carry unit tests. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…hecks generate_openapi.py writes the deterministic OpenAPI 3.1 document for the sixteen public routes and verifies it against the Rust truth beside it: the route inventory, handlers, authority profiles, bodies, parameters, and success statuses are parsed out of http.rs; the problem vocabulary, pinned titles, details, and statuses are read from the problem-catalog example's export, never copied; and every wire schema is compared field by field with the struct it projects. The per-operation problem mappings are the one hand-shaped table (the catalog exports no operations contract yet) and are verified for existence, pinned status, production reachability, and rule-derived minimums. check-checkpoint.sh runs the generator's --check first, so a drifted document fails the product gate. reserved-problems lists eligibility.unavailable and hook.unavailable, which the vocabulary defines and pins but no milestone path produces yet. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The development docset gains the Registry Scheduling product's API reference lane: the narrative contract page (authority profiles, task grants, idempotency, revision rules, problem codes, route families), the generated operations pages rendered from the product OpenAPI artifact, and the problem-code table generated from scheduling-api.yaml and cross-checked against the product problem catalogue by the new scheduling-api-reference test. Registration follows the Casework pattern: repo-docs and the latest docset name the product, fetch-openapi pulls its committed artifact, redocly and the starlight-openapi plugin render it, and docsets without the product (every archived set predating it) redirect the page to the current docset instead of serving it. The information-architecture test pins the product order, the docset gating, and the fetch rule. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Workspace.__init__ in ci_changes.py asserts the SHARDS inventory against the Cargo workspace and raises when a crate is unlisted. ci_event_routing.py constructs a Workspace unconditionally in the changes job, so the raise failed the entire PR gate closed with no test run. Add registry-platform-calendar to the platform shard: it is a shared, domain-neutral primitive (chrono/chrono-tz/thiserror only) consumed by registry-casework-core alongside the runtime and core crates, the same role every other registry-platform-* crate plays. Add a new scheduling shard mirroring casework's shape (core, runtime, ctl, client; Scheduling has no breg-adapter or node/py binding crates yet) for registry-scheduling-core, registry-scheduling, registry-schedulingctl, and registry-scheduling-client. python3 -m unittest discover -s .github/scripts -p 'test_ci_changes.py' goes from one error (raises before any test body runs) to 95 passing. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A suite that passes with no database proves nothing. The fixture now expects SCHEDULING_TEST_DATABASE_URL the way the sibling records-apply suite already does, so an absent database fails the binary instead of reporting ten skipped successes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…refusal The shared calendar crate reported a Casework noun for every product: a Scheduling opening pattern landing in a daylight-saving gap read back a refusal about a due time. The instant the conversion refused is a local time whatever the caller calls it. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…lished schedule Resolution concatenated each opening pattern's own expansion, so the list an evaluator reads kept the order the openings happened to be authored in. Both answers taken from that list are properties of a single interval, so the order decided which start times were on the grid, and an appointment running from one opening straight into the next was refused as unpublished though the location never closed across it. Sorting and joining intervals that overlap or meet answers the coverage question the same way for the evaluator and for the fixture harness. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The rebase onto main took the workspace to 0.32.0 and resolved Cargo.toml; the five scheduling crates' lock entries still carried 0.31.0 because they were written before it. Every scheduling crate reads its version from the workspace, so the lock now records the same 0.32.0 the manifests resolve to and --locked builds stop asking to rewrite it. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ransaction The admission request carried a caller-settable rescheduleOf that excluded any live claim from the capacity checks, so naming any active claim's id on a create path double-booked that member, bypassed the duplicate-booking refusal, and inflated window headroom. The mirror defect cut the other way: the reschedule transaction never supplied the appointment's own id, so moving an appointment inside the interval it already held was answered capacity.exhausted unless the caller volunteered the id themselves. The wire type no longer carries the field: AdmissionRequest drops rescheduleOf and strict request parsing refuses a body that names one, so no create path can be handed another party's allocation to ignore. The evaluators take the exclusion as their own argument, and the only supplier is the reschedule transaction, which reads the appointment's claim id from the row it locks. The authored fixture grammar keeps rescheduleOf, because a fixture case legitimately says this case reschedules that one; the harness passes the named claim as the evaluator argument and now refuses a dangling reference instead of replaying it as an ordinary booking. Fixture case names must also be unique, because one reschedule answering to two names frees two allocations. While the hash tuple is open, capabilities and prerequisites are sorted before hashing: two spellings of one set are one request, and a retry that reorders them replays instead of executing twice. Security review notes: this moves a caller-controlled input out of the capacity check entirely, which is a narrowing of what a request can express on every commitment route (hold create, appointment create, hold confirm). The reschedule path gains the self-exclusion it was missing, and the exclusion it now supplies is read under the supply lock from the appointment row the transaction holds, never from the request. Test evidence: the core pins the refused wire shape and the order-insensitive hash; the PostgreSQL suite pins a reschedule that moves inside its own occupied interval (admitted now, where it was capacity.exhausted before) and a create naming a live claim (422 request.unprocessable, and the standing appointment still holds its slot). The OpenAPI document drops the field with the wire type. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…rence precondition.failed no longer has a producer in the Scheduling runtime: an offering the deployed policy does not publish answers request.not-found, and a stale revision answers policy.changed or revision.mismatch. The published reference still told a caller to reload and retry something that no reload produces. The site cannot be run end to end until the CLI publication record is reviewed, so this change is verified against the data file's own shape: 32 rows matching ProblemCode::ALL, sorted, every code present in the Rust vocabulary. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
DX-16 asked for a dev verb over the runtime with the demo's provisioning shape. The demo's shape depends on Docker container lifecycle, TLS certificate generation, and JWT signing infrastructure (products/scheduling/demo/support/demo.py), so a resident process supervisor over it is a project-sized effort of its own, comparable to crates/registry-evidencectl/src/dev.rs. Record the deferral and point at the demo script as the current local development path. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…uers allowedClients being empty by default no longer admits every client unconditionally: behind listener.tlsTermination: operator-controlled-upstream an empty or absent list is a startup refusal (config.rs, a_production_deployment_must_name_the_clients_it_admits), and only development loopback keeps the old convenience. The runtime example still said otherwise. Also document the assertionIssuers key, added alongside that change, in the same commented style. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The claim ledger keeps a single supply column: an exact-time claim occupies the pool member it was assigned, a window claim occupies the window itself. The published documents decided which of the two they were holding from the ledger kind, which only ever says booking or hold, so the guard was always true and an arrival-window appointment reported the window id through the field documented as the pool member. Decide it from the offering's mode instead, and cover arrival windows end to end: unit allocation, the channel subquota ceiling, exhaustion, and a stale window revision. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The branch's prose convention is commas, colons, semicolons, periods or parentheses. Comment text only, no behaviour change. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ts grammar A clock that is not `HH:MM`, or a date that is not `YYYY-MM-DD`, was reported as an invalid bound. That reads as a number out of range, so adopter tooling treated it as unfinished authoring and let the check pass. Nothing an author adds turns `9:00 AM` into a clock: only a correction does, and the reason now says so. The check also names the field that actually carries the malformed text rather than always naming `startTime`, and leaves the range such a value belongs to unjudged, because comparing text that is not a clock answers a question the author never asked. `PolicyCheckReason::is_malformed_value` is what a caller asks to refuse the document rather than report it incomplete. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
registry-scheduling-core now raises PolicyCheckReason::MalformedValue, distinct from InvalidBound, for a clock or date whose text is outside its grammar. Nothing an author adds turns "9:00 AM" into a clock, only a correction does, so check and test must not treat it like ordinary incomplete authoring that --deny-findings alone gates. check now reports status: "invalid" for a malformed value and refuses regardless of --deny-findings; test reports authoringStatus: "invalid" the same way and skips fixture replay entirely, since a malformed value has no business being replayed against. The human-readable renderer names both new states in the product's voice. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
… deferrals The runtime now audits a permission refused before the capacity transaction, publishes the journal in the order it was written, bounds caller strings at the edge, admits only the RFC 9068 access-token type, requires a production deployment to name its clients, binds an exchanged credential to a declared authority, and fails closed on an unreadable clock. Promote SCHEDULING-SEC-14 to enforced and retire SCHEDULING-DEF-02 and SCHEDULING-DEF-03, whose compensating controls describe behaviour the runtime no longer has. Add five invariant rows with the tests that earn them, the matching traceability entries, the review notes the changes are security-sensitive enough to require, and the recorded decision behind a records swap that refuses to strand a live claim. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The runtime now writes an authorization record when the service refuses a permission before the capacity transaction opens, so the sentence that named only the commitment was true but incomplete. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Finishes the pass that cleaned the store and runtime prose. The two occurrences in registry-schedulingctl/src/records.rs stay for the lane that owns that crate. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The last em-dash holdout in the scheduling family, inside schedulingctl's own ownership boundary. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…table Add a second standalone-arrival-window offering, opening, and window: a Saturday afternoon household block sized by a banded units table instead of a flat per-recipient rate. Its aboveHighestBand policy refuses a party larger than the table's highest band rather than costing it by guess, so an adopter starting from the template has a working example of both units policies the grammar supports. The accompanying fixture proves a household in the first band, a household above the highest band refused with party.capacity-inadequate, and a household in the second band, all replaying clean offline. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ree builds Cargo unifies the features a dependency entry names across one invocation, so asking for `registry-scheduling/postgres-test` in this crate's dev-dependencies turned the feature on for every build that holds both crates. That satisfied the runtime's own `required-features` gate, and the shared test shard, which has no PostgreSQL server, compiled and ran `postgres_commitments`. The database targets here already declare `required-features = ["postgres-test"]` against this crate's own feature, which chains to the runtime's, so the gates that mean to run them still get the plaintext test connection and nothing else does. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A dependency entry naming a sibling's feature is the same as passing that feature on the command line for every build that selects both crates, so one crate can silently select another's database-only test targets and hand them to a job with no server. The checker reads `cargo metadata`, maps each Scheduling test target to the feature that gates it, and reports any Scheduling dependency entry that turns such a gate on. The checkpoint runs it beside the dependency-direction guard. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`intents_postgres` needs a PostgreSQL server and was executed by no job, so the undelivered delivery intent read had no proof in CI. It runs in the Scheduling PostgreSQL job on its own database, the way the authoring record application suite does, because both are destructive. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The Casework image-pin gate named a bare digest, and the Scheduling PostgreSQL job pins the same image, so removing Casework's pin left the gate satisfied by Scheduling's copy and its own probe stopped failing. Each pin now names the job it belongs to, Scheduling's pin is declared beside Casework's, and the Scheduling workflow and checkpoint gates get the removal probes the Casework ones already have, including the delivery intent suite and the database test isolation guard. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The record still carried the 0.31.0 source version and digests taken before the adopter tooling settled its report flag, so the generated reference stayed a draft and the publication gate refused it. Reviewed the published surface against main: the only catalog change is the two added binaries, `scheduling` and `schedulingctl`, with every other command tree byte-identical. Across both, every report option is the shared `--format` accepting `human` or `json` with `human` as the default, `check` carries `--deny-findings`, `intents` defaults its limit to 50, and no option publishes an environment variable, a filesystem default, or a domain term. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…reSQL keeps The seeded base time came straight from the clock, so it carried a nanosecond tail that `timestamptz` drops on write. The assertion then compared the microsecond value the command read back against the nanosecond value still held in memory, and the two never matched on a platform whose realtime clock resolves finer than a microsecond. Truncating the base time to microseconds makes the seeded instant and the reported `dueAt` the same value on any clock. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…vailability truth Close the review findings that admitted concurrent lifecycle writers, stamped cached policy evaluations, resolved stale supply facts, leased no dispatch, suppressed nothing already loaded, and disagreed between availability and admission about the published schedule. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…lease The authorization clock a mutation answers under is observed once more after its tentative writes, immediately before the commit that lands them, so a grant that lapses while a write waits for a lock commits nothing; the six mutation paths and the regressive test that crosses the boundary per operation pin it. Cancellation now guards the policy its cutoff is evaluated under, the same guard admission answers to. The dispatch liveness read refuses an intent whose remaining lease cannot cover one more send deadline, so an overrunning pass hands the send over instead of racing the reclaiming one. Co-Authored-By: Claude Code <noreply@anthropic.com> 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>
7ed0254 to
7be7edc
Compare
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>
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".
| if current_policy != commitment.policy_revision { | ||
| return Err(AdmissionRefusal::PolicyChanged.into()); |
There was a problem hiding this comment.
Remove the cancellation policy-revision fence
When an operator publishes a new policy while an older runtime instance is still serving, commitment.policy_revision remains the cached revision and this branch returns policy.changed, rejecting an otherwise valid cancellation until that instance restarts. This directly contradicts SCHEDULING-DEC-02, the security matrix, and the generated OpenAPI, which promise that cancellation is guarded by the appointment revision and cutoff but never by policy revision; it can therefore trap callers in bookings during a rollout while returning an undocumented problem code.
AGENTS.md reference: products/scheduling/AGENTS.md:L13-L15
Useful? React with 👍 / 👎.
| .reschedule_appointment( | ||
| appointment_id, | ||
| offering, | ||
| &supply.context(), | ||
| &request.admission, | ||
| request.observed_revision, |
There was a problem hiding this comment.
Reject mismatched offerings when rescheduling
RescheduleAppointmentRequest.admission requires an offering, but this path resolves authorization and supply from the existing appointment and passes the caller's admission to an evaluator that never compares its offering field. A request naming a different or nonexistent offering can therefore return 200 while moving the appointment under its original offering, service, and location, so the response does not perform the operation the request describes. Either validate equality with appointment.offering or use a reschedule-specific admission shape that omits the field.
Useful? React with 👍 / 👎.
| let pool_ids = offering_pool_ids(&policy); | ||
| let window_ids = offering_window_ids(&policy); | ||
| let policy_revision = store | ||
| .apply_policy(&scheduling_id, &policy_digest, &pool_ids, &window_ids) | ||
| .await | ||
| .map_err(database_step("policy publication"))?; |
There was a problem hiding this comment.
Fence policy publication against standing commitments
When a replacement policy reduces a published window or channel subquota below its live holds/bookings, moves the window away from those claims, or removes it, startup reaches this call and publishes the new digest and revision without inspecting the ledger. The repository already provides assess_publication_impact specifically to reject such changes, but a repo-wide search shows it is called only by its own unit tests; apply_policy receives neither the old policy nor a snapshot. Consequently a normal restart can successfully publish capacity below existing commitments and leave claims outside the supply the service now advertises.
Useful? React with 👍 / 👎.
| self.consuming(now).any(|claim| { | ||
| claim.duplicate_key.as_deref() == Some(duplicate_key) | ||
| && Some(claim.id.as_str()) != exclude | ||
| }) |
There was a problem hiding this comment.
Scope duplicate-active checks to the offering
For offerings that share a resource pool or published window, the snapshot contains claims from every offering using that supply, but this predicate compares only the caller-provided duplicate key. A booking for offering A therefore causes a request for offering B with the same subject-derived key to be refused as booking.duplicate-active, even though the migration explicitly describes duplicate enforcement as offering-scoped and avoids a database uniqueness constraint for that reason. Carry the offering through LedgerClaim and include it in this comparison.
Useful? React with 👍 / 👎.
| let capable: Vec<&PoolMember> = members | ||
| .iter() | ||
| .filter(|member| member.serves(&offering.requires_capabilities)) | ||
| .collect(); |
There was a problem hiding this comment.
Apply the admission request's capability set
AdmissionRequest.capabilities is a required public field documented as the party's held capabilities, but neither admission evaluator uses it: this exact-time branch checks only each resource member against offering.requires_capabilities, and the arrival-window path performs no capability check at all. Thus a request with an empty or unrelated capability set is admitted exactly like one carrying the required values, while the field only changes its idempotency hash. Validate the request set against the offering requirements, or remove the field from the public request if it is not part of admission.
Useful? React with 👍 / 👎.
| let earliest = open | ||
| .iter() | ||
| .map(|interval| interval.start) | ||
| .min() | ||
| .unwrap_or(now); |
There was a problem hiding this comment.
Include buffers in the locked snapshot range
For an exact-time offering, the transaction loads claims only between the raw opening start and end even though admission occupies time before and after that interval through buffer_before_minutes and buffer_after_minutes. If another offering sharing the pool has a booking that ends after this offering's buffered start but before its opening starts, that claim is omitted from the snapshot and the first slot can allocate the same resource during the overlap; the symmetric case exists after the opening end. Widen the transaction snapshot by the exact-time duration and buffers, as the read-only availability path already does.
AGENTS.md reference: AGENTS.md:L147-L153
Useful? React with 👍 / 👎.
| .pools | ||
| .iter() | ||
| .flat_map(|pool| pool.members.iter()) | ||
| .map(|member| member.resource_id.clone()) | ||
| .collect(); |
There was a problem hiding this comment.
Refuse moving occupied resources between pools
The records replacement guard reduces incoming pool membership to a flat set of resource IDs, so moving an occupied member from pool A to pool B passes as long as its ID remains present. The existing claim is then invisible to pool B's snapshot while still naming that member, and a concurrent or later booking under pool B can allocate the same resource; hold confirmation is also serialized on the old offering's pool rather than the member's new pool. Compare each occupied resource's current and incoming pool as well as its presence before allowing the wholesale swap.
AGENTS.md reference: AGENTS.md:L147-L153
Useful? React with 👍 / 👎.
| "INSERT INTO scheduling_attempts(attempt_id, actor_issuer, actor_subject, scope, \ | ||
| idempotency_key, request_hash, state, status_code, receipt, expires_at) \ | ||
| VALUES($1,$2,$3,$4,$5,$6,'refused',$7,$8,$9) ON CONFLICT DO NOTHING", |
There was a problem hiding this comment.
Replay the winning receipt after an attempt conflict
With two concurrent identical requests using the same idempotency key, both can initially see no attempt; the winner commits a booking, while the loser subsequently acquires the supply lock, observes exhausted capacity, and reaches this refused-attempt insert. ON CONFLICT DO NOTHING silently discards the loser's receipt, but the service still returns its local 409 response, whereas the next retry replays the winner's 201 appointment. Detect the zero-row insert and load/replay the stored attempt instead of returning a response that contradicts the key's durable result.
Useful? React with 👍 / 👎.
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.