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
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
| Piece | Why |
|---|---|
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.