Story 1995: the envelope DTO — one frame contract, pinned across two repos

pramaan-docs ADR-153 (EPIC-1990) decided how Vega's Matter Frame persists: one MatterFrameEnvelope JSONB in the future business.matter_frames table. The envelope's frame compartment must be Vega's FrameOutput — but that contract lives in pramaan-agents, and a hand-copied twin model silently drifts. Story-1992 (agents side, 2a) exported the canonical FrameOutput.model_json_schema() + field vocabulary as committed artifacts. This Story (2b) builds the functions-side mirror and the CI gate that makes drift a red build instead of a production surprise.

The envelope

MatterFrameEnvelope            (extra='forbid' at every level)
├── frame:      FrameOutput    — pure Vega output, mirror of the agents contract; nothing injected
├── resolution: FrameResolution — ingest-stamped (D2): forum/party match records,
│                                 matched canonical ids XOR proposed markers, never written by Vega
└── provenance: FrameProvenance — fact-level citations (field + document_refs + source_excerpt)
                                  + run-manifest pointer (storage contract = ADR-153 Story #7)

Two pins, one contract

ADR-153 D1 is honest about mechanism: a parity test detects drift, codegen-from-canonical prevents it. This lands the verify half of "generate or verify" with both sides pinned: pin 1 — tests/unit/test_matter_frame_envelope.py compares the mirror's model_json_schema() to the vendored artifact field-for-field (names, declaration order, types, required-ness, enums, defaults; only prose descriptions and the mirror's deliberate additionalProperties: false are tolerated divergences). pin 2 — the new frame-schema-parity CI job checks out agents/framer/schemas/ from pramaan-agents main and byte-diffs it against the vendored copy. Agents-side custody tests already pin artifact ≡ their models. Chain: agent contract ≡ artifact ≡ vendored copy ≡ functions mirror.

What landed where

PieceWhy
src/domains/matter_frame/dtos.py — FrameOutput/Party/Issue mirror, FrameResolution, FrameProvenance, MatterFrameEnvelope, FRAME_FIELD_NAMES The persistence contract for business.matter_frames.envelope (ADR-153 D1). Mirror classes keep canonical names — they title the JSON Schema $defs, so parity is testable at all. Plain snake_case models, not ApiRequestModel: this is a JSONB payload, not a camelCase wire body.
src/domains/matter_frame/schemas/frame_output.v1.*.json Vendored copy of the pramaan-agents Story-1992 artifacts — the local truth the pytest pin verifies against, kept fresh by the CI pin.
FRAME_FIELD_NAMES + FieldCitation.field validator ADR-153 D6: ONE field vocabulary for the DTO, the future matter_frame_field_locks.field values, and CritiqueFinding.field (which adds "general"). Derived from the mirror, pinned to the artifact's fields.json in declaration order — the tier grouping is meaningful, so it is a list, not a set.
ForumResolution/PartyResolution matched-XOR-proposed validators D2 match contract encoded in the type: a canonical id is present exactly when outcome='matched'; proposed rows carry the reason for human confirm. Fuzzy matches cannot be smuggled in as half-bound records.
.github/workflows/ci.yml — frame-schema-parity job Sparse-checkout of pramaan-agents schemas (same pattern as the pramaan-app contract-test job), byte-diff against the vendored copy. Hard-fails with a clear error when GH_READ_PRAMAAN_AGENTS is missing — prevent-over-detect, no silent skip.
tests/unit/test_matter_frame_envelope.py The parity pins plus full-envelope round-trips (model_dump/model_dump_json both ways — full-shape assertion per AGENTS.md), extra='forbid' probes at every nesting level, vocabulary rejection, and the matched-XOR-proposed invariants.

Updating the contract (the workflow this creates)

1. change FrameOutput in pramaan-agents models.py     (extend-only doctrine)
2. uv run python agents/framer/schema.py              (regenerate artifacts there;
                                                       their custody test enforces this)
3. copy both JSONs into src/domains/matter_frame/schemas/
4. adjust the mirror in dtos.py until the parity test passes

Skip step 3/4 and any functions PR goes red at frame-schema-parity; fake step 4 and pytest goes red. There is no green path that leaves the two repos disagreeing about what a frame is.

Deliberately NOT here

No table, no migration (ADR-153 Story #1), no ingest endpoint (Story #3), no lock rows (Story #5). The envelope is pure content: Layer-2 stamps (version, source, agent_run_id, created_at) belong to the row, so they are not fields on the DTO — Vega can never supply them by construction.

Observability

Pure Pydantic models — no executable boundary, no route, no job. Instrumentation-exempt per pramaan-docs ADR-124; the future ingest span (functions.matter_frame.ingest, Story #3) covers validation.

Verification

ruff check                       clean on changed files (repo-wide: 120 preexisting alembic/versions hits, untouched)
mypy src/domains/matter_frame tests/unit/test_matter_frame_envelope.py   clean (mypy is local-only)
pytest tests/                    2133 passed, 1 skipped (full suite vs make test-db Postgres 16;
                                 test_counsel_approves_document_type_and_audits flaked once in three
                                 full-suite runs, passes clean-tree and changed-tree alike — unrelated domain)
make openapi                     no snapshot drift (no route/API DTO touched)

Merge prerequisite: the GH_READ_PRAMAAN_AGENTS repo secret (fine-grained PAT, contents:read on pramaan-io/pramaan-agents) must exist, mirroring GH_READ_PRAMAAN_APP. Until it is added, the frame-schema-parity job fails loudly by design.