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".
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>
…t admits The runtime now refuses to start when a deployment behind an operator-controlled terminator names no clients, and refuses an exchanged token whose assertion authority no entry declares. The configuration reference still told an operator that an empty client list admits every client the issuer verifies, which would now be a startup failure, and said nothing about the authority map at all. 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>
…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>
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>
4bf86bc to
34deb3e
Compare
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: 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".
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: 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".
| let rows = transaction | ||
| .query(CONSUMING_CLAUSES, &[&member_ids, &earliest, &latest, &now]) | ||
| .await?; |
There was a problem hiding this comment.
Check duplicate keys beyond the current pool
When an exact-time offering with an active booking is republished to use a different pool, which apply_policy currently permits, this snapshot contains only members of the new pool. The old claim therefore never reaches check_duplicate, so a request carrying the same offering-scoped duplicate key can create a second active booking. Either query active duplicate keys independently of the current supply snapshot or prevent pool changes while such claims remain active.
Useful? React with 👍 / 👎.
| let text = crate::project::read_authoring_input(&records_path)?; | ||
| let facts = parse_records(&text)?; | ||
| validate(&facts)?; |
There was a problem hiding this comment.
Validate exception layers before replacing records
When a records document contains a semantically invalid exception layer—such as an opening without reopens/authority, an opening naming an unknown closure, or a closure carrying reopening fields—this validation accepts it and the transaction commits it. Subsequent exact-time availability and commitment requests at that location fail during location_open_intervals and return service.unavailable until the operator repairs the records. Run the calendar layer validation against the loaded policy before replacing the live facts.
Useful? React with 👍 / 👎.
| if let (Some(start), Some(end)) = (start, end) { | ||
| if end <= start { | ||
| return Err(SchedulingClientError::invalid_request( | ||
| "the availability interval ends before it starts", | ||
| )); |
There was a problem hiding this comment.
Preserve availability range normalization in the client
When callers provide end <= start, the HTTP contract explicitly accepts the request and raises the effective end to one minute after start, but the maintained Rust client rejects it locally and never sends it. This makes documented server behavior inaccessible through the canonical SDK; remove this stricter check or align the published contract and runtime to reject the range too.
Useful? React with 👍 / 👎.
| if request.window_revision != Some(window.revision) { | ||
| return Err(AdmissionRefusal::RevisionMismatch { | ||
| observed: window.revision, | ||
| }); | ||
| } | ||
| check_horizon(window.start, *now, *lead_time_minutes, *horizon_days)?; |
There was a problem hiding this comment.
Match arrival requests to the requested start
For an arrival-window offering, the evaluator never compares request.start with the published window start: it checks the horizon using window.start and returns an admission at that same window start. A caller can therefore request any timestamp and receive a successful booking for a different time, while that ignored timestamp still changes the idempotency hash. Require the requested start to identify the selected window, or remove it from the arrival-window request shape.
Useful? React with 👍 / 👎.
| if request.party.recipients == 0 || request.party.recipients > exact.max_recipients { | ||
| return Err(AdmissionRefusal::PartyCapacityInadequate); | ||
| } | ||
| check_capabilities(offering, request)?; |
There was a problem hiding this comment.
Reject impossible party counts
When attendees is zero or smaller than recipients, both admission evaluators still accept the request as long as the recipient count satisfies capacity. This contradicts PartyCounts' definition that attendees include every recipient, so inputs such as two recipients and zero attendees can produce a committed hold or appointment. Validate attendees >= recipients (and therefore nonzero for an admitted party) before evaluating capacity.
Useful? React with 👍 / 👎.
| let window = policy | ||
| .window(&arrival.window) | ||
| .ok_or_else(|| operator_gap(&arrival.window))?; |
There was a problem hiding this comment.
Resolve arrival windows from operator records
Every arrival-window commitment obtains its dates, revision, and capacity directly from the authored policy, while SchedulingFacts contains no window records and startup automatically creates the corresponding lock row. Consequently publishing policy is sufficient to invent bookable window supply without the independent operator-anchored runtime record required by the product boundary. Move published-window facts into the operator record layer and resolve them there before opening the capacity transaction.
AGENTS.md reference: AGENTS.md:L147-L150
Useful? React with 👍 / 👎.
| Ok(Self::Window { | ||
| window: window.clone(), | ||
| lead_time_minutes: arrival.lead_time_minutes, | ||
| horizon_days: arrival.horizon_days, | ||
| channels: policy.channels.clone(), |
There was a problem hiding this comment.
Apply location closures to arrival windows
When an operator applies a valid closure covering an arrival window, this branch builds the window context without the location's effective openings or closures, so availability continues advertising the window and holds or appointments still commit. The offline fixture checker explicitly requires each window to be covered by location_open_intervals, but production admission does not preserve that rule; resolve the calendar here and refuse or hide windows no longer covered by the effective schedule.
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.