FrameOutput is the canonical shape of the Matter Frame (pramaan-docs ADR-117: the type IS the contract). pramaan-functions is about to persist that frame (business.matter_frames, pramaan-docs ADR-153) — which means it needs a DTO that matches Vega's contract exactly, forever. Story-1992 exports the contract as committed, machine-readable artifacts so the functions repo generates its mirror instead of hand-typing one.MatterFrameEnvelope DTO in pramaan-functions that "looks like" FrameOutput, and add a parity test that compares field lists. That only detects drift after it happened — and a hand-maintained field list is itself a second copy that rots. pramaan-docs ADR-153 D1 prefers prevent: export the schema FROM the one canonical Pydantic source, commit it, gate it, and let the downstream repo codegen-or-verify from the artifact. There is one author (Vega's models.py) and everything else is generated.Two generated files land under agents/framer/schemas/, both emitted by agents/framer/schema.py:
| Artifact | What it is | Who consumes it |
|---|---|---|
| frame_output.v1.schema.json | FrameOutput.model_json_schema() — Party and Issue ride along in $defs, so one file carries the whole contract | pramaan-functions story 2b: generates/verifies its MatterFrameEnvelope DTO from it in CI |
| frame_output.v1.fields.json | The 23 FrameOutput field names, in declaration order (the tier grouping in models.py is meaningful) | The ONE field vocabulary (ADR-153 D6): functions DTO ≡ matter_frame_field_locks lock names ≡ CritiqueFinding.field |
# regenerate after any models.py change (extend-only, per framer AGENTS.md)
uv run python agents/framer/schema.py
This is the same discipline the repo already runs for the wire contracts (agent_contracts/schema.py + its custody test, pramaan-docs ADR-074): humans author the shape in Pydantic, the artifact is generated, checked in, and gated. agents/framer/tests/test_frame_schema_custody.py regenerates both artifacts from models.py on every CI run and asserts the committed files match — parsed-equal and byte-exact, so even formatting churn can't dirty the downstream codegen diff.
# the gate, in one line each committed == FrameOutput.model_json_schema() # schema drift → red fields == list(FrameOutput.model_fields) # vocabulary drift → red schema["required"] == ["frame_status"] # the honesty gate stays the ONE required field
No new CI step was needed: the test rides the existing agents/*/tests discovery loop in .github/workflows/ci.yml.
| Check | Result |
|---|---|
| 4 custody tests (schema match, fields match, $defs carry Party/Issue, declaration-order vocabulary) | green |
| Red-proof: artifact deliberately drifted (extra field appended) → custody test | fails as designed (2 failed), restored |
Full framer suite (85 tests) + ruff check agents/ agent_contracts/ tests/ scripts/ + brain-egress gate | green |
One-sentence recall: Vega's models.py stays the only author of the Matter Frame contract — everything downstream is generated from two committed artifacts that CI refuses to let drift.