Blake Govern — the closest-match 3-lens vocabulary gate

Behind every controlled-vocabulary create_*, Blake runs three INDEPENDENT model lenses — Match · Taxonomy · Legal-validity — and a deterministic aggregator that applies the §8 thresholds + the §6 catalog-maturity regime. Default verdict is REJECT (bias to reuse). Coined types land as "proposed, pending approval"; Blake never coins firm-canonical vocabulary and can never approve his own proposal.
Story-1539 · EPIC-1536 · agents/registrar/ (Govern) · branch issue-1539-registrar-govern-panel · 2026-06-21

The classify capability (#1538) assigns an existing type where the catalog has a credible match, and deliberately leaves document_type null when none honestly fits — that gap routes here. This Story fills the Govern capability: the closest-match panel that gates NEW controlled-vocabulary creation (spec §8, pramaan-docs ADR-128 §2). It is the exception path, not every document — classify handles the routine. Coined values are proposals the pramaan-functions broker executes under auth · firm/matter RLS · per-tool policy · idempotency · audit; Blake calls no tool, holds no credentials, and writes no business truth.

What to review

AreaWhereWhat it does
Lens + panel contractsagents/registrar/models.pyMatchLensVerdict / TaxonomyLensVerdict / LegalValidityLensVerdict (forced structured, default-reject) · GovernRoute (the four routes) · GovernTargetKind (the strictness gradient) · RegistrarGovernInput · the rich RegistrarGovernOutput panel with the §6/§8 closest-match fields + votes + provisional lifecycle.
Three lens promptsagents/registrar/prompts/govern.pyThree DISTINCT system prompts, no shared scratchpad — Match (reuse-bias), Taxonomy (anti-sprawl), Legal-validity (filing-only). Catalog + document body rendered inside the recursive evidence fence (spec §10.6).
Deterministic aggregatoragents/registrar/govern.pyaggregate_panel applies the §8 bars (reuse ≥ 0.82 · panel band 0.60–0.82 · create < 0.60 + proposal ≥ 0.85) + the §6 regime over the three verdicts → one of four routes. Thresholds read from registrar.yaml (config, not code).
Gated, idempotent intentsagents/registrar/policy.pyintents_for_govern emits the broker-mediated create_matter_document_type / request_firm_document_type / request_filing_type intents — never auto — each carrying an idempotency_key (create-race guard) + the provisional lifecycle args.
Orchestration + wireagents/registrar/agent.py, runtime.pyrun_govern runs the lenses required by the strictness gradient, aggregates, assembles the panel, and fails closed to the safe REJECT route. The runtime stamps each govern intent is_proposal+requires_broker_mediation+frozen source_refs.
Eval fixturesagents/registrar/evals/govern_cases.jsonlThe four §12 anti-sprawl scenarios — agreement-vs-contract (Match) · force-fit-rejected (NoForceFit) · bootstrap-not-starved · near-dup-no-create — driven through the aggregator now; full model-judge gates land in #1544.
Testsagents/registrar/tests/test_registrar_govern_capability.py38 offline tests: every route, the strictness gradient, the no-force-fit / no-near-dup / bootstrap-not-starved invariants, idempotency, fail-closed, the wire wrap, and the four fixtures.

The three lenses → deterministic aggregator → one of four routes

Each lens is its own Pydantic AI agent with a distinct prompt and no shared scratchpad (spec §8). The lenses report; the aggregator decides. Making the decision deterministic code (not a fourth LLM) is what makes the gate testable + calibratable and lets the eval gates assert exact behaviour.

# govern.py — aggregate_panel, the §8 bars + the §6 regime
exact catalog dup            -> reuse_existing            # NoNearDupCreate, strongest
match_confidence >= 0.82      -> reuse_existing            # the reuse bias, when honest
not a distinct category      -> leave_unassigned_..review # a variant never force-fits / creates
0.60 <= match < 0.82          -> leave_unassigned_..review # show the closest-match panel
match < 0.60 + proposal>=0.85 -> create_matter_provisional # Bootstrap/Growing only (BootstrapNotStarved)
   ...same, Mature catalog   -> leave_unassigned_..review # no auto-create for a singleton
   ...same, filing_type      -> request_firm_type         # unanimous + legal-valid + HUMAN-gated
otherwise                    -> leave_unassigned_..review # the safe REJECT default
WHY default-reject, adaptively. A wrong existing assignment (force-fit) and a near-duplicate new type (sprawl) are BOTH failures. The lenses bias to reuse and the aggregator's fall-through is always leave_unassigned_needs_review — Blake never coins on doubt and never jams a doc into a near-but-wrong type. But an empty catalog must be populated or Blake can't classify, so the create bar relaxes by catalog maturity (§6): generous in Bootstrap, conservative in Mature. NoForceFit and BootstrapNotStarved live on different axes (create vs reuse), so they never conflict.

The strictness gradient (spec §6/§8)

TargetLenses runCreate fallback
tagMatch only (lowest bar — may skip the full panel)matter-local, low bar (cheap + reversible)
document_typeMatch + Taxonomymatter-local provisional (regime-gated) pending approval
filing_typeMatch + Taxonomy + Legal-validity (required)never matter-local — unanimous + human-gated firm request only

Coined types land as proposals, never live creates

A create route emits a single gated intent (the broker decides + executes), stamped with the provisional lifecycle and a deterministic idempotency key so two concurrent batches proposing the same value never double-create.

# policy.intents_for_govern — the gated create intent for a matter-local provisional
RegistrarToolIntent(
  tool="create_matter_document_type",
  policy="suggest",   # the LAUNCH policy — never "auto" (gate-rule, #1544)
  args={
    "name": "Mediation Settlement", "matter_id": "matter-1",
    "provisional": True, "created_by": "blake-via-ledger",
    "review_state": "proposed", "source_doc_ids": ["doc-1"],
    "idempotency_key": "firm-1:matter-1:document_type:mediation settlement",
  })
WHY the brain can't self-approve. The intent is a proposal — is_proposal + requires_broker_mediation + frozen source_refs ride the agent_event_stream.v1 turn. The broker holds the credentials and the gate-rule; it decides per effect and writes the catalog row under RLS + audit. Promotion of a provisional to firm-canonical is a separate human admin action. Blake proposes; he never mutates (pramaan-docs ADR-128).

Tests & gates

38 govern tests 152 registrar suite 101 governance ruff clean brain-egress clean — Blake stays thin: no DB/credentials, OpenRouter+ZDR only, one event language. The four named eval fixtures run through the aggregator now; the full model-judge gate harness lands in the evals Story (#1544).