One contract, two repos, zero hand-mirrors

Vega's 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.
Story-1992 · EPIC-1990 · pramaan-docs ADR-153 story 2a · pramaan-docs ADR-074 custody pattern · repo pramaan-agents · branch issue-1992-frameoutput-schema-export
The naive fix that detonates: hand-write a 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.

1 · The artifacts — schema + field vocabulary

Two generated files land under agents/framer/schemas/, both emitted by agents/framer/schema.py:

ArtifactWhat it isWho consumes it
frame_output.v1.schema.jsonFrameOutput.model_json_schema() — Party and Issue ride along in $defs, so one file carries the whole contractpramaan-functions story 2b: generates/verifies its MatterFrameEnvelope DTO from it in CI
frame_output.v1.fields.jsonThe 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

2 · Custody — the CI gate that fails on drift

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
Why the gate lives in THIS repo: drift is caught at the source, in the PR that changes models.py — before pramaan-functions ever sees a stale artifact. The reviewer of a contract change sees the schema diff in the same PR, which is exactly the "field change is an RFD/ADR discussion, not a PR" tripwire the framer doctrine wants.

No new CI step was needed: the test rides the existing agents/*/tests discovery loop in .github/workflows/ci.yml.

3 · Proof

CheckResult
4 custody tests (schema match, fields match, $defs carry Party/Issue, declaration-order vocabulary)green
Red-proof: artifact deliberately drifted (extra field appended) → custody testfails as designed (2 failed), restored
Full framer suite (85 tests) + ruff check agents/ agent_contracts/ tests/ scripts/ + brain-egress gategreen

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.