feat: close six authoring and contract tickets across BReg and Casework - #1271
Conversation
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: 57a303a8b3
ℹ️ 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".
57a303a to
ffffb34
Compare
There was a problem hiding this comment.
💡 Codex Review
registry-stack/crates/registry-bregctl/src/lib.rs
Lines 8077 to 8078 in ffffb34
For a package containing a WASM action handler, a server built without the wasm feature cannot load the package and defer failure until invocation as this explanation claims. load_package re-derives the package through compile_project_with_assets, and the featureless compile_handler path emits action.handler.wasm_build_unsupported, causing package derivation to fail during load. Because this adopter-facing statement is also pinned as a schema constant, it gives operators the wrong deployment compatibility guarantee; describe the load-time refusal instead.
ℹ️ 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".
Inspecting a Casework policy's digest and file list required running `caseworkctl package` against a real output directory, so a caller who only wanted to compare digests had to create and clean up a package tree. Add `--dry-run`, mutually exclusive with `--output` through clap so the misuse is a usage error rather than a runtime refusal. It runs every validation and refusal the written path runs, reports the same `policyDigest` and `files`, sets `dryRun: true`, and omits `output` entirely. The shared work moves into `compute_package` so the two paths cannot drift. The checkpoint gate now asserts that equivalence. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`bregctl explain routes` emits entity routes and action routes in one array. They share no field, and nothing on a record said which shape it was, so the obvious consumer that reads `entityId` on every route crashed on any project declaring an immediate action. Action routes sort last, so the failure landed on the final element and read as truncated output rather than a second record shape. Every record now carries `kind`, either `entity` or `action`. It is distinct from `actionRouteKind`, which says which action route a record is rather than which shape it has; `action` also covers the target-conditions routes sharing the array. The field lives in the explain payload, not on `CompiledRoute` or `CompiledActionRoute`, which are `deny_unknown_fields` types that round-trip through package inventories. This also removes an early return that skipped the whole pass for action-free projects, which would have left those records with no discriminator at all. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`bregctl init --template <id>` shipped its four starters as pretty-printed JSON in files named `.yaml`. They parsed, since YAML is a superset of JSON, but the first thing an adopter opened looked nothing like the commented YAML that plain `bregctl init` and `--from publicschema` already write, and nothing in it invited editing. Convert all sixteen starter files to block style: one key per line, sequences as `-` items, no flow collections at any depth. Each `registry.yaml` gains a short header naming what the file is and calling out `canonicalBaseIri` as a reserved example.org stand-in to replace. The conversion is semantically identical, verified by a type-preserving round-trip against the previous bytes. Scalars that YAML would otherwise resolve to another type are quoted: `'0.000'` and `'100.000'` would become floats, `'2026-01-01'` a date, and an empty `authorizationConditions` would become null. `starter_projects.rs` reads these files as YAML rather than JSON, since they are no longer both. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…view policy A BReg source description pins `review.policyId`, but nothing offline confirmed that id still resolves against the project's declared `reviewKinds` and `reviewProducers`. `validate_breg_source_description` only checks the description's shape against the closed adapter contract and forwards the id unchecked, so removing, renaming, or repurposing a `reviewKinds` entry, or dropping the producer that admits the pair, left the binding broken with no signal at authoring time. At runtime the consequence is silent. `enqueue_submission` copies the pinned id verbatim into the review request, the runtime finds no matching policy and refuses with 403, and `terminal_submission_error` treats any 400-499 except 429 as terminal: the row is set to `failed` with `remote-refused` and never retried. The review dead-letters permanently and the author is told nothing. `check_source_descriptions` now re-derives the four assertions `caseworkctl source add` makes when a binding is first written: the named policy exists, its purpose is approval, its contextStrategy is source, and exactly one `reviewProducers[]` entry admits this source's id for the pinned kind. Because `check`, `explain`, `simulate`, `package`, and `package --dry-run` all route through it, every one of them refuses the same broken binding. The producer assertion carries two distinct refusals because the two faults are not the same fault. No admitting producer breaks the binding the way a missing policy does: the runtime forbids a submission whose producer covers neither the submitted kind nor the subject's source namespace, and it dead-letters as above. More than one admitting producer is not a runtime fault at all, since the runtime resolves a producer by actor identity; it is `source add` that refuses to choose between two, so a project in that state can no longer be repinned by the command that wrote its binding. The check is offline and claims only what it can prove. It compares the pinned description against the policy on disk right now; it does not re-derive `sourceRevision`, so it cannot detect drift in the BReg `registry.yaml` the description was compiled from. Every refusal names the pinned `sourceRevision` and is worded to say the binding is broken as pinned, never that the pin has been verified current. The `multi-stage-routing-clocks` example carried the bug it now demonstrates: both its source descriptions pinned `regional-review` while the project declared no `reviewKinds` at all, and the matching `casework.id` was a naming coincidence, not a resolution rule. It gains the requester access profile, the `regional-review` kind, and the producer that binds them. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A compiled entity recorded nothing about where it came from. For a project that composes modules, `bregctl explain` showed an entity identical to one declared at the project root, so an author reading the compiled output could not tell which module owned an entity, nor which module contributed an individual field, constraint, hook, index, access profile, selector profile, read path, or derived relation. `CompiledEntity` now carries `sourceModule`, the module that declared the entity, and `moduleOrigins`, one map per id-keyed collection recording which module contributed each id. Attribution follows declaration: the declaring module owns the entity and everything it declares, and an extending module owns exactly the ids it appends. An id absent from a map came from the project root. Both fields are omitted when empty, so a project that uses no modules serializes byte-identically to before. The compiler previously threaded origin information through separate parameters; they are folded into one `CollectedOrigins` value passed through `collect_entities`, `apply_extensions`, and `compile_entities`. One consequence is not visible in any committed artifact and is worth stating plainly. `CompiledRegistry.revision` digests the whole generated artifact map, which always includes `compiled/effective-model.json`, and that file serializes `CompiledEntity` verbatim. The revision therefore changes for a module-using project even though every committed generated baseline is byte-identical, because no fixture commits `compiled/effective-model.json` and the `generate <selector>` path never writes it. An operator who records or diffs the revision reported by `bregctl generate` for a module-using project will see it move on upgrade. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`bregctl explain` prints JSON that adopters already parse, but nothing described its shape and nothing stopped a refactor from renaming a key. A reader could not tell which payload they were holding, nor which keys were safe to depend on. Every `explanation` object now opens with an envelope, `apiVersion: registry.registrystack.org/breg-explain/v1alpha1` and a `kind` naming one of eight shapes, and each shape is authored as a JSON Schema under `products/breg/contracts/explain/`. The envelope is inserted into the serialized explanation rather than declared on `SuccessReport`, because `explanation` is a slot that `project lock` and `generate` also populate; their output is unchanged. The contract distinguishes two kinds of key. A key bregctl builds field by field is pinned: required, typed, and sealed with `additionalProperties: false`. A key bregctl produces by serializing a compiled-model value straight through is declared opaque, typed only as an object or array, because its shape follows that Rust type and can change with no corresponding line changing in bregctl. Pinning a passthrough would assert a contract nothing enforces. The README names every opaque node and the three branches no tracked fixture exercises. `tests/explain_contract.rs` keeps the contract honest. It runs the real binary over eleven fixtures and two access-review scenarios, 79 payloads in total, and validates each against its schema file, so drift between `explain_*` and the authored contract fails here rather than at an adopter. `check-contracts.sh` runs it. One consumer-visible consequence: every explain payload gains two keys. A caller asserting an exact key set, as `explain_routes_preserves_action_free_output_shape` did, now sees `apiVersion` and `kind` alongside what it expected. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`caseworkctl package` is the only public command content this branch changes: `--output` is no longer unconditionally required and its help now says "Required unless --dry-run", `--dry-run` is new, and the two are mutually exclusive. Those are the only clap attribute changes in the workspace across all six commits. The catalog and content digests are what `npm run cli-reference:digest` reports after that review. `reviewed_source_version` stays `0.32.0` and `last_reviewed` stays `2026-09-21`, because this is a content review of an unreleased change, not a version bump. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
ffffb34 to
3a9f701
Compare
The source-description review check refused a project whose casework.yaml declares more than one reviewProducers[] entry admitting a source id for a pinned review kind. A failover integration beside a primary one is exactly that shape, and the runtime supports it: ReviewRuntime::producer_for_actor resolves a producer by the authenticated actor's profile, issuer, and subject, never by uniqueness, so both identities submit under the same policy and neither shadows the other. Only `caseworkctl source add` needs exactly one, because it has to choose the credentials it writes into the description, and it keeps its own rule. Mirroring that count into check, explain, and package turned a working project into a refused one on all three commands. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ey policy Three defects in the published explain contract, all of which would refuse a valid payload or promise something the schemas do not keep. ModelExplanation required `package` and `manifestProjection` to be objects. Both are `Option` on CompiledRegistry (model.rs), compilation succeeds with a finding when a project omits them, and `explain_model` serializes them as null, so a valid `bregctl explain model` response failed the gate. AccessExplanation typed `claimContractError` as an object. AuthorityInventoryError is a value-free enum carrying `rename_all = "snake_case"`, so it serializes as a bare string: the schema rejected the one payload that field exists to expose. It is now pinned as its eight-member string vocabulary plus null, which moves it out of the opaque list. The compatibility promise allowed adding an optional key to a pinned object without an apiVersion bump, but every pinned object seals itself with `additionalProperties: false`, so a consumer holding one version's schema rejects a response carrying the next version's new key. Adding a key to a pinned object is a bump. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Six ranked authoring and contract tickets, one commit each, in the order they were ranked.
feat(casework): read a policy digest without writing a packagefix(breg): discriminate the two route shapes in explain routesexplain routesmixed two record shapes with no discriminatorfix(breg): write starter projects as block-style YAMLbregctl init --templatewrote JSON into.yamlfilesfix(casework): refuse a source description pinning an unresolvable review policyreview.policyIdbinding dead-lettered silently at runtimefeat(breg): attribute module-declared entities and their membersfeat(breg): publish the explain payloads as a versioned wire contractPlus a final
docs(cli):commit updating the CLI publication record, which is a separate owning area (docs/site/).Consequences worth reading before merge
#1259 moves the revision digest for module-using projects.
CompiledRegistry.revisiondigests the whole generated artifact map, which always includescompiled/effective-model.json, and that file serializesCompiledEntityverbatim. AddingsourceModuleandmoduleOriginstherefore changes the revision for any project that composes modules, even though every committed generated baseline is byte-identical — no fixture commitscompiled/effective-model.json, and thegenerate <selector>path never writes it.check-generated.shcannot see this. An operator who records or diffs the revision reported bybregctl generatewill see it move on upgrade. Module-free projects are unaffected: both fields are omitted when empty, so those serialize byte-identically.There is a sharper consequence than an operator diffing a revision. A Casework BReg source description pins its registry revision:
sourceRevisionis read intoexpected_registry_revision(crates/registry-casework-breg/src/config.rs:340), and the adapter compares it against the live value on every metadata read, returningSourceAdapterError::BindingMovedon a mismatch (crates/registry-casework-breg/src/lib.rs:305). For a module-using deployment, recompiling the registry on this engine moves that revision, so an existing, working source binding starts refusing at runtime. Nothing offline catches it first:caseworkctl checkdoes not re-derivesourceRevision. Those descriptions need repinning as part of the upgrade.#1256 re-derives all four assertions
caseworkctl source addmakes, not three. The namedreviewKindsentry exists, its purpose is approval, its contextStrategy is source, and exactly onereviewProducers[]entry admits this source's id for the pinned kind. The producer assertion carries two distinct refusals because the two faults are not the same fault. No admitting producer breaks the binding the way a missing policy does: the runtime forbids a submission whose producer covers neither the submitted kind nor the subject's source namespace, and it dead-letters. More than one admitting producer is not a runtime fault at all, since the runtime resolves a producer by actor identity; it issource addthat refuses to choose between two, so a project in that state can no longer be repinned by the command that wrote its binding.#1256 claims only what it can prove. The new check is offline. It compares the pinned description against the policy on disk right now; it does not re-derive
sourceRevision, so it cannot detect drift in the BRegregistry.yamlthe description was compiled from. Every refusal names the pinnedsourceRevisionand is worded to say the binding is broken as pinned, never that the pin has been verified current.#1256's refusal text does not currently reach the author, and that is pre-existing.
caseworkctl checkclassifies anyanyhowerror that is not an io failure, aRuntimeConfigError, aConfigLoadErroror aConfigErrorinto a catch-all bucket, so all five new refusals are collapsed toerror[caseworkctl.refused] runtime: The Casework command was refused because an authored input did not satisfy its contract.withnext: Correct the unavailable runtime dependency, then retry.The--format jsondiagnostic drops the detail too. Verified on this head by breaking apolicyIdand running the command; the classifier atcrates/registry-caseworkctl/src/lib.rs:447-453and:481-486is untouched by this branch. The refusal and the non-zero exit are correct and the wording is right in the source, but an author reading stdout is pointed at infrastructure rather than at their YAML. Filed as #1272 rather than scoped here, since fixing it changes the message of every caseworkctl refusal.#1256 fixes an example that carried the bug it demonstrates.
products/casework/examples/multi-stage-routing-clockspinnedregional-reviewfrom both source descriptions while declaring noreviewKindsat all. The matchingcasework.idwas a naming coincidence, not a resolution rule. The example gains the requester access profile, theregional-reviewkind, and the producer that binds them.#1258's YAML conversion is semantically identical, verified by a type-preserving round-trip against the previous bytes. Scalars YAML would otherwise resolve to another type are quoted:
'0.000'and'100.000'would become floats,'2026-01-01'a date, and an emptyauthorizationConditionswould become null.CLI publication record
#1260 is the only change on this branch that touches public command content; across all commits the only clap attribute changes in the workspace are the two in
crates/registry-caseworkctl/src/lib.rs. The delta tocaseworkctl package:--outputis no longer always required, and its help gains "Required unless --dry-run"--dry-runoptionmutually_exclusiveconstraint over the twodocs/site/src/data/cli-reference.yamlis updated with the digestsnpm run cli-reference:digestreports after that review.reviewed_source_versionstays0.32.0.#1248: what the explain contract does and does not promise
Every
explanationobject now opens withapiVersion: registry.registrystack.org/breg-explain/v1alpha1and akindnaming one ofeight shapes, each authored as a JSON Schema under
products/breg/contracts/explain/.The envelope is inserted into the serialized explanation rather than declared
on
SuccessReport, becauseexplanationis a slotproject lockandgeneratealso populate. Their output is unchanged.Consumer-visible: every explain payload gains two keys. A caller
asserting an exact key set sees
apiVersionandkindalongside what itexpected;
explain_routes_preserves_action_free_output_shapeis the onein-tree example and is updated.
Additive on the wire. The runtime change is a single insertion at one
call site: two keys added to an already-serialized object, which cannot
alter, drop or reorder anything else. A consumer outside this repository
re-captured the explain payloads it reads on this head, seven of the eight
kinds across two projects, and reports the wire diff is exactly the two added
keys. That is that consumer's own gate result, not re-run here, and it does
not cover
AccessPreview.Pinned is not the same as sealed everywhere, and that is deliberate. A key
bregctl builds field by field is pinned: required, typed, sealed with
additionalProperties: false. A key bregctl produces by serializing acompiled-model value straight through is declared opaque, typed only as an
object or array. A passthrough's shape follows its Rust type and can change
with no line of bregctl changing, so pinning it would assert a contract
nothing enforces. The README names every opaque node. The ones worth knowing
before you rely on this:
EventsExplanationis opaque past the envelope.deliveriesis a singleraw passthrough of
CompiledEventDeliveryInventory, so this schemapromises little beyond the envelope today.
RoutesExplanation's entity-route half is opaque except thekinddiscriminator explain routes returns a union of two record shapes with no discriminator #1257 added. Only the action-route half is hand-rolled and
fully pinned.
ActionsExplanation.handler.possibleWritesand.handler.limitsareopaque, while the equivalent
ChangeRequestsExplanation.requests[].plannerfields are pinned in full, because that path builds them field by field.
fieldTypeis opaque everywhere it appears, even inside otherwise sealedobjects.
Three branches are pinned from source but unexercised by any tracked
fixture, recorded in the README rather than hidden: the WASM handler branch
in
ActionsExplanation, the"clear"field-mutation kind inActionsExplanationandChangeRequestsExplanation, andrevisionKindon arevisions route.
The gate,
crates/registry-bregctl/tests/explain_contract.rs, runs the realbinary over 11 fixtures and 2 access-review scenarios, 79 payloads, and
validates each against its schema file.
check-contracts.shruns it, and the9 new files are registered in
artifact-inventory.yaml.Verification
cargo fmt --checkcargo check --locked --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningscargo test --locked -p registry-breg --features postgres-test --libproducts/casework/scripts/check-checkpoint.shproducts/breg/scripts/check-contracts.shcargo test --locked -p registry-bregctlexplain_contract.rsThe
registry-bregsuite is run withpostgres-testand a live database onpurpose. Under
--workspace, cargo feature unification enablesregistry-breg/postgres-testthroughregistry-casework, so a barecargo test --workspacefails 8 tests withBREG_TEST_DATABASE_URL is requiredunless a database is present. CI shards per package and never hitsthis. The condition is pre-existing and untouched by this PR; it is recorded
here so the next person does not read those failures as a regression, and
filed as #1273.
The casework checkpoint gate needed a macOS-only workaround to run at all: a
directly executed
caseworkctlaborts because the FIPS build of AWS-LC is adylib with no rpath on Apple targets. That is unrelated to this PR and is
filed as #1270. The gate result above is a genuine pass, not a skip.
Not in this PR
#1261 (released professional-review example carries no
reviewKinds) is not code. It is release sequencing: it resolves itself if #1189 is in the next cut. Nothing here addresses it, and nothing here blocks it.Review round, 2026-09-22
Seven threads from the review connector. Four were real defects in this
branch and are fixed here, in two commits.
fix(casework): accept several producer identities for one source bindingwithdraws the exactly-one
reviewProducers[]rule that the#1256commitadded to
check_source_review_binding. It was wrong.ReviewRuntime::producer_for_actorresolves a producer by the authenticated actor's profile, issuer, and subject,
never by uniqueness, so two identities admitting one source id for one pinned
kind are a working failover pair. Mirroring
source add's count alongside itsfilter turned that valid project into a refusal on
check,explain, andpackageat once. The filter is still mirrored; the count is not.source addkeeps its own rule, because it has to choose the credentials it pins. The test
asserting the refusal is now its inverse.
This corrects the fourth assertion as described in this PR's own body: zero
admitting producers is still a genuine dead letter and still refuses, but more
than one was never a runtime fault.
fix(breg): correct the explain contract's optional passthroughs and key policyfixes three contract defects, two of which meant a valid payload failed the
gate published in this same PR:
ModelExplanationrequiredpackageandmanifestProjectionto be objects.Both are
OptiononCompiledRegistry, andexplain_modelserializes themas
nullfor a project that declares neither. Now["object", "null"].AccessExplanationtypedclaimContractErroras an object.AuthorityInventoryErroris a value-free enum underrename_all = "snake_case", so it serializes as a bare string: the schemarejected the one payload that field exists to expose. Now pinned as its
eight-member string vocabulary plus
null, which moves it out of theopaque list.
needs no
apiVersionbump, while every pinned object seals itself withadditionalProperties: false. A consumer holding this version's schemawould reject exactly the addition the policy called compatible. Adding any
key to a pinned object is now a bump, and the paragraph says why.
The remaining three threads are recorded rather than fixed, each for a stated
reason: #1277 (module provenance omits attachment slots, pre-existing and
in the compiler rather than this surface), #1278 (
ActionsExplanationdoesnot tie backend fields to
handler.kind, and four branches the gate pins fromsource but never replays; the
oneOfwants the WASM fixture to land with it),and #1272 (every authoring refusal is flattened to
caseworkctl.refusedacross 79
bail!sites, which belongs at the classifier, not at one call site).