Story-1282 · pramaan-docs ADR-115 · grounded ECA prior

Atlas consumes matter_type — walkthrough

Branch issue-1282-atlas-matter-type-prior · Story: pramaan-io/pramaan-spec#1282 · Provenance: pramaan-docs ADR-115 (matter_type coarse axis), ADR-110 (input contract). Consumer half of Spike-1274.

🛰️ 1 · Satellite — why this exists

Atlas's job is the issue-framework layer of Early Case Assessment — the lens every downstream tool reads documents through. The frame for an arbitration is a different shape from a matrimonial matter, which is a different shape from a criminal appeal: different live issues, different applicable-law family, different relief category. Get the frame's nature wrong and everything built on it leans the wrong way.

Until now Atlas guessed the dispute nature from counsel's free text alone (proceeding_types was sent empty). The firm had already declared what kind of matter this is — that signal just never reached Atlas. Spike-1274 / ADR-115 added a real coarse axis (business.matter_types, one per matter, set at create). This Story is the consumer half: pipe the firm's own matter_type.name into Atlas so the prior is grounded input, not a guess.

WHY a prior, not a rule: the naive fix is "branch on type → emit a fixed template." That's brittle and wrong — counsel's raw text is the truth, and a declared type can be mislabelled. So matter_type only shapes the framework and vocabulary; Atlas still reasons over the raw words, and if the text contradicts the type, it trusts the text and flags the mismatch.

✈️ 2 · Airplane — the shape

functions (S2/S3)              broker envelope            Atlas brain
  matters.matter_type_id  ──→  input.matter_type   ──→  AtlasIntakeInput.matter_type (string?)
  (firm's matter_type.name)    (the .name string)        │
                                                         ▼
                                          prompt grounds the prior:
                                          arbitration ≠ matrimonial ≠ criminal appeal
                                                         │
                          present ─→ frame on that dispute nature (Other if unrecognised)
                          absent  ─→ infer from free text, exactly as before

The whole change rides one optional string. No functions-side mapping table — Atlas owns coarse bucketing: an unrecognised firm-custom type collapses to a generic Other matter framed purely from counsel's words. The anti-hallucination rule is untouched: a declared type never licenses inventing clause numbers, rates, or citations.

🔬 3 · Microscope — what changed, and WHY each piece

ChangeWhereWhat & WHY
Soul: new input fieldagents/atlas/baml_src/atlas.bamlmatter_type string? added to AtlasIntakeInput — the firm's own matter_type.name verbatim. Optional so older matters (pre-catalog) omit it and Atlas falls back cleanly (ADR-115 D1).
Soul: grounding ruleatlas.baml system promptNew "GROUND THE FRAME ON THE DECLARED MATTER TYPE" block: steers issue framework / applicable-law family / relief category per dispute nature (arbitration / matrimonial / criminal appeal / civil), with explicit "PRIOR not a fact", "trust the raw text on conflict + flag it", "unrecognised → Other", and "type never licenses fabricated specifics". When absent → infer from text as before.
Soul: render the fieldatlas.baml user prompt{% if input.matter_type %}Matter type … {{ input.matter_type }}{% endif %} — only rendered when present, so the prompt is byte-identical to pre-1282 when absent.
Regenerated clientagents/atlas/baml_client/baml-cli generate --from agents/atlas/baml_src, committed. AtlasIntakeInput.matter_type: Optional[str] = None now in the typed model.
No Python wiring neededagent.py / runtime.pybuild_intake_input already does AtlasIntakeInput.model_validate(request["input"]). An optional field flows through automatically: present key → carried; absent key → None. Graceful fallback is structural, not branchy.
Truth boundary kept stable
matter_type is an input prior, not part of the refined-vs-raw promotion contract, so it is deliberately not added to source_snapshot. The frozen snapshot (core_dispute / outcome / value) stays byte-identical — proven by test_matter_type_not_in_source_snapshot and the unchanged test_source_snapshot_is_frozen_verbatim.

4 · The single most important guarantee

matter_type absent = zero behaviour change
The field is optional. An older matter's payload (no matter_type key) validates unchanged, inp.matter_type is None, the prompt's {% if %} renders nothing, and Atlas infers the dispute nature from free text exactly as before. Proven by test_build_intake_input_matter_type_absent_is_none plus the BAML AtlasNoMatterTypeFallsBackCleanly case (same sparse intake, no type, still a clean all_clear frame).

5 · Tests

TestProves
test_build_intake_input_carries_matter_typefunctions' matter_type.name rides the typed input verbatim
test_build_intake_input_matter_type_absent_is_nonebackward compat — absent key → None, payload accepted unchanged
test_matter_type_not_in_source_snapshotthe prior never leaks into the frozen truth-boundary snapshot
AtlasMatterTypeGroundsMatrimonialFrame (BAML)same sparse intake + matter_type "Matrimonial" → matrimonial framing (divorce/family proceeding), not commercial damages — live-model check
AtlasNoMatterTypeFallsBackCleanly (BAML)identical intake with NO type still frames cleanly from free text

6 · Verification

$ (cd agents/atlas && pytest tests -q)
45 passed                      # 42 existing (zero regressions) + 3 new unit tests
$ ruff check agents/
All checks passed!
$ baml-cli generate --from agents/atlas/baml_src
Wrote 14 files to agents/atlas/baml_client     # committed, reproducible
# ADR-110 §6 statelessness intact: no sqlalchemy / psycopg / boto3 s3·sqs·dynamodb added

7 · Boundary respected

Consumer half only
This Story ships no functions-side change — the matter_type catalog, the matter contract field, and the OpenAPI mirror are the producer half (functions S2/S3, ADR-115). Atlas just consumes the string. No mapping table anywhere: coarse bucketing to Other is Atlas's job, in the prompt. The brain stays stateless and writes nothing.