Arising from dec-0003 — No model client in the binary — semantic extraction is delegated to the live session
Lexical matching in the binary drives conflict detection; no model, no agent, ever gates the exit code
The decision
`dira check "<free text>"` matches the candidate string against the ledger's enforcement set (enumerated in `docs/plan/lanes/E3.md`: accepted-decision alternatives, rejected-decision subjects, and active constraints) using **lexical matching computed fresh, in the binary, over the ledger being checked** — no external corpus, no shipped model weights, no pretrained artifact:
- **idf-weighted content-word overlap.** Document frequency is measured across the entries in the ledger under test at check time (not a fixed external corpus), so the computation is deterministic, offline, and requires nothing to be downloaded or kept in sync. Stopwords are dropped; everything else is a content word. - **Exact multiword phrase hits are a strong, independent signal.** If an alternative's `option` text (for a decision) or a constraint's distinctive phrase appears as a contiguous, whitespace-normalized, case-insensitive phrase inside the candidate plan, that alone is treated as a match regardless of the idf score — this is what makes `"add a background daemon to track run state"` catch `dec-0060`'s `a daemon` alternative trivially, without needing the idf score to clear a threshold on a two-word phrase. - **A match is declared when the idf-weighted score clears a threshold OR an exact phrase hit occurs.** The threshold itself is **not** fixed by this decision — it is tuned by whoever implements the matcher (E3-L2) against the frozen corpus this lane ships (`internal/enforcer/testdata/corpus.yaml`, `corpus.sha256`). If zero false positives across the corpus's near-miss rows and ≥90% recall across its conflict rows cannot jointly be reached, E3-L2's own risk note already commits to reporting an honest precision/recall result and a proposal to change the contract — **never** a quietly edited corpus, which is why the checksum freeze exists.
**What is matched against what** (mechanics already decided in `docs/plan/lanes/E3.md`, restated here only because the matching text sources are this lane's call):
| Kind / state | Matched against | |---|---| | `decision` / `accepted` | each `alternatives[].option` (+ its `why_not` contributes to idf scoring, but a match is cited by the `option` it corresponds to) | | `decision` / `rejected` | the entry's own `title` — the thing said no to | | `decision` / `staged`, `superseded` | nothing (never enforced) | | `constraint` / `active` | `title` + `tags` + markdown **body** | | `intent`, `question`, `note` | never matched independently |
Two things this decision also settles
(a) Does a `constraint` gain an entry-level `revisit_if`?
**Yes, as a proposed additive schema change — not applied by this lane.** `schema/entry.schema.json` currently gives `revisit_if` only to `$defs/alternative`, which only `decision` entries have. A `constraint` conflict can therefore only ever say "supersede it, in writing" — which `docs/plan/lanes/E3.md` already accepts as the correct asymmetry, and the canonical demo block (`README.md`) already renders that way (`cst-0004` cited with no revisit line).
The gap worth closing anyway: nothing today lets a constraint's author record *"here is what would legitimately reopen this standing rule"* even when one genuinely exists (dec-0001's own body distinguishes "closed door" from "locked door," and a constraint deserves the same distinction a decision's alternative gets). Proposed, unapplied, diff:
```json "revisit_if": { "type": "string", "description": "Top-level revisit condition, for entries whose kind has no `alternatives` array to carry one — chiefly `constraint`. Optional; does not change how a decision's per-alternative revisit_if (inside $defs/alternative) works.", "examples": ["cold-start latency stops being the binding constraint"] } ```
Added as a sibling of `private` in the top-level `properties` object. No change to `required`, `allOf`, or `$defs`. Backward compatible: every existing entry omits it and validates unchanged; `additionalProperties: false` means the field must land in `properties` before any entry may set it — `schema/entry_test.go`'s `unknown-field.md` fixture (which pins `"priority"` as a name genuinely absent from the schema) is unaffected. **Not implemented here** — this lane produces data and prose only, per its own instruction; whoever next touches `schema/entry.schema.json` (most likely E3-L2, since it owns the check's rendering rules) should apply this diff and update this fixture ledger's `cst-0004` entry to exercise it.
(b) Does the check read entry bodies, or only frontmatter — and what does that cost against `int-0002`'s latency posture?
**Frontmatter only for decisions; frontmatter + body for constraints — and only for constraints.** A decision's entire matchable surface (`title`, `alternatives[].option`, `alternatives[].why_not`) is already parsed YAML, so no separate markdown-body parse is needed for the kind that is numerous and grows every session. A constraint's operative reasoning typically lives in prose, because the schema gives constraints no `alternatives`/`why_not` field to carry it structurally (see the real `cst-0004`'s own body versus its one-line title) — so constraint matching must read the body to have anything to quote.
This bounds the expensive body-read path to the constraint set, which is small and closed by construction — new constraints require superseding `cst-0002`'s five-kind closure or amending an existing one, not a routine session event — while the decision set is unbounded and ordinary. The same reasoning `dec-0083`'s SQLite-cache-not-source-of-truth argument makes for reads in general applies here: keep the O(ledger size) work bounded to the part of the ledger that stays small, and int-0002's sub-100ms felt-latency bar (already proven for `dira brief`) survives `dira check` the same way.
What this lane shipped alongside this entry
- `internal/enforcer/testdata/ledgers/daemon/` — 9 fixture entries (`int-0002`, `dec-0060`, `dec-0042`, `dec-0075`, `cst-0004`, `qst-0007`, `dec-0081`, `dec-0082`, `dec-0083`) exercising every row of the enforcement-set table above at least once, including the two kinds that must never be cited (`staged`, `question`). - `internal/enforcer/testdata/corpus.yaml` (43 rows: 24 expected-conflict, 19 compliant near-misses) and `corpus.sha256`, frozen before any matcher exists. - The `TestCorpusWellFormed` specification, in prose (this lane creates no `.go` file): `docs/decisions-pending/E3-L1-report.md`.
This lane is **startable now, not green now** — `go test ./internal/enforcer -run TestCorpusWellFormed` cannot run until a Go package exists at that path (E0/E1 territory), and the matcher itself is E3-L2's.
Alternatives on record — 3
✗Binary emits candidate matches; the live coding-agent session adjudicates which ones are real conflicts, mirroring dec-0003's regex/semantic splitrefused Makes `dira check`'s exit code contingent on an agent being present and willing to adjudicate synchronously — which this lane's binding rule forbids outright: `dira check` must reach a non-zero exit with no model, no network, and no agent present.
It also breaks the one demo asset the go-to-market plan is built on (`.agents/product-marketing.md` §6): that terminal block is a bare `dira check "..."` invocation with no agent narration, runnable by a human at a terminal or a CI job, and its non-zero exit is the entire "it caught me" beat. If the exit code needs an agent to render judgment, the identical command run outside a live session — a pre-commit hook, a CI step, a human typing directly — produces no verdict at all. That is not "check works with reduced quality," it is "check does not work," which is a different failure than the one `dec-0003` accepted for capture (where a slow or absent agent costs nothing, because the output is merely staged pending confirmation). `dec-0003`'s regex/semantic split is safe to reuse for capture and unsafe to reuse for a command whose output gates planning.
revisit ifan additive agent-assist mode is layered on top of the lexical floor, so a live session can upgrade a conflict's citation with richer paraphrase-level matching without ever being required for the exit code to be non-zero
✗An optional schema field carrying explicit trigger terms per constraint or alternative, authored at capture timerefused Moves the matching work to whoever writes the entry, who must anticipate every future phrasing that could relitigate it — a burden the "zero clerical work" pillar of dira's own pitch exists to avoid, and one that degrades to zero recall for every entry captured before this field existed (all 13 entries in dira's own ledger today, for a start).
It also does not eliminate the need for a matcher underneath: even with trigger terms present, deciding whether a plan uses a trigger term in a context that actually proposes the rejected thing still needs lexical reasoning. Kept as a compatible, additive hint a future matcher may weight more heavily when present — never adopted here as the sole or primary mechanism.
revisit ifrecall from lexical matching alone plateaus below an acceptable bar on real corpora, and an opt-in per-entry hint measurably closes the gap without displacing the deterministic floor
✗Embed a model client directly in the binary so `dira check` can semantically judge whether a plan contradicts a decisionrefused This is `dec-0003`'s own rejected option, restated at check time instead of capture time.
It would require an API key to function, add a network round-trip and a per-call cost to a command whose entire pitch is "works with the network unplugged" (`cst-0004`), and pin dira to one model provider. `dec-0003` already settled that dira never embeds a model client; nothing about `check` specifically reopens that, and reopening it here would make the flagship demo — a bare terminal command with no narration — secretly dependent on a live API call succeeding.