The brain's contracts stop being typed twice
The pipeline
pramaan-agents @ 35adb805 (Story-3563 — 12 canonical JSON Schemas) │ byte-diffed, BOTH directions, by the required check ▼ src/shared/agent_contracts/_schemas/*.v1.schema.json ← vendored INPUT │ make gen-agent-contracts (datamodel-code-generator 0.72.1, pinned flags) ▼ src/shared/agent_contracts/_generated/*.py ← generated OUTPUT │ git diff --exit-code → exit 1 on ANY difference ▼ `Agent contracts codegen` (required status context)
The sha lives in exactly one file, .github/agent-contracts-pin.txt,
which is what the generator reads. The workflow carries a literal copy only
because tests/unit/test_gate_location.py requires a required
job's cross-repo ref: to be a 40-character sha rather than an
expression it cannot read — and
test_the_workflow_checkout_ref_is_the_pin holds the two equal, so
R6's "two writers for one resource" does not reappear here. Never
ref: main: that is the mechanism behind the 2026-08-04 repo-wide
push blockage.
What this check asserts, exactly
Every file under src/shared/agent_contracts/_generated/ is
byte-identical to a regeneration from the canonical schemas vendored under
src/shared/agent_contracts/_schemas/, and those schemas are
byte-identical to pramaan-agents at the pinned commit, in both directions —
a schema vendored here with no producer fails, and a schema published there
and not vendored here fails. It does NOT assert that the
pinned schemas match the deployed producer (that is W3.2), and it
does not assert that any consumer uses the generated models (that is W2.4,
pramaan-spec#3565).
A byte diff proves the pipeline, not the meaning
A flag change that quietly dropped every maxLength would
regenerate cleanly and diff clean forever. So
test_generated_schema_fidelity.py regenerates the JSON Schema
from each generated model and compares it to the artifact it came
from. Five differences between the two documents are representational and are
normalized on both sides — required ordering, 1 vs
1.0, hoisted enum $defs, const vs
single-member enum, and the root $schema dialect
line. Nothing else: description, default,
additionalProperties, every constraint keyword and every
required membership are compared verbatim.
| Surface | Verdict | Why |
|---|---|---|
step_drop.v1 (the ticket's named surface) | EXACT | — |
agent_invoke.v1 | EXACT | — |
agent_event_stream.v1 | EXACT | — |
agent_result_artifact.v1 | EXACT | — |
result_delivery.v1 | EXACT | — |
registrar_document_input.v1 | EXACT | — |
journey_context.v1 | EXACT | — |
document_sections.v1 | EXACT | — |
run_stamp.v1 | EXACT | — |
classify_tool_intents.v1 | EXACT | — |
production_card.v1 | 15 paths, ledgered | $defs NAMING only: pydantic writes Cited[CardPartyValue], the generator writes CitedCardPartyValue. Renaming the four keys on both sides leaves zero divergent paths — measured, not asserted. |
registrar_carve_input.v1 | 16 paths, ledgered | The four ratcheted optionals — see below. |
The ledger is a ratchet, not an allowlist: a surface absent from it must be exactly equal, and a surface present must diverge at exactly the listed paths. A second divergence on an already-ledgered surface is red, and a fixed one is red until its entry is deleted — the same shape pramaan-agents uses for the identical four fields.
The four ratcheted optionals — decided, not defaulted
anchor.AnchoredPage.blocks and
boundary.CarveIndexEvidence.{identities,locations,anomalies} use
default_factory producer-side. That emits no default
into the artifact and leaves the field outside required, so a
consumer generated from it comes out list[…] | None = None.
Story-3564 accepts them rather than opening a parity window,
for three stated reasons: the window is a coordinated two-repo change that
would move a REQUIRED context's pin inside a PR whose subject is a new
pipeline; the deferred state is inert, because nothing imports
_generated and a test fails if anything starts to; and the
acceptance is recorded twice — structurally in the divergence ledger, and by
value in test_carve_input_ratcheted_optionals.py.
That value-level test records what is true rather than what is convenient.
This repo is the writer of registrar_carve_input.v1, and
it never emits an explicit null for any of the four;
blocks and anomalies are always present. But
carve_index_evidence's UNAVAILABLE branch deliberately omits
identities and locations — absence the brain's own
model reads as [] and a generated consumer reads as
None. Harmless today; a W2.4 blocker, pinned by
a test that fails the day the parity window lands (pramaan-spec Chore-3581).
Refuses, never warns
$ printf '\nX = 1\n' >> src/shared/agent_contracts/_generated/step_drop_v1.py $ python scripts/gen_agent_contracts.py && git diff --exit-code HEAD -- $GENERATED_PATHS ::error::The committed agent-contract consumers are not what scripts/gen_agent_contracts.py produces from the vendored schemas. This is a REFUSAL, not a warning ... exit 1
A warning on drift would be another green-by-omission surface — the tree would read as generated while carrying whatever a hand put in it. Every failure path in the job exits non-zero: a hand edit, a hand-written module in the generated tree, a missing do-not-edit banner, a schema vendored with no producer, and a schema published upstream with no consumer here.
Found while building it
The live ruleset returns fourteen required contexts, not the
fifteen this repo's AGENTS.md claimed.
Agent invoke + event stream parity (pramaan-agents) is declared in
.github/required-status-checks.txt and by Chore-3548 but was never
added to the ruleset, so it runs and cannot block. That is the Bug-3547 shape
one level up, and it is exactly the limit the committed copy states about
itself. Recorded in AGENTS.md and filed as pramaan-spec Chore-3582, rather
than quietly repaired on another lane's gate.