Skip to content

docs(adr): supersede ADR-0005 with ADR-0027, ratifying the deterministic evaluator - #139

Merged
mbeacom merged 4 commits into
mainfrom
mbeacom-vigilant-garbanzo
Aug 14, 2026
Merged

docs(adr): supersede ADR-0005 with ADR-0027, ratifying the deterministic evaluator#139
mbeacom merged 4 commits into
mainfrom
mbeacom-vigilant-garbanzo

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Supersedes ADR-0005 with ADR-0027, ratifying the deterministic evaluator and re-timing the calibration obligation.

ADR-0005 had sat at proposed since 2026-07-18 — the only unratified record in the corpus, and the sole item in adr queue throughout. It was never blocked by disagreement. Pass 0 shipped and is landed / reference-verified; what stalled the record was one sentence in its Consequences, stated as an explicit commitment:

publish escalation precision and recall each release, including the false-negative rate.

That is unmeetable while the evaluator is deterministic-only. Every implemented trigger fires on proven evidence, so precision is 1.0 by construction and recall is undefined against a class of judgment the shipped evaluator does not attempt. Publishing 1.0 / undefined every release would be a compliance ritual measuring nothing — precisely the "evaluator theater" ADR-0005 was written to prevent. Accepting it unchanged would have made that ritual project law; leaving it proposed left a landed, in-use surface formally ungoverned.

ADR-0027 therefore:

  1. Ratifies the deterministic layer as built — deterministic-before-probabilistic, the evaluator never approves it only routes, escalation as a declarative OR, routing to a named human, and Pass 0 remaining useful with no model configured.
  2. Carries the four-pass architecture forward as intent, not as a shipped claim. Per ADR-0014's binding vocabulary the probabilistic layer is scoped and nothing above it. The three triggers defined as functions of Pass 2/Pass 3 output — low-confidence, pass-disagreement, novel-no-precedent — are recorded as deliberately deferred, so the gap between eleven documented and eight implemented triggers is explicit rather than unexplained.
  3. Converts the per-release report into a shipping precondition: no probabilistic pass may ship without a holdout frozen before that pass produced its first score. Strictly harder to evade than the original — a report is discharged by publishing a number, a precondition blocks the ship. While none has shipped, each release states that absence explicitly rather than publishing a figure that measures nothing.

Option C (amend by reference, the ADR-0014 → ADR-0012/0013 pattern) was considered and rejected: the change is to a published commitment in Consequences, and an amendment would leave the record's own text asserting an obligation no release can meet. ADR-0005's own closing instruction asked for the opposite — "this record should be superseded explicitly rather than quietly ignored."

A correction found during review

The third commit fixes a defect in ADR-0027 itself, caught by the session scoping specs/012-evaluator-calibration/ and verified independently before acting.

The evidence table claimed ADR-0005's action item 3 — "log every escalation decision with reason codes from day one" — was Met. That is true of emission and false of retention:

  • 0 of 27 records carry an evaluation: block
  • adr evaluate has no --write (packages/cli/src/evaluate.ts:7-8, FR-014)
  • RunMetadata.ranAt is declared (types.ts:357) and populated by no production path; its only other occurrence asserts it is banned from canonical bytes
  • renderHuman prints routing.reasons (proven only) — the 8-element routing.evidenceStatus is reachable solely under --json, so default output retains exactly the half from which recall cannot be computed

plan.md:277 settles the intent: "Escalation reason codes logged from the first run — this is the calibration set, and it cannot be backfilled." Retention was asked for, never delivered, and that warning has now come true.

It is also not a bug to fix by adding persistence: specs/005 SC-008 ratifies that "Pass 0 persists nothing" and ADR-0027 §1 forbids any evaluator surface to persist. The two commitments were always in tension; ADR-0005 never resolved it and neither did the first draft of this record.

Three corrections landed, all before the record was pushed:

  • The table row separates emission from retention, and Context states the consequence: the calibration corpus must be re-derived from committed history rather than harvested, and re-derivation is only partial because --snapshot never existed historically — so a not-proven from an absent snapshot is byte-identical to one from evaluated-and-false evidence.
  • §3 now requires precision/recall/FNR published twice — whole-gate and probabilistic-marginal (cases where no deterministic trigger fired) — and states that the marginal figure is what satisfies the obligation. Escalation is an OR, so eight deterministic triggers at precision 1.0 by construction would otherwise let a worthless probabilistic pass hide behind them. That is the same theater failure displaced one level down, which is why it belongs in the ADR rather than in the spec.
  • Consequences records the permanent loss and requires the evidence index to disclose snapshots as reconstructed, and to distinguish evaluated-and-false from evidence-absent.

Review feedback addressed (commit 4)

Both comments on this PR were correct.

A fabricated duration. ADR-0027's Context claimed "for eleven months it has been the sole item in adr queue". ADR-0005 is dated 2026-07-18 and this record is decided 2026-08-12 — 25 days. The number was never computed from the dates; it was asserted and then repeated into this description. A record whose subject is an evaluator that must not assert more than its evidence supports carried a fabricated fact in its second paragraph. Corrected in both places; the point it stood in for survives without it, and is now stated in terms of the reviewBy date instead.

Two stale MANIFEST sections. The earlier "sync derived surfaces" commit updated the ADR tree listing and never re-read the prose asserting counts about that listing, so the file contradicted itself: "One record remains proposed (0005)", and a Verification section reading "27 files … 26 records, ids 0001–0026 … 24 accepted, 1 proposed, 1 superseded". Both now match the tree, counted rather than reasoned about: 28 files, 27 records, ids 0001–0027, 25 accepted, 0 proposed, 2 superseded, template at draft. This is the drift issue #131 already tracks — MANIFEST is hand-maintained with no CI check.

Ratification state, checked while here: all 25 accepted records carry a named provenance.ratifiedBy, which is what the schema requires of an agent-drafted record reaching accepted. review.decidedAt/approvals are optional and populated on 7 of 25; that inconsistency predates this PR and is deliberately not backfilled, because the decision dates for the other 18 are unknown.

Derived surfaces

Three hand-maintained surfaces still asserted the pre-supersession state:

  • MANIFEST.md listed 0005 as proposed and stopped at 0026. Hand-maintained with no CI check (MANIFEST.md inventory drifts silently: generate it from the corpus instead of hand-maintaining it #131), so it drifts silently.
  • README.md's principle table cited 0005 for "deterministic checks run before any model call"; that authority is now 0027.
  • README.md's Dogfooding section claimed "changes to it ship with calibration data" in the present tense. No calibration data exists. It now states the absence and names the holdout precondition — leaving it would have made the new record's own commitment false on the day it was signed.

Deliberately unchanged: the adr queue sample in README.md still shows 0005 as a queued item. It is an explicitly dated snapshot (2026-07-25) carrying its own corpus fingerprint; regenerating it against today's empty queue makes a poor illustration, and hand-editing would fabricate a fingerprint no corpus produces. Flagged rather than faked — happy to handle differently on request.

Verification

  • bun test2064 pass / 0 fail, 35479 expect() calls across 170 files
  • lint, typecheck, check:deps (core-has-no-adapter-deps: ok), check:freeze-hashes, check:changelog, check:doc-pins — all green
  • adr lint27 records, 0 errors, 0 warnings
  • adr graph — resolves the 0027 -> 0005 [supersedes] edge and both statuses
  • adr queueempty for the first time; 0005 was its only item

No code changes; documentation and corpus only.

Follow-on work

Two scoping sessions are branched from this one and depend on ADR-0027 being normative:

  • specs/011-probabilistic-evaluator-passes/ — Passes 1–3 plus the three deferred triggers
  • specs/012-evaluator-calibration/ — the frozen holdout, the precondition gate, and the absence statement that make §3 satisfiable

Neither is in this PR. ADR-0027 action items 2–5 remain open and are tracked in the record.

Copilot AI balanced review requested due to automatic review settings August 13, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Supersedes ADR-0005 with ADR-0027 to ratify the deterministic evaluator and defer calibration requirements until probabilistic passes ship.

Changes:

  • Adds ADR-0027 and supersedes ADR-0005.
  • Updates evaluator documentation and governance references.
  • Adds ADR-0027 to the manifest inventory.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
README.md Updates evaluator governance and calibration guidance.
MANIFEST.md Updates ADR-0005 and adds ADR-0027.
docs/adr/0005-deterministic-first-evaluator-with-declarative-escalation.md Marks ADR-0005 as superseded.
docs/adr/0027-ratify-the-deterministic-evaluator-and-bind-calibration-reporting-to-the-first-probabilistic-pass.md Defines the replacement evaluator decision.
Suppressed comments (4)

docs/adr/0027-ratify-the-deterministic-evaluator-and-bind-calibration-reporting-to-the-first-probabilistic-pass.md:56

  • novel-no-precedent is defined by Pass 1 retrieval output (docs/EVALUATOR_RUBRIC.md:48-60,156), not Pass 2/Pass 3 output. It is still unavailable because Pass 1 is unimplemented, but the stated dependency is factually wrong.
| 3 of 11 escalation triggers (`low-confidence`, `pass-disagreement`, `novel-no-precedent`) | Not evaluable — each is defined as a function of Pass 2/Pass 3 output |

docs/adr/0027-ratify-the-deterministic-evaluator-and-bind-calibration-reporting-to-the-first-probabilistic-pass.md:88

  • True negatives do not enter the precision formula, so counting an absent snapshot as a true negative cannot inflate precision. The real problem is that unknown cases are being mislabeled, which makes the confusion matrix—and potentially recall if positives are hidden among them—invalid.
`--snapshot` that historical evaluations never had, so a `not-proven` produced by an
*absent* snapshot is byte-identical to one produced by *evaluated-and-false*
evidence. Counting the first as a true negative would inflate deterministic
precision and corrupt the recall denominator — the same absence-versus-evidence

docs/adr/0027-ratify-the-deterministic-evaluator-and-bind-calibration-reporting-to-the-first-probabilistic-pass.md:100

  • Determinism does not make classifier precision 1.0 or recall undefined. proven establishes that a trigger predicate matched; it does not establish that escalation was correct against labeled human-review outcomes. ADR-0005:96-99 explicitly anticipates false-positive deterministic escalations, and docs/EVALUATOR_RUBRIC.md:176-180 defines outcome-based calibration. This invalidates both the rationale here and the later assumption that deterministic triggers provide “perfection”; please reframe the rescope around the missing labeled holdout/retained inputs rather than determinism.
attempt. Every implemented trigger fires only on proven evidence, so its precision
is 1.0 by construction and its recall is undefined — there is no probabilistic
estimate to be wrong about. Publishing `1.0 / undefined` every release would be a

docs/adr/0027-ratify-the-deterministic-evaluator-and-bind-calibration-reporting-to-the-first-probabilistic-pass.md:143

  • This immediately effective rule leaves the repository noncompliant at merge: docs/EVALUATOR_RUBRIC.md still describes Passes 1–3 and per-release calibration in present-tense normative language (for example lines 3-5, 48-68, 123-136, and 173-190). Leaving the required rubric update as open action item 5 does not satisfy “No artifact may describe” them this way; update that derived surface in this PR or defer acceptance of this rule.
specified in `docs/EVALUATOR_RUBRIC.md`. They are **not** ratified as built. No
artifact may describe the evaluator as four-pass, rubric-scoring, or adversarial in
the present tense until the corresponding pass ships. Per ADR-0014's binding state
vocabulary the probabilistic layer is **scoped**, and nothing above it.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +43 to +44
the only record in the corpus that never reached `accepted`, and for eleven months
it has been the sole item in `adr queue` — tier `arb`, routed to `@mbeacom`,
Comment thread MANIFEST.md
├── 0025 badges as recipes over existing output accepted
└── 0026 identify the CI comment by token evidence accepted
├── 0026 identify the CI comment by token evidence accepted
└── 0027 ratify the deterministic evaluator accepted
mbeacom and others added 3 commits August 12, 2026 20:58
…tic evaluator

ADR-0005 has sat at `proposed` since 2026-07-18 as the only unratified record
and the sole item in `adr queue`. Pass 0 landed and is reference-verified, but
the record's Consequences section carries an explicit commitment to "publish
escalation precision and recall each release, including the false-negative
rate" — unmeetable while the evaluator is deterministic-only, because every
implemented trigger fires on proven evidence, so precision is 1.0 by
construction and recall is undefined.

ADR-0027 ratifies the deterministic layer as it stands, carries the four-pass
architecture forward as intent rather than as a shipped claim, and re-times the
calibration obligation from *every release* to a shipping precondition: no
probabilistic pass may ship without a holdout frozen before that pass produced
its first score. While none has shipped, each release states that absence
explicitly rather than publishing a figure that measures nothing.

The three triggers defined as functions of Pass 2/Pass 3 output —
`low-confidence`, `pass-disagreement`, `novel-no-precedent` — are recorded as
deliberately deferred, so the gap between eleven documented and eight
implemented triggers is explicit rather than unexplained.

Follows the ADR-0019 / ADR-0020 rescope precedent and ADR-0014's separation of
correctness from adoption. ADR-0005's own closing instruction asked for exactly
this: superseded explicitly rather than quietly ignored.

`adr lint` reports 27 records, 0 errors, 0 warnings; `adr queue` is now empty.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Three hand-maintained surfaces still asserted the pre-supersession state:

- `MANIFEST.md` listed 0005 as `proposed` and stopped at 0026. This inventory
  is hand-maintained and has no CI check (issue #131), so it drifts silently.
- `README.md`'s principle table cited 0005 as the authority for "deterministic
  checks run before any model call"; that authority is now ADR-0027.
- `README.md`'s Dogfooding section claimed "changes to it ship with calibration
  data" in the present tense. No calibration data exists, because no
  probabilistic pass has shipped. ADR-0027 requires that absence be stated
  rather than assumed, so it now says so and names the holdout precondition.

The last one is the substantive fix: an unqualified present-tense calibration
claim is precisely the evaluator theater ADR-0005 was written to prevent, and
leaving it in place would have made the new record's first commitment false on
the day it was signed.

Left deliberately unchanged: the `adr queue` sample in README.md still shows
0005 as a queue item. It is an explicitly dated snapshot (2026-07-25) carrying
its own corpus fingerprint; regenerating it against today's empty queue would
make a poor illustration, and hand-editing it would fabricate a fingerprint that
no corpus produces. Flagged rather than faked.

Verified: `bun test` 2064 pass / 0 fail, `lint`, `typecheck`, `check:deps`,
`check:freeze-hashes`, `check:changelog`, `check:doc-pins` all green; `adr lint`
27 records 0 errors 0 warnings; `adr graph` resolves the 0027 -> 0005 supersedes
edge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
The scoping session for specs/012-evaluator-calibration found that ADR-0027's
own evidence table was wrong, and the finding is verified:

- No record in docs/adr/ carries an `evaluation:` block (0 of 27).
- `adr evaluate` has no `--write` (packages/cli/src/evaluate.ts:7-8, FR-014).
- `RunMetadata.ranAt` is declared (types.ts:357) and populated by no production
  path; its only other occurrence asserts it is banned from canonical bytes.
- `renderHuman` prints `routing.reasons` (proven only). The 8-element
  `routing.evidenceStatus` is reachable solely under `--json`, so default output
  keeps exactly the half from which recall cannot be computed.

The table claimed action item 3 was "Met". That is true of emission and false of
retention, and retention is what was asked for: Phase 4's exit criterion reads
"Escalation reason codes logged from the first run — this is the calibration set,
and it cannot be backfilled." The warning came true.

It is also not a bug to fix by adding persistence. specs/005 SC-008 ratifies that
"Pass 0 persists nothing", and this record's own §1 forbids any evaluator surface
to persist. The two commitments were always in tension; ADR-0005 never resolved
it and neither did the first draft of this record.

Three corrections:

1. The table row now separates emission from retention, and the Context section
   states the consequence plainly: the calibration corpus must be re-derived from
   committed history rather than harvested, and re-derivation is only partial
   because `--snapshot` never existed historically — so a `not-proven` from an
   absent snapshot is byte-identical to one from evaluated-and-false evidence.
2. §3 now requires precision/recall/FNR be published twice: whole-gate and
   probabilistic-marginal (cases where no deterministic trigger fired), and
   states that the marginal figure is what satisfies the obligation. Escalation
   is an OR, so eight deterministic triggers at precision 1.0 by construction
   would otherwise let a worthless probabilistic pass hide behind them — the
   theater failure this record exists to prevent, one level down.
3. Consequences records the permanent loss and requires the evidence index to
   disclose snapshots as reconstructed, and to distinguish evaluated-and-false
   from evidence-absent.

Corrected before the record was pushed. Shipping an accepted ADR whose evidence
table asserts a calibration foundation that does not exist would have been the
manufactured false assurance it warns against.

`adr lint`: 27 records, 0 errors, 0 warnings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom
mbeacom force-pushed the mbeacom-vigilant-garbanzo branch from 5944e59 to d4f55e2 Compare August 13, 2026 00:58
mbeacom added a commit that referenced this pull request Aug 13, 2026
…metrics

ADR-0027 converted ADR-0005's unmeetable per-release reporting commitment into a
shipping precondition: no probabilistic pass may ship without a frozen holdout
that existed before the pass produced its first score. This scopes the feature
that makes that precondition satisfiable. Until it lands, no probabilistic pass
can ship at all, so specs/011-probabilistic-evaluator-passes is gated on it.

The investigation corrected the premise it started from. ADR-0027's table
recorded ADR-0005 action item 3 ("log every escalation decision with reason
codes from day one") as Met. That is true of emission and false of retention:
the codes are emitted every run and retained on none, and zero historical Pass 0
evaluations exist anywhere. Phase 4's own exit criterion in plan.md reads
"Escalation reason codes logged from the first run — this is the calibration
set, and it cannot be backfilled." The warning came true. ADR-0027 was corrected
upstream (PR #139) before it was pushed; this spec starts from the corrected
text.

It is also not a bug to fix by adding persistence. specs/005 SC-008 ratifies
"Pass 0 persists nothing", SC-012 forbids any write, evaluate.ts has no --write
by design, and ADR-0027 §1 lists "persist" among the forbidden verbs. So the
corpus is a separate tracked artifact in git (ADR-0004), re-derived from
committed history. No persistence is added to any evaluator surface.

Re-derivation is only partial, and the gap is invisible in the output.
`adr evaluate` requires --snapshot; historical runs had none. Only one-way-door
and contradicts-accepted-adr are faithfully re-derivable, regulatory partially;
the other five need routingEvidence that never existed. A not-proven from an
absent snapshot is byte-identical to one from evaluated-and-false evidence, so
the case format records a third state, evidence-absent, which never enters a
confusion-matrix cell.

Six metrics are fixed now, before any pass exists to define them favorably to
itself. Each escalation figure publishes twice — whole-gate and
probabilistic-marginal (cases where no deterministic trigger fired). Escalation
is an OR, so eight deterministic triggers at precision 1.0 by construction would
otherwise let a worthless pass hide behind them. That argument is normative in
ADR-0027 §3, which states the marginal figure satisfies the obligation and
whole-gate alone does not.

The gate detects "a pass shipped" from two sources that must agree: a declared
registry cross-checked against the dependency-boundary evidence specs/005 SC-006
already asserts. A single registry is a self-report and the obvious thing to
forget. Ordering is proven by commit ancestry, not a date field. Everything is
fail-closed, and every gate carries an observed-failing-first task per ADR-0016.

The absence statement is enforced on the no-correctness-claim.test.ts model and
auto-flips: forbidden the moment a pass is declared, at which point the marginal
figures become required. A statement that is only remembered decays; one that
never flips becomes a lie.

Five [NEEDS CLARIFICATION] items stay open by decision rather than oversight. No
epsilon value and no N value ship — only the derivation mechanism and the
not-computable behavior, so an unset constant can never read as green. The
override rate is published as not-computable with a reason code naming the
missing decision log, neither invented nor silently dropped. With 27 records,
the not-computable path is expected to be exercised on day one.

Scoped only, per ADR-0014; landing targets rungs 1-2 and rung 3 stays open. No
model, no schema change, no approval, no label-class rubric edit. adr lint: 27
records, 0 errors, 0 warnings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom added a commit that referenced this pull request Aug 13, 2026
The spec cited 012's frozen definitions at commit e7c2fb7. That commit is
unreachable from any remote branch — verified with git branch -r --contains,
which returns nothing. 012's branch was rebased twice after it was written:
once onto the coordinator's re-signed ADR-0027 commits, and once for
git rebase --signoff because its own commits lacked DCO trailers.

Swapping in a current SHA would only defer the problem. A squash-merge
creates a new SHA again, so any branch-SHA citation of unmerged work is
fragile by construction. 012 hit the same failure citing the coordinator's
5944e59 in five places and replaced all five with PR #139.

Cite PR #141 and the FR numbers instead, and state the rule in the document
so the next reader does not reintroduce a SHA. The dead SHA is retained in
one sentence as the worked example of why, rather than deleted silently.

FR-001-FR-029, SC-001-SC-017, T001-T040. adr lint 27/0/0. Still scoped per
ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Both findings are from the review on #139 and both are correct.

ADR-0005 is dated 2026-07-18 and ADR-0027 is decided 2026-08-12 — a span of
**25 days**. ADR-0027's Context claimed "for eleven months it has been the sole
item in `adr queue`". That number was never computed from the dates; it was
asserted and then repeated into the PR narrative. A record whose subject is an
evaluator that must not assert more than its evidence supports had a fabricated
fact in its second paragraph.

The corrected passage keeps the point the duration was standing in for, which
survives without it: the record did not stall for long, it stalled
structurally, and its 2027-01-18 `reviewBy` would not have forced the question
for another five months. That is why supersession was needed rather than
waiting.

MANIFEST.md's tree listing was updated when 0027 landed, but two prose sections
were not, so the file contradicted itself:

- "One record remains `proposed` (0005)" — false since the supersession; there
  is now no proposed record and `adr queue` is empty.
- The Verification section still read "27 files … 26 records, ids 0001–0026 …
  24 accepted, 1 proposed, 1 superseded".

Both now match the tree, which was counted rather than reasoned about: 28 files,
27 records, ids 0001–0027, 25 accepted, 0 proposed, 2 superseded, template at
draft.

This is the same defect class the file already carries an open issue for (#131,
MANIFEST drifts silently because it is hand-maintained with no CI check) — and
it is why the earlier commit in this PR that "synced derived surfaces" did not
catch it: that sweep updated the listing and never re-read the prose asserting
counts about the listing.

Ratification state checked while here: all 25 accepted records carry a named
`provenance.ratifiedBy`, which is what the schema requires of an agent-drafted
record reaching accepted. `review.decidedAt`/`approvals` are optional and
populated on 7 of 25; that inconsistency predates this PR and is not backfilled
here, because the decision dates for the other 18 are not known.

Verified: `adr lint` 27 records / 0 errors / 0 warnings; `adr queue` empty;
`bun test` 2064 pass / 0 fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom
mbeacom merged commit 79e0f94 into main Aug 14, 2026
11 checks passed
@mbeacom
mbeacom deleted the mbeacom-vigilant-garbanzo branch August 14, 2026 00:56
mbeacom added a commit that referenced this pull request Aug 14, 2026
The spec cited 012's frozen definitions at commit e7c2fb7. That commit is
unreachable from any remote branch — verified with git branch -r --contains,
which returns nothing. 012's branch was rebased twice after it was written:
once onto the coordinator's re-signed ADR-0027 commits, and once for
git rebase --signoff because its own commits lacked DCO trailers.

Swapping in a current SHA would only defer the problem. A squash-merge
creates a new SHA again, so any branch-SHA citation of unmerged work is
fragile by construction. 012 hit the same failure citing the coordinator's
5944e59 in five places and replaced all five with PR #139.

Cite PR #141 and the FR numbers instead, and state the rule in the document
so the next reader does not reintroduce a SHA. The dead SHA is retained in
one sentence as the worked example of why, rather than deleted silently.

FR-001-FR-029, SC-001-SC-017, T001-T040. adr lint 27/0/0. Still scoped per
ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom added a commit that referenced this pull request Aug 14, 2026
…metrics

ADR-0027 converted ADR-0005's unmeetable per-release reporting commitment into a
shipping precondition: no probabilistic pass may ship without a frozen holdout
that existed before the pass produced its first score. This scopes the feature
that makes that precondition satisfiable. Until it lands, no probabilistic pass
can ship at all, so specs/011-probabilistic-evaluator-passes is gated on it.

The investigation corrected the premise it started from. ADR-0027's table
recorded ADR-0005 action item 3 ("log every escalation decision with reason
codes from day one") as Met. That is true of emission and false of retention:
the codes are emitted every run and retained on none, and zero historical Pass 0
evaluations exist anywhere. Phase 4's own exit criterion in plan.md reads
"Escalation reason codes logged from the first run — this is the calibration
set, and it cannot be backfilled." The warning came true. ADR-0027 was corrected
upstream (PR #139) before it was pushed; this spec starts from the corrected
text.

It is also not a bug to fix by adding persistence. specs/005 SC-008 ratifies
"Pass 0 persists nothing", SC-012 forbids any write, evaluate.ts has no --write
by design, and ADR-0027 §1 lists "persist" among the forbidden verbs. So the
corpus is a separate tracked artifact in git (ADR-0004), re-derived from
committed history. No persistence is added to any evaluator surface.

Re-derivation is only partial, and the gap is invisible in the output.
`adr evaluate` requires --snapshot; historical runs had none. Only one-way-door
and contradicts-accepted-adr are faithfully re-derivable, regulatory partially;
the other five need routingEvidence that never existed. A not-proven from an
absent snapshot is byte-identical to one from evaluated-and-false evidence, so
the case format records a third state, evidence-absent, which never enters a
confusion-matrix cell.

Six metrics are fixed now, before any pass exists to define them favorably to
itself. Each escalation figure publishes twice — whole-gate and
probabilistic-marginal (cases where no deterministic trigger fired). Escalation
is an OR, so eight deterministic triggers at precision 1.0 by construction would
otherwise let a worthless pass hide behind them. That argument is normative in
ADR-0027 §3, which states the marginal figure satisfies the obligation and
whole-gate alone does not.

The gate detects "a pass shipped" from two sources that must agree: a declared
registry cross-checked against the dependency-boundary evidence specs/005 SC-006
already asserts. A single registry is a self-report and the obvious thing to
forget. Ordering is proven by commit ancestry, not a date field. Everything is
fail-closed, and every gate carries an observed-failing-first task per ADR-0016.

The absence statement is enforced on the no-correctness-claim.test.ts model and
auto-flips: forbidden the moment a pass is declared, at which point the marginal
figures become required. A statement that is only remembered decays; one that
never flips becomes a lie.

Five [NEEDS CLARIFICATION] items stay open by decision rather than oversight. No
epsilon value and no N value ship — only the derivation mechanism and the
not-computable behavior, so an unset constant can never read as green. The
override rate is published as not-computable with a reason code naming the
missing decision log, neither invented nor silently dropped. With 27 records,
the not-computable path is expected to be exercised on day one.

Scoped only, per ADR-0014; landing targets rungs 1-2 and rung 3 stays open. No
model, no schema change, no approval, no label-class rubric edit. adr lint: 27
records, 0 errors, 0 warnings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom added a commit that referenced this pull request Aug 14, 2026
…140)

* docs(specs): scope feature 011 — probabilistic evaluator passes 1-3

Scopes Passes 1 (retrieval), 2 (rubric scoring), and 3 (adversarial), plus
the three escalation triggers ADR-0027 section 2 records as deliberately
deferred: low-confidence, pass-disagreement, novel-no-precedent.

Scoping only. No code under packages/. Status is 'scoped' per ADR-0014 and
nothing above it. Implementation is blocked on two hard gates, neither
satisfiable today: feature 012's frozen holdout set with its precondition
gate observed failing (ADR-0027 section 3/4, ADR-0016), and an accepted ADR
ratifying the harness-driven architecture.

Load-bearing design: the model produces evidence, deterministic code does
the deciding. adrkit emits a prompt bundle and consumes a structured
response; the harness makes the model call. No package opens a socket. This
extends the existing RoutingTriggerEvidence boundary rather than inventing
one, and makes 'escalation is never model discretion' a structural property
instead of a promise. It satisfies Principles II and IV with nothing
amended; the in-adrkit model client is documented as rejected-unless-amended
with its constitutional cost stated.

Four findings from scoping:

- No schema change is needed. All eleven EscalationReason values and the
  scores/confidence fields already exist, so Principle V's one-way door is
  not tripped.
- novel-no-precedent is blocked on a primitive that does not exist. There is
  no relevance scoring in the repository; MCP search documents itself as
  'No stemming, fuzzy, weighting, or ranking'. This feature does not build a
  ranker and reports the trigger as evidence-absent, which is the honest
  state.
- ADR-0027 section 3's probabilistic-marginal obligation imposes two output
  requirements here, not only on 012: the marginal subset must be
  mechanically determinable, and evidence-absent must be distinguishable
  from evaluated-and-false.
- A downward D4 correction escalates to a named human and never silently
  re-routes; a model changing a review tier is model discretion over
  escalation.

Six open questions are carried as [NEEDS CLARIFICATION] rather than
answered by invention, including the undefined 'aggregate confidence' the
rubric never specifies and the exact per-tier weights, which are a rubric
change and therefore an ADR.

Verified: adr lint 27 records / 0 errors / 0 warnings; bun run lint green;
the 20 purity and contract gate tests this spec cites are green unmodified.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): fold in the Q7 ruling and feature 012's frozen definitions

Q7 is resolved architecturally and narrowed to token selection.

Ruled: the routing vocabulary is not widened. proven/not-proven is correct
for routing, whose rule is escalate only on proven evidence; widening it
would change landed behavior for eight triggers to serve a calibration
need. Calibration becomes a separate namespace reusing neither token, owned
by 012. Verified this trips no one-way door: not-proven appears 0 times in
schema/adr.schema.json and 0 times in adr.schema.ts, so a distinct
calibration vocabulary needs no schema change and no Principle V ADR. That
is recorded so the question is not re-opened out of a fear of a breaking
change that does not exist.

The consequence that outlives the naming: for the eight landed triggers the
false-versus-absent distinction is destroyed at emission and cannot be
retrofitted without changing landed behavior, so the whole-gate recall
denominator is structurally weaker than the marginal one, permanently. The
marginal figure is the only one whose denominator can be clean. That is a
second and sharper justification for ADR-0027 section 3 than the record
gives, and it converts the marginal requirement from a policy choice into a
measurement necessity. Recorded in Q7 and in FR-020's rationale.

Feature 012's frozen definitions are now cited by their own FR numbers at
commit e7c2fb7 — the dual figure (FR-016), not-computable with a machine
reason code (FR-017a), the positive-class mapping (FR-018), per-dimension
drift with no epsilon shipping (FR-019/019a), inter-pass disagreement with
0.0 as a defect signal (FR-020), and the consumption relationship their
T044 verifies (FR-022).

Adds FR-028: an uncomputable value renders as not-computable with a machine
reason code, never coerced or defaulted into something readable as success.
Adds T037, which produces the epsilon-derivation observation set 012 needs —
two model versions over the same frozen holdout, reported per dimension and
never averaged, since an average hides a compensating pair. Adds T038 for
FR-028, observed failing.

T001 now enforces 012's actual ordering mechanism: commit ancestry, not a
date field. The holdout's freeze commit must be an ancestor of the first
commit that produces a score, which nothing here can satisfy after the fact.

FR-001-FR-028, SC-001-SC-016, Q1-Q7, T001-T038. Still scoped per ADR-0014
and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): ground the absence-as-evidence paragraph in existing law

The Overview claimed the hazard 'has already appeared three separate times
in this project's history'. That claim does not survive audit. One of the
three listed instances — an empty retrieval set read as novelty — never
happened: it was rejected during this feature's design, which is the system
working rather than a failure. Asserting a repeated historical failure on
that basis was a narrative outrunning its evidence, and this repository's
own rule is that absence is reported as absence.

Rewritten to say what is actually true and more useful: absence-reported-as-
evidence is the specific way this feature could fail while appearing to
succeed, and it is established project law rather than a new observation.
Now cites where the rule already lives — ADR-0014 (status reported as
explicitly absent or present, never assumed, never fabricated), ADR-0026
(a permission-shaped refusal is not an absence of the thing sought),
Principle IV (a missing backing source resolves to inert with an
informational finding, never a fatal error), and ADR-0027's Consequences
(a calibration set that silently conflates the two reports a precision it
did not measure).

This grounds the paragraph in enforceable records instead of a pattern
claim, and makes clear the feature inherits the rule rather than inventing
it. FR-018, FR-021, and FR-028 are named as the mechanisms that make the
mistake unrepresentable here rather than merely discouraged.

No pending project-wide record is implied or gestured at anywhere in this
feature; verified none of the three files contains such a gesture.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): adopt 012's frozen calibration tokens and close Q7

Feature 012 froze the three-state calibration vocabulary (their FR-005, PR
#141): condition-met, condition-unmet, evidence-absent. Adopted verbatim;
011 consumes and does not redefine. evidence-absent never enters a
confusion-matrix cell.

Q7 moves out of Open questions into a new 'Decisions recorded during
scoping' section, keeping its anchor so every inbound reference still
resolves. Six questions remain genuinely open.

FR-021 rewritten around the frozen tokens and gains the mechanical guard
012 flagged: the tokens must be compared by exact equality against an
exhaustive union, never by substring or prefix matching, because 'met' is a
substring of 'unmet' — a prefix or includes test would silently classify
condition-unmet as condition-met and invert the finding. SC-005 and T005
carry the same requirement, and T005 specifies a typed exhaustive union.
Also notes that scope-hierarchy.evidence-absent already exists as a
rule-level ReasonCode: the same idea at a different scope and field, a
deliberate echo rather than a collision.

FR-017 sharpened: 012 aggregates and never recomputes the contradiction
comparison, so a disagreement 011 fails to record is one their published
rate cannot recover. 011's obligation is to record three-state evidence in a
form they can aggregate directly; they own the predicate.

FR-020 inherits 012's whole-gate qualifier — every whole-gate figure carries
a machine-readable denominator-limitation qualifier from the report itself
rather than prose a consumer can forget to copy — and its rationale now
states the irreducible unverifiable population explicitly.

Per 012's correction, the record is explicit that the landed proven/
not-proven vocabulary is correct for routing and is not a defect: escalation
fires only on proven evidence, so false and absent route identically and
must. It is merely insufficient for a calibration use case that did not
exist when it was written. Widening it is forbidden by FR-002.

ADR-0027's own 'evaluated-and-false' prose is preserved verbatim where it is
quoted about the landed eight triggers.

FR-001-FR-028, SC-001-SC-016, Q1-Q6 open plus Q7 resolved, T001-T038. Still
scoped per ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): address deep-review findings and fold in 012's Q1/Q2/Q3 answers

A four-lens deep review (adversarial, architect, consumer,
governance-compliance) over pinned commit af302e4 returned 35 findings,
3 blocking. Two lenses independently found the same FR-027 defect.

Blocking 1 - FR-027's shipped-pass detector was inverted. It keyed on a
dependency-graph cross-check, but under the harness-driven architecture this
same spec ratifies, no model/prompt/embedding/retrieval dependency is ever
added, so that side of the check is permanently empty by construction. An
honest declaration reads as 'a registry entry with no matching dependency'
and would fail every release forever; an undeclared pass leaves both sides
empty, they agree, and the gate stays silent - reducing ADR-0027 section 3's
precondition to voluntary self-declaration. FR-027 now states why the graph
cannot be that signal, lists candidate replacements, and requires both specs
to name the same detector or to state plainly that the registry is
self-declared. Raised as new Q8.

Blocking 2 - FR-020's marginal partition was model-dependent. FR-012
recomputes routing over a Pass 2 correction to reversibility, and a Pass 2
D5=0 satisfies contradicts-accepted-adr, so 'did any deterministic trigger
fire' could be answered by Pass 2's own output. Pass 2 would choose its own
denominator: every case it corrected into deterministic escalation would
leave the marginal subset, and it would be measured only over the cases it
declined to touch - reconstituting the masking ADR-0027 section 3 closes.
The partition is now computed from the pre-probabilistic routing decision,
and the output must carry both decisions.

Also corrected a misattribution the governance lens caught: the --json
rendering asymmetry is named in a commit message, not in ADR-0027. The
record addresses the evaluated-and-false versus evidence-absent conflation,
not rendering. The claim is now stated as an observation about the current
implementation.

Further findings addressed: FR-029 added because nothing owned merging the
eight landed triggers with the three new ones into the canonical order that
patch/project.ts emits and the ARB queue routes on - without it no
probabilistic reason would ever reach escalationReasons; FR-010 now requires
citations be validated rather than merely present, since presence-only
enforcement is satisfiable by a plausible span that quotes nothing; FR-011
stops claiming the caps are model-independent when two of three antecedents
are model-supplied judgments, and requires recording which evidence fired
each cap; FR-014 states the limit of what adrkit can enforce about a
'separate context' it does not control; FR-007 no longer claims retrieval
has no effect on routing when it feeds the passes that do. Tasks gained the
golden fixture that T007 assumed existed, an observed-failing requirement on
T032, a resolve-not-defer obligation on Q6 in T003, an instruction that
answers are recorded by amending spec.md, and a traceability index.

From feature 012: Q1 resolved as citation coverage over a fixed denominator
of 8, Q2 resolved as the two-disjunct contradiction predicate with the
rubric's own >= 3 anchor, and Q3's relevance floor withdrawn rather than
deferred. FR-028 expands to 012's four not-computable classes, with
undefined-value and measurement-failed forbidden from collapsing into each
other - zero escalations across the holdout is a finding, not a failure.
Drift now reports both surviving and raw scores so a citation-behaviour
shift is distinguishable from a judgment shift.

Spike 009's carry-forward blocker is now cited as discharged rather than as
a live cautionary tale.

FR-001-FR-029, SC-001-SC-017, five open questions, T001-T040. Still scoped
per ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): address Copilot review — scope the network claim, exempt an unevaluable trigger, close test gaps

Copilot's review independently reproduced the FR-027 detector defect two of
my review lenses found (already fixed in 2b93b24) and added five genuine
findings that all four lenses missed.

The strongest: FR-004 and SC-004 claimed 'no adrkit package opens a network
connection'. That is false today, before this feature starts. packages/ci
declares @actions/github and constructs an Octokit client in
packages/ci/src/github.ts to post PR comments with the workflow token, which
made SC-004 and T032 unsatisfiable as written. Verified against the source
and the package manifest. The prohibition is now scoped to what this feature
actually governs — no adrkit package may call a model or hold model
credentials, and no network-capable dependency may be added to
@adrkit/evaluator or @adrkit/cli — with the pre-existing, separately-governed
CI client named as out of scope.

Second: making novel-no-precedent permanently evidence-absent contradicted
SC-005 and T024, which required all three states for each of the three
triggers, and SC-011, which required a novel proposal to reach the met state.
That would have forced either a fabricated fixture or a permanently red
suite. The trigger is now explicitly exempt while no relevance primitive
exists, fixtured as evidence-absent only, plus an assertion that an empty
retrieval result can never be promoted to condition-met.

Three test gaps closed: T008 omitted FR-006's fourth retrieval category (the
originating diff or spec artifact), which no other task covered; T009 tested
only the unconfigured-strategy case while FR-009 also requires absent and
stale index handling, and a stale derived projection consumed as
authoritative is exactly what ADR-0004 forbids; and FR-010's second clause —
every score below 3 names the specific missing thing — had neither a test nor
an implementation step, so T016 and T018 now cover it alongside citation
validation.

Also corrected stale vocabulary Copilot's first comment surfaced: five
acceptance scenarios still said 'proven' for the new triggers after the
condition-met adoption, and the token citation pointed at e7c2fb7, which is
the metric-definitions commit rather than the later commit that froze the
vocabulary. Both fixed, with a note that the condition-* tokens superseded an
earlier proven/not-proven draft in 012.

FR-001-FR-029, SC-001-SC-017, five open questions, T001-T040. adr lint 27/0/0.
Still scoped per ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): distinguish structural from incidental absence for novel-no-precedent

Feature 012's FR-023a adds a distinction 011 needs to consume, and it closes
a real gap: their fail-closed gate could have rejected a trigger that is
recorded evidence-absent for every case, treating a structurally permanent
and fully expected state as suspicious. That is a false alarm blocking every
release on a condition nothing can satisfy.

Two axes, now stated explicitly in FR-018: trigger evidence states (FR-021)
and metric not-computable classes (FR-028) are different things. A trigger
recorded evidence-absent for every case is not a missing outcome label class
and must not fail the gate.

The class matters too. novel-no-precedent's absence is nothing-to-measure -
structurally absent, because no relevance primitive exists at all and no case
could ever supply one - not input-unavailable, which means the primitive
exists and a particular case's inputs could not supply it. Collapsing the two
would hide a regression in which a working primitive silently stops producing
evidence, by making it look like the permanent expected condition. If a
relevance primitive ever ships, this trigger moves from nothing-to-measure to
input-unavailable, and that transition must be visible. T024 now asserts both
halves.

Also corrects the last stale token: plan.md's Complexity Tracking still said
'fabricating proven from an empty result set' after the condition-met
adoption. Verified zero remaining stale tokens across all three files, and
verified the spec continues to state that the landed proven/not-proven
routing vocabulary is correct for routing and explicitly not a defect.

FR-001-FR-029, SC-001-SC-017, T001-T040. adr lint 27/0/0. Still scoped per
ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): cite feature 012 by PR and FR number, never by commit SHA

The spec cited 012's frozen definitions at commit e7c2fb7. That commit is
unreachable from any remote branch — verified with git branch -r --contains,
which returns nothing. 012's branch was rebased twice after it was written:
once onto the coordinator's re-signed ADR-0027 commits, and once for
git rebase --signoff because its own commits lacked DCO trailers.

Swapping in a current SHA would only defer the problem. A squash-merge
creates a new SHA again, so any branch-SHA citation of unmerged work is
fragile by construction. 012 hit the same failure citing the coordinator's
5944e59 in five places and replaced all five with PR #139.

Cite PR #141 and the FR numbers instead, and state the rule in the document
so the next reader does not reintroduce a SHA. The dead SHA is retained in
one sentence as the worked example of why, rather than deleted silently.

FR-001-FR-029, SC-001-SC-017, T001-T040. adr lint 27/0/0. Still scoped per
ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): name the FR range on the 012 citation line

Follow-up to 7b3a97c, which removed the dangling e7c2fb7 SHA. The header
line cited only PR #141, leaving the FR anchors to the individual bullets.
Naming the range (FR-005, FR-016 through FR-023a) makes the pointer complete
at the point of reference, so a reviewer checking whether the definitions
were reproduced faithfully — the check the conflict rule invites — lands on
the requirements rather than on a whole-file snapshot.

FR numbers survive every rebase; the PR number survives squash-merge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): state that observed-failing is a claim about a check's scope

A check that ran and found nothing, and a check whose scope could never have
found it, render identically — both are a silent green pass. Observing a
check fail is what separates them, because it proves the check's scope
actually reaches the thing it reports on. That is ADR-0016's own rationale
stated at the point where this feature's tasks rely on it, not a new
principle.

It explains why T032 and T036 must be observed failing in both directions
rather than once: a cross-check that has only ever agreed has not
demonstrated it is capable of disagreeing, so it would report the honest
state and the broken state identically.

Q8 gains the same framing, because the shipped-pass detector is precisely
this failure at the gate level: keyed on a signal that cannot fire, it
reports 'no disagreement' in the same bytes whether the registry is honest
or empty. Not a gate that fails open — a gate whose scope never covered the
question.

Prompted by the coordinating session, which found it in its own verification
of this branch: it grepped an enumerated list of known SHAs rather than the
pattern, so the query could not have matched the SHA in question, and the
empty result was reported as evidence of cleanliness.

No new principle is introduced and no project-wide record is implied.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): stop implying a relevance floor exists

Feature 012 withdrew the relevance-floor parameter rather than deferring it,
and FR-018 records that. Two of my own lines still spoke of 'the relevance
floor' as a thing that exists: US4 acceptance scenario 4 and SC-006.

Both now say what is true. The scenario is stated as counterfactual - if a
primitive existed and retrieval admitted nothing, the trigger would be
condition-met - and notes the state is unreachable today rather than
implying a threshold is merely unconfigured. SC-006 says 'below any ranking
threshold' and states the reason it matters: rule-admitted records must be
immune to ranking defects, which is why FR-006 categories (b) and (c) are
unconditional.

The rubric's own wording is preserved verbatim where the trigger table
quotes it, since that is a quotation of docs/EVALUATOR_RUBRIC.md and not a
claim by this spec.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): require the not-computable class be computed, not branched on identity

Feature 012 found this in their FR-023a and it applies identically here.
FR-018 required the nothing-to-measure to input-unavailable transition to be
visible, but never said the class must be computed rather than assumed. An
implementation could write:

  if (trigger === 'novel-no-precedent') return 'nothing-to-measure'

which is correct the day it is written and silently wrong the day a ranking
primitive ships: the trigger keeps reporting nothing-to-measure while a real
primitive sits behind it producing nothing, and the transition this spec
requires to be visible never appears. Without the mechanism, 'must be
visible' was an aspiration.

The class must now be derived from observed state — is a ranking strategy
configured, did it produce output — so the transition happens by itself.
T024 observes a hardcoded-per-trigger implementation failing, and asserts a
primitive that is present but produces nothing yields input-unavailable.

Stated once in its general form, since it is not specific to relevance
primitives: any place a reporter encodes 'this will always be X' as a branch
rather than deriving X is correct when written, invisible when it rots, and
rots on exactly the day the thing it described stops being true — which is
the day someone is relying on the report. FR-028's absence-statement rule and
012's FR-023a and FR-026 are all instances.

Also extends the observed-failing note with the sharpest form of the same
hazard: a check that shares an assumption with the thing it checks cannot see
the defect, so its green result is not evidence. Construct the failing input
from the requirement's wording rather than from the code, so the check and
the code cannot be wrong together. This has now occurred three times in this
workstream, most recently a case-sensitive fix validated by a case-sensitive
grep.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): adopt 012's pass-surface detector and close Q8

Feature 012 chose the detector for the shipped-pass gate (their FR-013), and
it is their gate, so FR-027 adopts it rather than re-deriving it — the same
way this spec took their calibration tokens.

The registry is now cross-checked against the evaluator's committed public
surface: a shipped probabilistic pass must be invocable, meaning a
request-builder and response-parser reachable from
packages/evaluator/src/index.ts and its committed types. A pass no caller can
invoke has not shipped. That surface is committed, greppable, deterministic,
model-free, observable under the harness architecture, and — the property
that makes it a genuine second source — not authored by the same edit that
writes the registry entry.

The dependency graph is withdrawn for two independent reasons, either
sufficient alone. It is empty by construction under the harness architecture.
And it would be toothless even if non-empty: scripts/check-deps.ts is an
allowlist whose own comment at line 187 records that a package with no entry
is 'silently unconstrained and passes check:deps no matter what it declares',
and no denylist of model libraries exists in the repository. Verified both
against the source rather than taking the claim on report.

Per 012 FR-013a the gate fails when the second source is structurally unable
to observe — a cross-check whose second source is a constant is one source
wearing two names, which is the defect this replaced. Per FR-013b both specs
name the same detector; FR-027 is this side of that agreement. T036 now
observes the disagreement failing in both directions and observes the
unobservable-surface case.

Two further consequences taken from 012:

FR-028 records that a not-computable probabilistic-marginal figure does not
discharge ADR-0027 section 3 once a pass is declared, and fails their gate
(012 FR-023b). Their validity preconditions bound the holdout as a whole
while every figure discharging section 3 is computed over the marginal
subpopulation, so a pass could otherwise ship measured over nothing with
every stated rule followed. This scopes rather than contradicts the
four-class table; undefined-value remains a finding where it is informative.

RubricScoreSnapshot and AdversarialSnapshot are now defined at field level by
012 in contracts/metric-definitions.md, and that contract is normative here.
012 defines them rather than citing this spec because an out-of-tree spec on
an unmerged branch is not a citable contract under Principle I, and 012 must
be self-contained since it gates this feature. A conforming producer may
carry extra fields.

Q8 moves to Decisions recorded during scoping with its anchor preserved. Four
questions remain genuinely open.

FR-001-FR-029, SC-001-SC-017, T001-T040. adr lint 27/0/0. Still scoped per
ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): state the detector's bounded coverage, and close two denominator holes

My previous commit closed Q8 in the resolved-decisions section while FR-027
still carried the pre-decision text listing candidate signals. The two
disagreed: a reader arriving at the requirement would have found the question
open, and a reader arriving at the decision would have found it settled. An
earlier script aborted partway and I did not re-verify the requirement body
afterwards. FR-027 is now rewritten to name the adopted detector.

Adopts 012's FR-013c, which is the more important half. The pass-surface
signal is strictly stronger than what it replaced — that one could never fire
— but it is not a proof: an implementer who places the request-builder and
response-parser outside every inspected location defeats it entirely. So the
gate contract must enumerate the locations constituting a pass surface, a
declared pass whose surface is in none of them is a measurement-failed rather
than a tolerated gap, and any artifact describing the gate must state that
its coverage is bounded by that enumeration. This spec now carries that
statement about itself.

The honest fallback is written in as the correct resolution rather than a
concession: if no enumeration holds, declare the registry self-declared and
say plainly the cross-check is not evidence of pass-shipping. A gate that
reads as mechanical and is not buys confidence it has not earned, which is
this feature's own subject applied to itself.

Two holes 012 found in definitions this feature consumes, both of which would
have corrupted a denominator rather than failed loudly:

FR-016 — low citation coverage has two causes, a pass that ran fully and
scored poorly and a pass that produced almost nothing. Without a boundary the
second fires low-confidence, records condition-met, and counts as a measured
escalation, from which recall 1.0 is manufacturable out of a pass that hardly
executed. Below 012's coverage boundary the trigger now records
evidence-absent so it leaves the denominator instead of flattering it.

FR-017 — the ordinary agreement case must be recorded as condition-unmet
rather than left stateless. Both passes ran and no contradiction was found:
that is a measured negative and belongs in the denominator. Left stateless
the denominator would hold only disagreements and collapse toward 1.0 by
construction, making the rubric's 'zero disagreement is a defect signal'
unreachable — the one reading meant to alarm could never occur.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T040. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): adopt the decided detector — report field primary, export surface secondary

Feature 012 decided Q8 rather than leaving a marker open, and their second
argument defeats the objection I had raised against this exact candidate.

I had argued the PassAbsence field was weaker because it lives in emitted
output, so reading it means running the evaluator and its absence is
ambiguous. That was wrong about their formulation: the field is on the
committed report type in packages/evaluator/src/types.ts, which is committed
state and greppable without executing anything. It therefore has the property
I wanted from the export surface, plus the enumerability I had conceded it
was better at.

Their decisive reason is one neither of my arguments reached: it makes the
detector and the metrics read the same artifact. A pass that hides from the
detector by not reporting its results also starves the metrics, whose figures
become evidence-absent or measurement-failed and, once a pass is declared,
fail the gate under FR-023b. The evasion path leads to a failure by another
route rather than to silence — and silence is the one thing a gate must never
produce about the thing it exists to detect. A module-location set has no such
coupling: evading it produces exactly nothing.

FR-027 now names both enumerated locations with the report field primary, and
states that carrying that field on emitted output is the whole of what
conforming requires. FR-013c's coverage bound is unchanged: this is a better
detector, not an airtight one, and the spec still says what it does not prove.

Also sharpens two definitions to 012's more precise wording. FR-017 now turns
on 'produced no output' (evidence-absent) versus 'produced output containing
nothing' (condition-unmet) — Pass 3 running and raising no objection is the
second, and it is evidence. FR-016 carries their exact boundary: no comparable
result yields evidence-absent, a result covering at least one dimension is
computed normally, and escalations driven solely by structurally-absent input
are reported as a separate subpopulation rather than mixed into the headline.

Three headings still described the previous sole detector after the body was
rewritten — the same requirement-versus-decision drift I reported last round,
caught this time by grepping for the superseded phrase rather than by reading.
Reference integrity would not have caught it: every heading was well-formed.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T040. adr lint 27/0/0.
Still scoped per ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): make the superseded-vocabulary scan an enforceable check

Feature 012 generalized the repair for a withdrawn design after finding four
stale locations of the dependency-graph detector on their side, including one
in plan.md's rationale — where a reader following the reasoning would have
been persuaded of the superseded design by the document that supersedes it.

I ran the same scan here for the withdrawn mechanism's vocabulary rather than
for requirement ids, and it came back clean: every occurrence of check:deps,
dependency graph, or dependency-boundary in these three files either records
the withdrawal or refers to the architecture guard, which T032 already
annotates as explicitly not a detector of shipped passes. My plan.md never
argued the detector design, so their worst finding has no analogue here.
Reporting that as a scoped negative rather than a bare 'clean', since the
scope of a verification is part of its result.

T033 now enforces the discipline instead of leaving it advisory: when a design
is withdrawn its terms must not survive in prose that still argues for it, and
the scan targets the mechanism's words rather than the requirement's id —
reference integrity passes cleanly on this class, because every FR/SC id still
resolves while the prose disagrees. That is precisely how it escaped twice in
this feature and twice in 012.

Two rules travel with it: state the withdrawal where the old design was
argued, since rationale sections outlive requirement edits; and keep the
superseded reasoning attached as its own counterexample rather than deleting
it, so a reader wondering why the obvious approach is unused finds the answer
instead of proposing it again. Same reason the dead e7c2fb7 stayed.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T040. adr lint 27/0/0.
Still scoped per ADR-0014 and nothing above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): invert the detector ordering to exported surface primary

Feature 012 inverted their ordering in 068db2e after I adopted their earlier
one, leaving a live divergence: their surface-primary against my
field-primary. Two specs naming overlapping signals with opposite primaries
is not 'the same detector', and 012 FR-013b exists precisely to prevent it —
it would have surfaced as an implementer building one gate from two
documents. Inverted here to match.

Their two arguments are stronger than the ones I had made for the field, and
the first is a defect rather than a preference. 'A pass-result field on the
emitted report type' read literally invites the emitted output, and observing
a value there means running the evaluator over some input — which puts the
gate in violation of the neighbouring requirement confining it to committed
state at the release commit, and leaves an absent value unable to separate
'no pass shipped' from 'this run produced none'. That is the same reason the
ordering rule keys on commit ancestry rather than a date field.

The second is decisive and neither of us reached it earlier: a type can carry
a field before any pass exists to populate it, but an invocable surface
cannot be added speculatively. 'A pass no caller can invoke has not shipped'
is a necessity; a field's presence is a convention someone can satisfy early
or omit late. A detector must key on what a shipped pass cannot avoid having.

The field is kept as the second location rather than dropped, because its own
argument survives as a secondary benefit: a pass hiding by never reporting
also starves the metrics, whose figures become evidence-absent or
measurement-failed and fail the gate under FR-023b once declared. Evasion
leads to failure by another route rather than to silence. Real property, just
not one that outweighs being readable without execution.

The resolved-Q8 record states that the ordering was corrected once rather
than reading as first-time correct. A decision record that hides its own
revision is what this feature spends its requirements arguing against.

Applied the withdrawal-scan discipline codified in T033 immediately after
the edit — grepping for the superseded ordering vocabulary rather than for
requirement ids — and confirmed no location still names the field as primary.

Two further items from 012 that touch definitions consumed here. FR-017:
agreement is published alongside disagreement and the two share a
denominator, since the rubric asks for both and publishing one silently
substitutes a different measurement for the requested one — which is also why
ordinary agreement must occupy condition-unmet rather than nothing. FR-028:
measurement-failed is sub-classed environmental / artifact-defect /
corpus-inadequate, because retrying is right for the first and destructive
for the second, where it can discard the evidence identifying the defect.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T040. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): write the enumeration basis into the spec, and check asserted relationships

Two gaps, both of the class this feature keeps finding: a thing established
in conversation that never reached the artifact.

First, the enumeration basis. I argued for enumerating the pass surface by
committed type name rather than by module location, feature 012 adopted it
and landed it in their FR-013, FR-013c, SC-034a and T009 — and my own spec
still said only 'enumerate the locations', with no basis. So the two specs
had converged on the detector and its ordering while diverging on how it is
enumerated, which is the same defect FR-013b exists to prevent, introduced by
the session that proposed the fix. FR-027 now states it: one exported type
name per pass role, the request-builder's input type and the response-parser's
result type. A type name is precisely nameable, greppable and closed;
'every module reachable from index.ts' is a transitive graph property that
cannot be enumerated without judgment and is moved by ordinary refactors, so
its enumeration silently narrows as it goes stale.

Second, T039a. Feature 012 added a check after finding a success criterion
that asserted 'FR-024a cites FR-018a' while FR-024a cited nothing, with every
identifier resolving and their cross-reference check green. I reported the
same limitation from my own FR-027 incident — 'resolved' in one section and
'to be settled' in the requirement body, both internally well-formed — but
only reported it. Reference integrity is a check on form; this class is a
failure of agreement and passes ID resolution cleanly.

Ran the audit here before adding the task: seventeen success criteria against
twenty-nine requirements, checking that asserted relationships hold in the
target's text rather than that the id exists. Clean. Adding the check anyway,
because a clean result today says nothing about the next edit, and both
instances were found by reading rather than by any check either session runs.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T041. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): drop the detector rank — two co-equal enumerated locations

Feature 012 diagnosed the churn better than either of us argued the orderings.
We inverted the same rank twice in opposite directions: they made the field
primary, I adopted it, they inverted to surface-primary, I inverted to match —
and after my last message we were diverged again in the mirror image of the
first divergence. Four moves, each ordering argument individually sound.

The diagnosis: nothing depends on the rank. I verified that here before
dropping it rather than taking it on report — twelve ranking mentions across
the three files, and no gate behavior, evidence handling, cross-check, or
ordering anchor reads any of them. Either location firing detects.

A decorative distinction that cannot change an outcome but can disagree
between two documents that FR-013b requires to agree is a defect in the
framing, not a question with a right answer. It had already produced two live
cross-spec divergences and one contradiction inside a single spec.

FR-027 now states two co-equal locations with no primary, and both are
enumerated by committed type name. Both are retained, because each is hard to
omit in a way the other is not, and the spec carries that asymmetry as a table
rather than as a ranking: entry-point types cannot be added speculatively,
since a pass that ships must be invocable; and the report variant couples the
detector to the metrics, so hiding by not reporting starves them and fails the
gate under FR-023b rather than producing silence. Neither argument ranks the
locations — together they establish that both must be enumerated, which is the
stronger claim and survives dropping the rank.

The closed-decision record says the ordering churned twice before being
dropped rather than reading as first-time correct.

Ran the T033 superseded-wording scan after the edit: the only surviving
occurrences of the ranking vocabulary are the two sentences stating that no
primary exists.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T041. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): audit for negative cases that cannot fail, and extend the withdrawal scan

Feature 012's second withdrawal miss shows the rule I wrote in T033 is
necessary and not sufficient. I had said 'scan for the mechanism's vocabulary,
not the requirement's ID.' They then found four further references that
described the withdrawn mechanism by what it did, three of which contained no
word that scan searches for.

The worst was a success criterion enumerating a deliberate violation the gate
must be observed rejecting — one that could no longer occur. An implementer
building fixtures from it would have constructed a negative case that cannot
fail, satisfying ADR-0016 vacuously while reporting as coverage. In a project
whose central discipline is that a check counts only once observed failing,
that is the most dangerous artifact available: it is indistinguishable from a
working guard.

Audited every 'Observe ... failing' task here against that hazard before
extending the rule. All name violations still constructible under the current
design, including T036, which was the one most at risk because it was rewritten
when the detector changed: a registry entry with no observable surface and an
observable surface with no registry entry both remain possible. Clean, and
reported with its scope rather than as a bare clean.

T033 now requires a behavioural pass after the vocabulary scan, and an
impossibility audit of every deliberate-violation fixture, re-run whenever a
design it depends on changes.

The detector needs no further change: 012 dropped their rank and so did this
spec, so both enumerate both locations by committed type name with either
firing as detection, which is the whole of what FR-013b requires.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T041. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): stop narrating the rank inversions, and scope the no-primary statement

Feature 012 found two defects in their own text after adding the check that
catches them. Both were present here too, and I found them by reading my own
prose against their diagnosis rather than by any check I run.

First, 'there is no primary' read as normative about the detector when it is
only presentational about this spec. Under that reading a spec that did state
a primary would look like an FR-013b violation, which it is not: the agreement
condition is the same enumerated locations, on the same basis, with either
firing as detection. Presentation order is not part of it. The spec now says
so, so a reader comparing the two documents sees a difference in emphasis
rather than a conflict.

Second, and worse, both the requirement and the closed-decision record
narrated the inversions move by move — 'four moves', 'corrected toward each
other', 'diverged again in the mirror image'. That was already stale when I
read it: there were further inversions after I wrote it, and the count was
wrong. A move-by-move history of a contested point goes stale on the next
move, which is the same class of rot as prose describing a withdrawn design.
012 hit this two commits after adding the check for it; I hit it while
adopting their fix for something else.

Both passages now state that the rank was inverted repeatedly until it was
clear nothing depended on it, and stop there. The decision record says
explicitly that it does not narrate the sequence, and why. The revision is
still disclosed — a record that hides its own revision is what this feature
argues against — but the disclosure no longer carries a running tally that
must be maintained to stay true.

No inversion of the detector: this spec has been rank-free since 73f42c3, and
both specs enumerate both locations by committed type name with either firing
as detection, which is the whole of the agreement condition.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T041. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): restore the FR-023b consumption clause, which was never written

FR-028 was missing the requirement that consumes feature 012's FR-023b: once
a pass is declared, a not-computable probabilistic-marginal figure does not
discharge ADR-0027 section 3 and fails their gate.

It was not lost to a later edit. Checking every commit back to 16a5fb7 shows
it was never present in any of them — including the commit whose own message
states it was added. The python script carrying that edit aborted on an
unrelated assertion before writing, and the retry carried forward only the
other two changes. I then reported it as landed in a commit message and again
to feature 012.

That makes it the worst instance of the class this feature spends its
requirements on: a thing asserted in a report and absent from the artifact,
with every mechanical check still green because FR-028 existed and every
identifier resolved. Reference integrity cannot see a missing paragraph.

The requirement matters. Without it, 012's validity preconditions bound the
holdout as a whole while every figure discharging section 3 is computed over
the marginal subpopulation, so a holdout could satisfy every precondition
with an empty marginal subset and a pass could ship measured over nothing
with every stated rule followed.

Found by verifying feature 012's scope exclusion rather than accepting it.
They reported auditing the detector vocabulary this round and explicitly not
re-auditing FR-023b consumption, on the grounds that nothing since had touched
it. Checking that assumption showed a paraphrase of FR-023b had moved inside
FR-027's rationale, which led to checking the requirement itself.

Audited the other definitions consumed from 012 in the same pass: the FR-020b
boundary, the FR-020c ordinary-agreement state, agreement published alongside
disagreement, and the measurement-failed sub-classes are all present. This
was the only casualty.

FR-001-FR-029, SC-001-SC-017, four open questions, T001-T041. adr lint
27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): extend the withdrawal scan to the pull-request description

Feature 012 ran the withdrawal scan against a target neither of us had ever
scanned — the PR description — and found their own body had argued the
withdrawn check:deps detector for the entire review while every scan over
their three committed files came back clean.

I ran it against mine. Clean on withdrawn-design vocabulary, but four stale
claims, one of them substantive: the body described Q8 as 'now raised,
blocking registry semantics in both specs' when Q8 has been resolved for
several commits. A reviewer reading the description would have believed the
feature had an open blocking interface question. The other three were the
open-question count in two places, and a task range citing T040.

T040 does not exist. There are 41 tasks and the highest identifier is T039a,
because two suffixed ids make the count exceed the maximum. A reader grepping
the cited range finds nothing and concludes tasks are missing. 012 caught this
in my body and it was correct; their equivalent was FR-001-FR-052 against a
maximum of FR-030. The body now states counts and identifier bounds
separately, and names T040 as absent so the next reader does not rediscover
the gap.

T033 now scopes the withdrawal scan to the PR description as well as the three
files, with both sub-rules from real misses: a count stated as an identifier
range is its own claim, so verify the highest identifier exists; and a count
must match the property rather than the token, since counting a marker string
also counts legends, quotations, and the check's own text.

The PR body is where the design is argued to a reviewer and it is the one
artifact nobody re-reads. Committed-file scans were clean throughout while it
carried a resolved blocker as open.

FR-001-FR-029, SC-001-SC-017, four open questions, 41 tasks (max T039a). adr
lint 27/0/0. Still scoped per ADR-0014 and nothing above it. No files under
packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): resolve cross-artifact citations against the artifact they cite

Ran feature 012's range-endpoint check over all three files and the PR
description: every stated FR/SC/T range endpoint resolves, with one hit —
'FR-016-FR-023a', which is a citation of 012's requirements, not this
feature's.

That hit is a false positive produced by the check's own scope: resolving
identifiers against the local set cannot distinguish a dangling reference
from a cross-artifact citation. A check that tries will either report a valid
citation as broken or silently skip it, and the output looks the same either
way.

So I fetched their branch and resolved the citations against their actual
artifact rather than asserting them. All ten identifiers this spec cites from
012 exist there: FR-005, FR-013, FR-013a, FR-013b, FR-013c, FR-016, FR-020b,
FR-020c, FR-023a, FR-023b. Verified, not accepted.

T033 gains the rule that fell out of it: resolve a cross-artifact citation
against the artifact it cites, or state that it was not verified. Do not let a
check's scope decide a claim's meaning without saying so — which is the same
lesson as the wrong-SHA grep, the case-sensitive validation, and the PR
description nobody scanned, arriving one more time from a new direction.

The T040 row 012 flagged was already corrected in a3385ab, which is after the
commit their re-pinned check cited. Counts and identifier bounds are now
stated separately in the PR body.

FR-001-FR-029 (max FR-029), SC-001-SC-017 (max SC-017), 41 tasks (max T039a),
four open questions. adr lint 27/0/0. Still scoped per ADR-0014 and nothing
above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): fix a negative case that cannot fail in T024

Feature 012 found this class in their T023b using the name this spec gave it,
then observed that each of us found the other's vacuous check and neither
found their own by rule. That prompted me to re-read mine, and I had the
mirror image of their defect in the same requirement family.

T024 required observing a fixture 'in which a ranking strategy is present but
produces nothing'. FR-018 establishes that no relevance primitive exists and
that novel-no-precedent is evidence-absent for every constructible case,
permanently. So the hardcoded implementation the observation is meant to
reject returns the correct class for every input that can exist: the test
cannot distinguish it from a correct implementation, and it would have been
recorded as ADR-0016 coverage while proving nothing.

I had declared this area clean two rounds ago. That audit was a scan over the
deliberate-violation lines, not a reading of each task against the current
design — which is exactly the insufficiency 012 demonstrated and I then
restated in T033 without applying it to myself.

Fixed with their remedy: bind the observation to the classifier's own
boundary rather than to repository state. The class classifier is a pure
function over a state value this feature defines, so the synthetic state
'configured: true, produced: none' is constructible at the unit boundary even
though no primitive ships. A correct classifier returns input-unavailable, the
hardcode returns nothing-to-measure, and the observation is recoverable. The
reason the original phrasing was wrong is kept attached so it is not re-added.

Re-audited the remaining observation tasks by reading each one against the
current design rather than grepping. All name violations that are still
constructible: perturbing a Pass 0 code, a deliberately added model-SDK
dependency, an introduced write, a defaulted and a misclassified value, a
false assertion, a purity-trap violation, and both directions of the registry
cross-check. The test-first tasks fail because the implementation does not
exist yet, which is constructible by construction.

Two matches were the token-versus-property artifact again: T033 matched on its
own description of the class, and T037 on the phrase 'observation set', which
is data rather than an observed-failing task. Third occurrence of that filter
error today.

FR-001-FR-029 (max FR-029), SC-001-SC-017 (max SC-017), 41 tasks (max T039a),
four open questions. adr lint 27/0/0. Still scoped per ADR-0014 and nothing
above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): name the episodes, drop the running tallies

Feature 012 applied the durable-versus-tally distinction to their own spec and
found two. I had articulated that distinction and then not run it here, which
has been the recurring shape: writing the rule is not the same as running it.

Two tallies in this artifact.

T033 said the two rules followed 'from having hit this twice in this feature
and twice in feature 012'. That was a running tally of class recurrences and
was already false when written — this feature had a requirement body calling a
question open after the decision section closed it, a set of headings still
naming a superseded detector after its body was rewritten, and a PR
description arguing a withdrawn design for an entire review. Those episodes
are now named and the arithmetic is gone, with the reason recorded so it is
not reintroduced.

FR-027's SHA-fragility passage said 012's branch had been 'rebased twice' and
that a squash-merge would rewrite it 'a third time'. On re-reading, that is a
tally rather than a bounded episode: another rebase before merge makes it a
fourth, and the ordinal carries none of the argument. Now 'more than once' and
'again', which is what the passage was actually claiming.

One carve-out held on inspection, and over-applying the rule would be its own
error: a count of a specific finite sequence cannot be falsified by the class
recurring elsewhere, so those stay.

The residual scan flags exactly one line — T033 quoting the forbidden form as
its own counterexample. Use rather than mention, which is 012's observation
that a rule forbidding a pattern will contain the pattern, so any scan for it
must distinguish the two. Same root as a marker count that catches legends and
quotations, which is why the marker filters here key on position rather than
on the bare token.

FR-001-FR-029 (max FR-029), SC-001-SC-017 (max SC-017), 41 tasks (max T039a),
four open questions. adr lint 27/0/0. Still scoped per ADR-0014 and nothing
above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): add T039b for edits reported as made and never written

Feature 012 observed that none of the checks in either feature covers the
class that cost this spec an entire clause: an edit reported as made and never
written. They added T051e; this adds the equivalent here.

It earns its own entry because it is invisible to every other check present.
Reference integrity passes, the asserted-relationship check passes, adr lint
passes, and the withdrawal scan passes, because nothing is wrong with what is
there. And it is the only class in this family with no contradictory text to
catch by reading: withdrawn-design prose contradicts the current design, a
false assertion contradicts its target, a stale tally contradicts the record,
but a missing paragraph contradicts nothing. Nothing in the document points at
its absence.

Both known episodes had the same cause: a scripted multi-part edit aborted
partway on an unrelated assertion, the retry carried forward only some parts,
and the change was then reported complete in a commit message. Here it cost
FR-028 its FR-023b clause, absent from every commit including the one whose
message announced it, found only by bisecting for the text.

Ran feature 012's audit style over this spec first rather than assuming: all
29 requirements are defined exactly once, and ten load-bearing properties were
verified present by their own distinctive phrasing rather than by identifier.
Clean.

The commit adding this check reproduced the defect it describes. The edit
consumed T039's opening line and welded its body onto T039b's tail, leaving 41
tasks where there should have been 42. Caught by verifying the write — the
count disagreed with the expected value — then repaired by splitting at the
seam and re-reading both bodies end to end. Same shape as 012's T051c
consuming T051d an hour earlier, and the reason the task says verify the
write, not the intent.

FR-001-FR-029 (max FR-029), SC-001-SC-017 (max SC-017), 42 tasks (max T039b),
four open questions. adr lint 27/0/0. Still scoped per ADR-0014 and nothing
above it. No files under packages/.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

---------

Signed-off-by: Mark Beacom <m@beacom.dev>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
mbeacom added a commit that referenced this pull request Aug 14, 2026
…dition gate, metric contract (#141)

* docs(specs): scope 012-evaluator-calibration — frozen holdout, gate, metrics

ADR-0027 converted ADR-0005's unmeetable per-release reporting commitment into a
shipping precondition: no probabilistic pass may ship without a frozen holdout
that existed before the pass produced its first score. This scopes the feature
that makes that precondition satisfiable. Until it lands, no probabilistic pass
can ship at all, so specs/011-probabilistic-evaluator-passes is gated on it.

The investigation corrected the premise it started from. ADR-0027's table
recorded ADR-0005 action item 3 ("log every escalation decision with reason
codes from day one") as Met. That is true of emission and false of retention:
the codes are emitted every run and retained on none, and zero historical Pass 0
evaluations exist anywhere. Phase 4's own exit criterion in plan.md reads
"Escalation reason codes logged from the first run — this is the calibration
set, and it cannot be backfilled." The warning came true. ADR-0027 was corrected
upstream (PR #139) before it was pushed; this spec starts from the corrected
text.

It is also not a bug to fix by adding persistence. specs/005 SC-008 ratifies
"Pass 0 persists nothing", SC-012 forbids any write, evaluate.ts has no --write
by design, and ADR-0027 §1 lists "persist" among the forbidden verbs. So the
corpus is a separate tracked artifact in git (ADR-0004), re-derived from
committed history. No persistence is added to any evaluator surface.

Re-derivation is only partial, and the gap is invisible in the output.
`adr evaluate` requires --snapshot; historical runs had none. Only one-way-door
and contradicts-accepted-adr are faithfully re-derivable, regulatory partially;
the other five need routingEvidence that never existed. A not-proven from an
absent snapshot is byte-identical to one from evaluated-and-false evidence, so
the case format records a third state, evidence-absent, which never enters a
confusion-matrix cell.

Six metrics are fixed now, before any pass exists to define them favorably to
itself. Each escalation figure publishes twice — whole-gate and
probabilistic-marginal (cases where no deterministic trigger fired). Escalation
is an OR, so eight deterministic triggers at precision 1.0 by construction would
otherwise let a worthless pass hide behind them. That argument is normative in
ADR-0027 §3, which states the marginal figure satisfies the obligation and
whole-gate alone does not.

The gate detects "a pass shipped" from two sources that must agree: a declared
registry cross-checked against the dependency-boundary evidence specs/005 SC-006
already asserts. A single registry is a self-report and the obvious thing to
forget. Ordering is proven by commit ancestry, not a date field. Everything is
fail-closed, and every gate carries an observed-failing-first task per ADR-0016.

The absence statement is enforced on the no-correctness-claim.test.ts model and
auto-flips: forbidden the moment a pass is declared, at which point the marginal
figures become required. A statement that is only remembered decays; one that
never flips becomes a lie.

Five [NEEDS CLARIFICATION] items stay open by decision rather than oversight. No
epsilon value and no N value ship — only the derivation mechanism and the
not-computable behavior, so an unset constant can never read as green. The
override rate is published as not-computable with a reason code naming the
missing decision log, neither invented nor silently dropped. With 27 records,
the not-computable path is expected to be exercised on day one.

Scoped only, per ADR-0014; landing targets rungs 1-2 and rung 3 stays open. No
model, no schema change, no approval, no label-class rubric edit. adr lint: 27
records, 0 errors, 0 warnings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): pin the 011/012 interface — aggregate disagreement, don't recompute

The session scoping specs/011-probabilistic-evaluator-passes asked which of us
owns `contradicts(pass2Dimension, pass3Finding)`, since the rubric's
`pass-disagreement` trigger and this feature's inter-pass agreement metric are
the same comparison. It proposed one shared predicate with two consumers. The
question exposed a real weakness in FR-020, which restated the comparison in
prose and so invited a second implementation.

Sharing a predicate is not enough. Two callers of one predicate can still pass it
different arguments and diverge, and a published agreement rate that fails to
reconcile with the trigger's own firings is exactly the incoherence ADR-0027
exists to prevent. FR-020 now removes the second computation instead: the metric
aggregates the recorded `pass-disagreement` evidence and never re-derives the
comparison. One computation, one recording, aggregation only.

That surfaced a second defect. The old denominator was |H|, which counts a case
whose passes produced no comparable output as agreement. Since the rubric treats
zero disagreement as evidence a pass is not doing its job, counting missing data
as agreement manufactures the defect signal out of absence — the same conflation
FR-005's `evidence-absent` state exists to prevent. The denominator is now
evaluated cases only, and a `0.0` rate below N evaluated cases reports
not-computable rather than firing the signal, because a zero drawn from too few
cases is not evidence of the defect.

FR-020a draws the other boundary the question implied. Three tuning parameters
have a calibration story and belong here: epsilon, the `low-confidence` threshold
(rubric default 0.7), and the `novel-no-precedent` relevance floor. The functions
they threshold do not — those are functions of output shapes this feature does
not define, and inventing them here would mean inventing another feature's
schema. This feature calibrates a number; it does not decide what the number
measures. It does depend on one property of that number: confidence must be
derived from output structure, never model self-report, because a self-reported
confidence is model discretion wearing a threshold and a holdout cannot correct a
number the model is free to restate.

Two case-shape constraints the same review made explicit. FR-005a: a case stores
the label, never the derived `positive(c)` — a stored boolean beside a stored
label is two sources of truth for one fact, and FR-018's mapping must stay the
single point of change. FR-005b: no per-dimension reference scores, because drift
is the difference between two model versions' score sets over the same frozen H
and needs no ground truth; adding one would introduce a second labeling task with
its own circularity and inter-rater problems to support a metric that does not
require it.

The reciprocal dependency is now stated here too, so 012-gates-011 is legible
from both directions rather than only from 011's side.

SC-011a, SC-011b, and SC-020 added with observed-failing-first tasks (T037,
T037a, T043a). All FR/SC cross-references resolve; five [NEEDS CLARIFICATION]
markers intact. adr lint: 27 records, 0 errors, 0 warnings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): freeze the calibration state vocabulary and state the denominator asymmetry

The 011 session escalated a token collision rather than settling it, and the
ruling came back to this feature. `not-proven` is already taken:
packages/evaluator/src/types.ts declares status as 'proven' | 'not-proven', and
routing/route.ts emits `not-proven` both when a condition is evaluated false and
when optional evidence is missing. For the eight landed triggers that one token
means "false OR absent" — exactly the conflation this feature's third state
exists to undo. My spec had been reusing both tokens for the calibration case
format, which would have carried the ambiguity straight into a recall
denominator.

The landed routing vocabulary does not change. It is correct for routing —
escalation fires only on proven evidence, so false and absent route identically
— and widening it would alter landed behavior for eight triggers to serve
calibration, which FR-002 forbids.

FR-005 now freezes a separate three-token vocabulary that reuses neither:
condition-met, condition-unmet (ADR-0027's "evaluated-and-false"), and
evidence-absent (its "whose evidence was absent"). Comparison is exact equality
against an exhaustive union, never substring or prefix matching, because `met` is
a substring of `unmet`. Verified and recorded so nobody reopens the question
fearing a breaking change: `not-proven` appears 0 times in schema/adr.schema.json
and 0 times in packages/core/src/schema/adr.schema.ts — both vocabularies are
internal to the evaluator package and neither is published contract. Also
recorded that `scope-hierarchy.evidence-absent` already exists as a rule-level
ReasonCode expressing the same idea at a different scope: a deliberate echo, not
a collision.

FR-016a states the consequence neither spec had written down. For the landed
eight the false/absent distinction is destroyed at emission — it never enters the
report, so no consumer can recover it from the report alone, and reconstructed
snapshots recover it only partially. The three probabilistic triggers can carry
three states from day one; the landed eight cannot be retrofitted without
changing landed behavior. The asymmetry is therefore permanent. It is not a
defect to fix but a measurement limitation to state once, where a reader
computing a metric will see it.

That gives ADR-0027 §3's dual-figure rule a second and stronger justification.
The record argues from masking: eight triggers at precision 1.0 by construction
would hide a worthless Pass 2. The sharper reason is that the marginal figure is
the only one whose denominator can be clean. That turns the dual requirement from
a policy choice into a measurement necessity, which is much harder to erode
later. Mechanically, every whole-gate figure is emitted carrying a
machine-readable qualifier naming the conflation as a denominator limitation —
from the report itself, not from prose a consumer can forget to copy forward.

SC-005a, SC-005b added with observed-failing-first tasks T016 and T035a. 37 FRs,
28 SCs, all cross-references resolve, five [NEEDS CLARIFICATION] markers intact.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): partition not-computable so a broken measurement cannot read as absence

Two corrections from the coordinating session, one retiring a warning and one
adding a requirement.

Retired: spike 009's carry-forward blocker is discharged, not open. Feature 010
Phase B ran the fresh T014 -> T014a cycle rather than reusing the defective
oracle — T017 re-froze `derivedPathPatterns` in `compareCodeUnits` order, T019's
independent auditor recomputes both hashes rather than copying recorded values
and must record an explicit adequacy finding, and T020 observed the audit FAIL
against a deliberate input-order variant, retained at
`evidence/negative-cases/oracle-input-order/`. Verified on this branch: T017,
T019, T020, T021 all checked.

The discharge is a stronger precedent than the blocker it replaced. An open
blocker warns that a freeze can go wrong; a retained negative case proves the
audit catches it when it does — which is the ADR-0016 standard this feature holds
its own gates to. Citations updated accordingly, and two refinements adopted into
FR-008: the auditor recomputes rather than confirms, and must reach an adequacy
finding. An audit that confirms every hash and stops has established that the
corpus is unmodified, not that it is fit to calibrate against — an H can be
perfectly intact and still lack a label class, be too small, or carry labels
derived from evaluator output. `specs/010-*` T021 observed the FAIL for exactly
that integrity-only audit; T012a now does the same here.

Added: FR-017b, prompted by PR #98's `run-network-denied.ts`, which treated empty
stdout plus a non-zero exit as "this environment cannot deny network access" when
the sandbox had been created successfully and only the payload failed to resolve.
A broken measurement was reported as an environmental absence, in a file whose
whole purpose was to prove denial rather than infer it from absent traffic. It
survived five consecutive CI failures because the wrong branch looked like an
honest "not available here".

FR-017a already required a not-computable state with a reason code, but it
treated not-computable as one thing. It is four, and they must not be collapsed:

- `nothing-to-measure` — the subject does not exist yet (no pass shipped). The
  only class that may render ADR-0027's absence statement.
- `input-unavailable` — well-defined measurement, required input absent from the
  project (the override rate's decision log).
- `undefined-value` — the measurement ran and the quantity is mathematically
  undefined (zero probabilistic escalations, so TP + FP = 0). A finding.
- `measurement-failed` — the input should exist and could not be used. A defect
  that fails the gate.

Two collapses are forbidden explicitly. `undefined-value` and
`measurement-failed` must stay distinct, because zero probabilistic escalations
may mean the passes contribute nothing — the signal this feature exists to
surface — and collapsing it into a harness failure hides whichever is real. And a
computed zero must be representable distinctly from every not-computable state:
"measured, and clean" and "could not measure" are different facts.

FR-026 follows from it. The absence statement is now rendered from the report's
`nothing-to-measure` state rather than from a bare `if (noProbabilisticPass)`
branch, and no other class may render it. A `measurement-failed` producing "no
probabilistic pass has shipped; no figures exist" would be false and reassuring
at the same time. The separate boolean branch is what makes that substitution
possible; deriving the statement from the state is what makes it impossible.

SC-021 through SC-025 added with observed-failing-first tasks T012a, T030a,
T034a, T034b. 38 FRs, 33 SCs, 26 OBSERVE-FAIL tasks; all cross-references
resolve; five [NEEDS CLARIFICATION] markers intact. adr lint: 27 records, 0
errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): define aggregate confidence and the contradiction predicate; drop the relevance floor

The 011 session published its snapshot shapes and asked for three things it is
blocked on. All three are answerable from the rubric's own text, so none required
inventing a number.

Taking ownership of two definitions I had previously pushed away. FR-020a had
assigned the confidence and contradiction *computations* to 011 on the grounds
that they are functions of output shapes this feature must not invent. That
premise died when 011 published RubricScoreSnapshot and AdversarialSnapshot —
they are now functions over a published shape. And the split was never sound in
the other direction: a threshold calibrated against a quantity its owner does not
define is calibrating something it does not control.

FR-020b — aggregate confidence is surviving-and-cited dimensions over a fixed
denominator of 8. Grounded in the rubric's existing mechanics ("every score above
0 must cite a span", "Uncited scores are dropped by the aggregator") rather than
invented, computable by the pure kernel from RubricScoreSnapshot with no
call-out, and structural rather than self-reported per Constitution IV. The
denominator is the constant 8, never "dimensions attempted": a pass that
attempted two dimensions and cited both would otherwise score 1.0, which is
exactly backwards — that is the least confident possible run. It also keeps the
documented default coherent, since 0.7 x 8 = 5.6 means "fire when three or more
of eight are uncited or absent"; a definition that made the rubric's own default
absurd would be evidence against the definition. Score dispersion and
self-consistency are recorded as rejected, with reasons.

FR-020c — the contradiction predicate, from the rubric's two disjuncts: a present
hidden-one-way-door output bearing on d, unconditionally; or any present
adversarial output bearing on d where Pass 2's surviving score for d is >= 3. The
cut point is not invented. The rubric's shared anchors define 3 as "adequate for
the blast radius" and require every score below 3 to name the specific missing
thing, so >= 3 is the rubric's own statement that the dimension was handled — an
objection against it is a contradiction in the rubric's vocabulary. 011 evaluates
it as the trigger and records three-state evidence; this feature aggregates that
record and never re-derives it.

FR-020d — the relevance floor is withdrawn, not deferred. Verified: no relevance
scoring exists anywhere. packages/mcp/src/search/normalize.ts documents the
primitive as "No stemming, fuzzy, weighting, or ranking" and search-decisions.ts
returns "no ranking, no model, no body". A floor calibrated over a ranking
function that does not exist is a parameter whose tuning changes nothing — a
measurement that measures nothing, which is the failure ADR-0027 rescoped
ADR-0005 to prevent and which FR-017b forbids elsewhere in this same document.
Applying this feature's own rule to itself, the floor is nothing-to-measure, not
a parameter awaiting a value. novel-no-precedent is expected to stay permanently
evidence-absent; ownership of such a primitive is unassigned and not claimed
here.

FR-019 also gained a precision the published shape exposed: drift is measured on
the post-drop surviving score, with the raw score retained, because the
difference between raw drift and surviving drift distinguishes a judgment shift
from a citation-behavior shift. A model upgrade that cites less diligently moves
surviving scores without changing what it thinks, and the two demand different
responses. A model version absent from the baseline is measurement-failed, never
a quiet skip.

SC-026 through SC-029 added with observed-failing-first tasks T043b, T043c,
T043d. 41 FRs, 37 SCs, 29 OBSERVE-FAIL tasks; six [NEEDS CLARIFICATION] markers
(the sixth being relevance-primitive ownership); all cross-references resolve.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): a permanently evidence-absent trigger is valid, not a coverage failure

The 011 session asked whether the gate would reject a holdout in which
`novel-no-precedent` is `evidence-absent` for every case, since "a holdout
missing a label class" is listed as a fail-closed rejection condition. Checked:
the concern is narrower than feared but the ambiguity is real and worth closing.

"Label class" in this spec means the four outcome labels on a case
(shipped-clean / shipped-reverted / caused-incident / rejected-in-review). Those
are not triggers, so an all-absent trigger does not trip that condition. But
FR-023 also required that "every trigger's evidence state is recorded
three-state", and said nothing about whether a trigger absent in 100% of cases is
acceptable. Combined with a deliberately fail-closed gate, an implementer could
reasonably read that as suspicious and reject it.

That would be a false alarm blocking every release, on a condition that is
structurally permanent and fully expected: FR-020d already records that
`novel-no-precedent` cannot fire, because no relevance primitive exists for its
floor to threshold. A correct fail-closed posture would have been turned into a
gate that fails on the expected state.

FR-023a states both halves. Outcome label classes and triggers are different
axes, and a trigger recorded `evidence-absent` — even for every case — must not
be treated as a missing label class and must not fail the gate.

It also keeps a distinction that would otherwise be lost. Structurally absent
(the primitive does not exist at all, so no case could ever supply it —
`nothing-to-measure`) and incidentally absent (the primitive exists but this
case's reconstructed inputs could not supply it — `input-unavailable`) are
recorded distinctly. Collapsing them would hide a regression in which a working
primitive silently stops producing evidence, by making it look like the
permanent expected condition — the same substitution FR-017b forbids of the
metrics.

SC-030 added with observed-failing-first task T023a, which observes the gate
wrongly rejecting an all-absent trigger before the distinction is implemented,
then observes it passing.

42 FRs, 38 SCs, 30 OBSERVE-FAIL tasks; all cross-references resolve; six [NEEDS
CLARIFICATION] markers intact. adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): qualify "label class" everywhere, not just where FR-023a fixed it

The 011 session asked a third time whether the gate treats a permanently
`evidence-absent` trigger as valid. I had answered twice that FR-023a settles it.
Their persistence was justified and my answers were over-confident: FR-023a fixed
FR-023, and left the term unqualified in ten other places — including the two
that actually decide gate behavior.

FR-017b's table is the normative statement of what fails a release, and its
`measurement-failed` row read "... a missing label class ..." with no qualifier
and a disposition of "MUST fail the gate". An implementer arriving at that table
first — which is the natural path, since it is where failure is specified — could
reasonably read "label class" as covering trigger evidence classes, conclude that
a trigger absent in every case is a measurement failure, and fail the release.
FR-023a says the opposite, so the two would contradict, and the contradiction
would surface as a gate that blocks every release on a structurally permanent
condition.

That is exactly the failure 011 predicted, and it survived two rounds of me
saying it was already handled, because I had checked only the requirement they
happened to quote rather than every place the term decides an outcome.

All occurrences in spec.md, tasks.md, plan.md, and checklists/requirements.md are
now "outcome label class", so the term cannot be read as trigger coverage
anywhere. FR-017b additionally states the distinction inline and points to
FR-023a as governing, rather than relying on a reader to find it — a normative
table has to be correct when read in isolation, because it will be.

No requirement changed meaning; this makes the existing meaning unambiguous at
every site. 42 FRs, 38 SCs, 30 OBSERVE-FAIL tasks; all cross-references resolve;
six [NEEDS CLARIFICATION] markers intact. adr lint: 27 records, 0 errors, 0
warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): derive the not-computable class from state, never from a trigger name

Two fixes, one of them a defect I introduced in the previous commit.

First, a bug of my own making. The previous commit qualified every "label class"
as "outcome label class" with a case-sensitive regex whose negative lookbehind
did not match a capitalized "Outcome ". FR-023a's own heading became "Outcome
outcome label classes". My verification grep for the double-qualification was
also case-sensitive, so it reported clean. Caught on re-reading the requirement
rather than by the check that was supposed to catch it; the scan is now
case-insensitive.

Second, and the substantive one. The 011 session noted that their FR-018 carries
a risk structurally identical to the `if (noProbabilisticPass)` hole I found in
my own FR-026 — a renderer that branches on "no relevance primitive" rather than
deriving from the report state. Checking my side, FR-023a had the same gap: it
required structurally-absent and incidentally-absent to be recorded distinctly,
but never said the distinction must be *computed* rather than assumed.

So an implementation could write `if (trigger === 'novel-no-precedent') return
'nothing-to-measure'`. That is correct today and becomes wrong silently the day a
ranking primitive ships: the trigger keeps reporting `nothing-to-measure` while a
real primitive sits behind it producing nothing, and the transition to
`input-unavailable` — the one that says "this used to be impossible and now it is
merely failing" — never appears.

It is the same defect as FR-026's, one level down. Both are a branch on a
condition the reporter believes is permanent, and both go stale without failing.
FR-023a now requires that wherever this feature reports a `not-computable` class,
the class is a function of observed state and not of an identifier.

SC-031 and T023b added: a fixture in which a relevance primitive is present but
produces nothing must yield `input-unavailable`, not `nothing-to-measure`, and an
implementation hardcoding the class per trigger is observed failing.

42 FRs, 39 SCs, 31 OBSERVE-FAIL tasks; all cross-references resolve; six [NEEDS
CLARIFICATION] markers intact. adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): fix seven blocking defects found by a five-lens deep review

A five-lens panel (adversarial, architect, consumer, operator, privacy) read the
spec independently at 31d97c8. Adjudication merged 38 findings from 5/5 lenses:
7 blocking, 11 major defects, 14 major risks, 4 unresolved tradeoffs. The seven
blocking ones are fixed here. Two were also found independently by the 011
session's own review, which is the strongest corroboration available.

1. The gate had no activation condition, so it blocked every release forever.
FR-011 scoped failure to "a release that includes a probabilistic pass", but
FR-014 and FR-023 stated their failure conditions unconditionally, and FR-017b
made |H| < N a measurement-failed that MUST fail the gate. N deliberately does
not ship, and FR-014 says unknown is never satisfied — so no H could ever be
shown qualifying and the pipeline would fail from the moment the gate was wired.
FR-011a now states two branches: with no pass declared the holdout branch is
inert (reported, not failing) while the detector branch still runs; with a pass
declared everything is fail-closed. FR-011b states the unset-N behavior the spec
had for epsilon and not for N: inert before a pass is declared, and a
measurement-failed that blocks after, because shipping a pass against a holdout
whose adequacy nobody defined is the thing the precondition exists to prevent.

2. The detector could not work, in either direction. FR-013's second source was
the dependency graph via check:deps. Verified: it cannot serve, for two
independent reasons. Under the harness-driven architecture adrkit emits a request
and consumes a response — the model call is in the harness — so adrkit ships no
model dependency when a pass ships, ever. And check-deps.ts is an allowlist whose
own comment records that a package with no entry is "silently unconstrained and
passes check:deps no matter what it declares ... a green check that means
nothing." So an honest declaration failed every release forever, and omitting the
declaration left both sides empty, agreeing, and silent — reducing the
precondition to the voluntary self-report ADR-0027 §3 claims it is harder to
evade than. The second source is now the evaluator's committed pass surface: a
pass no caller can invoke has not shipped, and that surface is not written by the
same edit as the registry entry. FR-013a fails the gate when source 2 cannot
observe; FR-013b requires both specs to name the same signal, tracked as a
seventh clarification.

3. The ordering anchor did not exist. FR-015 compared the freeze commit against
"the first commit that produced a score", but nothing in committed state records
score production — Pass 0 persists nothing and this feature adds none. The only
artifact that could carry it is the registry the declarer authors, which is the
date field the requirement exists to reject. The anchor is now derived from the
earliest commit at which source 2 observes the pass surface, and an underivable
anchor fails closed.

4. Validity bound H but not the population actually measured. Every FR-023
precondition is a property of H as a whole; every figure discharging ADR-0027 §3
is computed over the probabilistic-marginal subpopulation, which nothing
constrained. A holdout could satisfy every precondition while the marginal subset
was empty — FR-017b classes that as undefined-value, "a finding, not a failure",
and FR-026's "figures become required" is satisfied by their presence in a
not-computable state, since FR-021 established that not-computable counts as
published. A pass could ship measured over nothing with every rule followed.
FR-023b adds per-population validity and states that a not-computable marginal
figure does not discharge the obligation once a pass is declared. This scopes
rather than contradicts FR-017b: undefined-value stays a finding everywhere it is
informative, and becomes a failure only for the marginal figure once a pass ships.

5. FR-020a rested on a false premise. It justified taking the confidence and
contradiction definitions on the grounds that specs/011-* "has published"
RubricScoreSnapshot and AdversarialSnapshot. No such directory exists at this
revision and neither identifier occurs outside this feature's own files; an
out-of-tree spec on another branch is not a citable contract under Principle I.
The ownership decision stands on the argument that does not need that premise — a
threshold calibrated against a quantity its owner does not define is calibrating
something it does not control — and this feature now defines those shapes at
field level in contracts/metric-definitions.md as the frozen input contract, with
011 conforming.

6. The privacy obligation was asserted, never enforced. "Raw material stays
scratch-only" had no mechanism: no rule that scratch lives outside the clone, no
teardown, no closeout confirmation — all three of which specs/008, the precedent
this spec names, actually has. The only content inspection was T045, in the final
phase, ~30 tasks after the corpus is irreversibly committed, and it was the one
check with no observed-failing fixture while FR-030 omitted FR-024 from its
enumeration. FR-024 now carries the three mechanisms, T010d gates the freeze
rather than closing the feature, and FR-030 includes FR-024.

7. The prohibited-content list named bodies, not channels. Four tracked artifacts
could carry sensitive content without being a "proposal body": reconstructed
snapshots (the bundle admits principal ids, team rosters, codeowners, and
humanRequested.requester), free-prose exclusion reasons, the audit record, and
the outcome labels themselves, which are attributions about identifiable commits.
FR-024b constrains each. FR-024c makes external cases inadmissible until
clarification 5 resolves, giving that marker the mechanism the other six had.

Two architect majors folded in. Calibration reason codes must form their own
disjoint union, never appended to REASON_CODES — that array is the frozen Pass 0
contract vocabulary, ReasonCode is derived from it and exported from the
published package, and a calibration code can never be emitted by a Pass 0 run.
And the plan now names scripts/audit-oracle-freeze.ts and check-freeze-hashes.ts:
feature 010 discharged the freeze procedure as landed executable code, which is
why its audit FAIL was observable at all, so T012's audit must be executable or
T012a has no mechanism.

SC-032 through SC-040 added with observed-failing-first tasks T010d, T021, T022,
T023c, T023d, T026, T040. 49 FRs, 48 SCs, 35 OBSERVE-FAIL tasks, seven [NEEDS
CLARIFICATION] markers. All cross-references and links resolve. adr lint: 27
records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): fix eight defects from the Copilot review

Copilot's review posted 14 inline comments. Two were already fixed by the
deep-review commit (the check:deps detector, the first-score anchor) and four
were the clarification-count drift, now 7 everywhere. Eight are new and genuine.

The manifest digest was impossible to reproduce. FR-006 required "a sha256 over
the manifest itself" — but writing that digest into the manifest changes the
bytes it covers, so no independent implementation could ever recompute it. The
digest is now over a canonical projection with the self-digest field omitted, or
stored only in the evidence index, with T006 recording which. This also forces
the canonical form to be named rather than assumed, which the architect lens had
flagged separately.

FR-023's qualification list omitted the audit entirely. FR-008 and SC-003 require
an adequate PASS committed before derivation, but the gate implemented FR-023's
list — so a release could satisfy every precondition with no audit at all, or
with a recorded FAIL. The ordering gate is this feature's centerpiece and the
gate was enforcing the freeze's arithmetic without its discipline. FR-023 now
requires the audit to exist, to record PASS with an adequacy finding, and to be
an ancestor of the first derivation commit.

T013 accepted any explicit verdict, including FAIL, and never tested
audit-commit ancestry — so T014 could go green with the hard ordering gate
unsatisfied. It now asserts derivation is rejected when the audit is absent,
non-PASS, inadequate, or not committed first.

T033 and T034 defined two different return contracts for the same
empty-denominator case: "returns absent" versus "returns not-computable". There
is now one discriminated representation — `not-computable` carrying the
`undefined-value` class — and "absent" is how that state renders, never a second
contract. The negative case also rejects a bare `absent` sentinel with no class.

The determinism negative case did not discriminate. T015 proposed observing
failure "against a localeCompare sort", but two runs under the same locale are
identical, so the fixture would have passed and the ADR-0016 observation would
have observed nothing. It now requires two known-different ICU locales or two
deliberately different injected comparators.

Phase 6 could not be satisfied in its stated order. It was blocked on T025 alone,
but FR-026 requires the absence statement to be rendered from the report's
`nothing-to-measure` state, which does not exist until T040-T042 in Phase 7.
Sequencing on the registry alone would have left the state-driven renderer
unimplemented at exactly the moment T030a/T031 need it — and invited the boolean
branch FR-026 forbids. Phase 6 now depends on both, and the phase graph and
dependency prose say so.

The plan then described that enforcement as "a function of the registry", which
contradicts FR-026 in the same direction. Corrected: the registry is an input to
report construction, the report's state drives the statement, and enforcement
consumes the report.

The plan's summary also overstated the dual-figure rule as applying to all six
metrics. ADR-0027 §3 and FR-016 split precision, recall, and FNR only; drift,
disagreement, and override rate are not split, and inventing variants of them
would be unsupported.

49 FRs, 48 SCs, 35 OBSERVE-FAIL tasks, seven [NEEDS CLARIFICATION] markers. All
cross-references resolve. adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): close three holes in definitions 011 consumes, and four stale references

Non-blocking findings from the same five-lens panel. Three are real holes in
metric definitions this feature owns and specs/011-* consumes, so leaving them
would have shipped a known-wrong contract.

FR-020c had no path to `condition-unmet`. The predicate said an explicitly absent
adversarial output "contributes nothing; it is neither a contradiction nor
evidence of agreement", and that anything without comparable output is
`evidence-absent`. Together those left the ordinary agreement case — Pass 3 ran
and raised no objection — with no state to occupy. The disagreement denominator
would have collapsed to the cases where Pass 3 objected, driving the rate toward
1.0 by construction and making FR-020's zero-is-a-defect-signal unreachable. The
fix is the distinction between "produced no output" and "produced output
containing nothing": the first is `evidence-absent`, the second is
`condition-unmet` and is what the denominator is mostly made of.

FR-020b had no boundary against `evidence-absent`. Making absent dimensions lower
confidence is correct for routing — a thin result should escalate — but for
calibration accounting it meant a pass that produced almost nothing would fire
`low-confidence`, be recorded `condition-met`, and count as a measured
escalation. A recall of 1.0 could be manufactured by a pass that barely ran. Now:
no comparable result at all is `evidence-absent`; a result covering at least one
dimension is computed normally; and escalations whose sole driver is
`low-confidence` from structurally-absent input are counted as a separately
reported subpopulation.

FR-017b claimed four disjoint classes while the definitions overlapped on a state
the spec itself predicts — no pass declared AND |H| < N is where the repository
is today — and SC-021 requires exactly one class. Disjointness now comes from a
stated total precedence rather than from inspection: measurement-failed first,
because a broken measurement must never be reported as any kind of absence;
nothing-to-measure before input-unavailable, because a subject that does not
exist cannot have a missing input. Today's state resolves to nothing-to-measure,
which is what makes FR-011a's holdout branch inert. The class attaches to
observed state, not to the reason code, since one state can satisfy several
codes.

Four stale references, three of which I introduced in earlier commits.

The banner opening tasks.md — the most prominent statement of this feature's
load-bearing constraint — named T010 as the ordering gate three times and
prescribed a T007 -> T010 recovery. There is no T010; the audit is T012 and the
recovery is T010b -> T012. The banner asserting a task that does not exist is a
poor advertisement for a spec whose subject is verifiable ordering.

The plan's risk table still assigned the relevance floor to this feature as a
calibration parameter after FR-020d withdrew it. The SC-029 traceability row
pointed at T043 rather than T043a. And tasks.md listed the four contracts/
documents as prerequisites when they are authored by T004 and T007-T009 in the
same list.

49 FRs, 48 SCs, seven [NEEDS CLARIFICATION] markers. All cross-references
resolve. adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): state what the pass-surface detector does not prove

The 011 session made a point about Q8 that survives my having already answered
it: "what it can't stay is a gate that reads as mechanical and isn't." Re-read
FR-013 against that and they are right — the replacement detector was written
with more confidence than it earns.

The pass-surface signal is genuinely stronger than the dependency check it
replaced. That one was empty by construction and could never fire; this one
detects an undeclared pass and rejects a declaration with no surface, neither
direction depending on the declarer's honesty.

But it is not airtight, and the spec did not say so. An implementer who places
the request-builder and response-parser outside every location the gate inspects
defeats source 2 entirely. That makes the signal a heuristic over committed state
whose coverage is exactly the set of locations inspected — not a proof that no
pass shipped.

FR-013c states both halves and makes two requirements of the gap. The
precondition-gate contract must enumerate the locations that constitute a pass
surface, and a declared pass whose surface is in none of them is a
measurement-failed under FR-013a rather than a gap silently tolerated — source 2
was structurally unable to observe what the registry named, which is exactly the
condition FR-013a already covers. And any artifact describing this gate must
state that its coverage is bounded by that enumeration.

The reason is the feature's own subject matter turned on itself: a gate that
reads as mechanical and is not is worse than an acknowledged self-report, because
it buys confidence it has not earned. That is the same defect as a calibration
report that publishes precision it did not measure, one level up. If 011 and this
feature cannot agree an enumeration that holds, the correct resolution is not a
weaker detector described in strong terms — it is to declare the registry
self-declared and say plainly that the cross-check is not evidence of
pass-shipping. Weaker and honest beats stronger-sounding and unearned.

SC-034a added, with T009 owning the enumeration. 52 FRs, 49 SCs, seven [NEEDS
CLARIFICATION] markers. All cross-references resolve. adr lint: 27 records, 0
errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): close Q8 — the emitted report field is the detector, not the module surface

The 011 session raised the detector defect, declined to choose a replacement
because the gate is this feature's, and has now handed the decision back. Closing
it rather than leaving it open: an interface question both parties have answered
is settled, and an open marker would misreport the spec's state.

The signal is enumerated in two places, with the emitted report primary: a
pass-result / PassAbsence field on the report type, and secondarily the exported
entry-point surface reachable from packages/evaluator/src/index.ts. Either firing
is detection.

The report field is primary for two reasons, the second decisive.

It is enumerable. One field on one committed type has a boundary that can be
written down and checked. "Reachable from index.ts" is a set of module locations
that refactors move, so its enumeration would need continuous maintenance to stay
true — and a stale enumeration is a detector that silently narrows, which is the
failure this whole requirement was rewritten to remove.

More importantly it makes the detector and the metrics read the same artifact. A
pass that hides from the detector by not reporting its results also starves the
metrics: its figures become evidence-absent or measurement-failed, which under
FR-023b fails the gate once a pass is declared. The evasion path therefore leads
to a failure by another route rather than to silence. An unenumerated module
location produces silence instead, and silence is what a gate must never produce
about the thing it exists to detect.

FR-013c's coverage bound stands unchanged. This is a better detector, not an
airtight one, and the requirement still states what it does not prove.

Also recorded in FR-013c the general principle this feature has now hit three
times, which 011 named precisely: the scope of a verification is part of its
result, not a detail of how it was run. A check reporting clean over a scope that
never covered the case reads as corroboration and is not — the same structural
error as counting one corroborated finding twice, or treating an absent snapshot
as a true negative. A gate reporting no disagreement when it never held a signal
capable of disagreeing is that error wearing a release-blocking badge.

Markers go from seven to six, and the closure is recorded under a new "Closed
during scoping" heading so the count's change is legible rather than looking like
a dropped item.

52 FRs, 49 SCs, six [NEEDS CLARIFICATION] markers. All cross-references resolve.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): propagate the detector replacement into the prose that still argued for the old one

Self-check prompted by the 011 session, which grepped its own spec for "floor"
after my confirmation that the relevance floor was withdrawn, and found two of
its own lines still describing it as a thing that exists. I ran the equivalent
scan on the detector replacement, having twice now been caught fixing the one
place I quoted and leaving the others.

Four stale locations, all describing the withdrawn dependency-graph detector as
though it were the design.

The worst is plan.md's "Two sources that must agree" section — the design
rationale, the place a reader goes for *why*. It still argued for cross-checking
against `bun run check:deps`, an argument FR-013 had already withdrawn as
unworkable. A reader following the reasoning would have been persuaded of the
superseded design by the document that supersedes it. Rewritten to state the
withdrawal and its two reasons, and to name the replacement — kept rather than
deleted, because someone who wonders why the obvious check is unused should find
the answer instead of proposing it again.

US3 acceptance scenario 3 still specified the gate's behavior in terms of a
dependency the registry does not declare. Acceptance scenarios are read early and
treated as normative-adjacent, so this was the same failure as the FR-017b table:
a location that decides an outcome, left describing a mechanism that no longer
exists. Now stated over the pass surface, with FR-013a's structurally-unable-to-
observe case added.

The plan's summary and its Project Structure comment carried the same stale
description in shorter form.

Remaining mentions of `check:deps` and the dependency graph are deliberate and
were left: FR-013's own withdrawal explanation, SC-034's "not the dependency
graph", SC-014's unrelated purity check, and the tasks that keep `check:deps`
green as a pre-existing gate.

No requirement changed meaning. This makes the existing meaning consistent
wherever the artifact states it. 52 FRs, 49 SCs, six [NEEDS CLARIFICATION]
markers, all cross-references resolve. adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): find the stale detector references that describe it by behavior, not by name

Third sweep for the same class, and the one that found the rest. The coordinating
session asked whether the FR-013 rewrite had left more than one downstream
reference. It had left four more than my previous scan caught, and the reason is
worth recording because my last commit message stated the wrong lesson.

That commit said: scan for the mechanism's vocabulary, not the requirement's ID.
True, and still insufficient. These four never used the mechanism's name. They
described what it did:

- an Edge Case: the cross-check "fails in the 'registry entry with no dependency'
  direction"
- the PassesRegistry entity: "cross-checked against dependency evidence"
- SC-006, a success criterion: "registry/dependency-graph disagreement"
- the plan's risk table: "fails on the dependency the registry does not declare"

Grepping `check:deps` and `dependency graph` finds none of the first, second, or
fourth. The complete rule is to search by what the mechanism *does* — the shapes
of its inputs and outcomes — because a description can be entirely stale without
containing a single word from the thing it describes.

SC-006 is the one that mattered most: a success criterion enumerating the four
deliberate violations the gate must be observed rejecting, naming a disagreement
that can no longer occur. An implementer building fixtures from it would have
constructed a negative case that cannot fail, which is the same defect Copilot
caught in the determinism fixture and the same one this feature exists to
prevent — a check that observes nothing while appearing to observe.

The acceptance scenario the coordinator flagged was already fixed in 3b6ebd3;
their report was reading an earlier revision. The instruction to look for more
was correct anyway.

One mention of "a registry entry with no corresponding dependency" remains and is
deliberate: it is inside FR-013's own explanation of how the withdrawn design
inverted, where the stale phrasing is the subject rather than the claim.

52 FRs, 49 SCs, six [NEEDS CLARIFICATION] markers. All cross-references resolve.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): state the delta test once, publish agreement, sub-class measurement-failed

Six items from the coordinating session, four of which turn on one rule that was
missing.

The durable one first. ADR-0005 action item 4 reads "Treat rubric changes as
ADRs, with calibration deltas attached", and the qualifier is the test rather
than decoration: if a change has a calibration delta to attach, it needs a
record; if there is no "before" to delta against, it belongs in a spec, with the
derivation shown. My spec had the rule for changing a default and asserted the
ADR requirement for label classes, but never stated the general test — so the
boundary depended on which requirement a reader hit first, which is not a rule,
it is an accident of reading order that the next feature inherits.

FR-018a states it once. FR-024a now cites it rather than restating the ADR
requirement independently, so the label-class gap needing a record and the
confidence definition not needing one follow from a single test instead of
looking like inconsistent treatment.

Under that test the confidence definition and the >= 3 predicate are legitimate
in a spec, and both now say so explicitly. Rubric line 153 names "aggregate
confidence" and a 0.7 default and never defines the quantity, so there is no
prior behavior to delta against. The evidence is that 0.7 x 8 = 5.6 fires at
three or more uncited of eight — the definition recovers the documented default
rather than reinterpreting it. Same for >= 3, which is the published anchor. Both
requirements now state that they complete an undefined term rather than change a
defined one, and both show their derivation inline, which are FR-018a's two
conditions.

Agreement is published alongside disagreement. Rubric line 184 says "Publish
inter-pass agreement rates" and line 135 says "Disagreement between passes is
signal, not noise" — the rubric asks for both, they share a denominator, and
publishing both costs nothing. My spec published only disagreement, which quietly
substituted one term for the other. The substitution was the defect, not the
emphasis. No record is needed because nothing about the rubric changes:
agreement = 1 - disagreement over the same denominator. Disagreement keeps the
defect signal because that is where the rubric locates the failure, and an
agreement rate of 1.0 now carries it too.

measurement-failed is sub-classed. It was the only class that fails the gate and
it bundled causes demanding opposite responses: environmental (unreadable
holdout, incomplete object graph — retry), artifact-defect (hash mismatch, model
version absent from the drift baseline — do not retry, since a retry that "fixes"
it has destroyed the evidence), and corpus-inadequate (|H| < N, missing outcome
label class — neither retry nor repair). The gate's behavior is identical across
all three; the diagnosis is not. This is the partition habit this feature has
applied three times already, unapplied a fourth.

The no-evaluation:-block premise is now a standing check (FR-009a, T002a) rather
than a one-time confirmation at T002. The schema permits the block and nothing
forbids writing it, so a later feature can break the corpus premise silently —
nothing fails, the corpus simply becomes partly fitted to the evaluator it
grades. The repository's convention for load-bearing invariants is a standing
check, and this one had a one-shot.

SC-041 through SC-044 with observed-failing tasks T002a, T034c, T037b. 52 FRs, 53
SCs, 38 OBSERVE-FAIL tasks, six markers. All cross-references resolve. adr lint:
27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): invert the detector ordering — the exported surface is primary, not the report field

specs/011-* adopted the pass-surface signal and, in doing so, chose the opposite
ordering to the one this spec had shipped. That was a live divergence of exactly
the kind FR-013b exists to prevent: both specs naming overlapping signals with
different primaries is not "the same detector".

Their reasons defeat mine, and one of them is a defect rather than a preference.

My wording was "a pass-result / PassAbsence field on the emitted report type".
Read as intended — the field declared in types.ts — it is committed state. Read
as written, "emitted report" invites the emitted output, and observing a value
requires running the evaluator over some input. FR-012 confines this gate to
committed state at the release commit, so that reading would put the gate in
violation of its own neighbouring requirement. An emitted absence is also
ambiguous between "no pass shipped" and "this run produced none". It is the same
reason FR-015's anchor keys on commit ancestry rather than a date field, which I
had already argued three requirements earlier and did not apply here.

The second reason is about strength. A type can carry a field before any pass
exists to populate it, so the field's presence is weaker evidence than a
request-builder and parser a caller can actually reach. A pass no caller can
invoke has not shipped; a field is not subject to that constraint.

The field is kept as the second enumerated location rather than dropped, because
the original argument for it survives as a secondary benefit: a pass that hides
from the detector by never reporting results also starves the metrics, whose
figures become evidence-absent or measurement-failed and fail the gate under
FR-023b once a pass is declared. That turns an evasion into a failure by another
route rather than into silence — a real property, just not one that outweighs
being readable without execution.

FR-013b's decision record now says the ordering was corrected once rather than
implying it was right the first time, since a record of a decision that hides its
own revision is the thing this feature spends fifty requirements arguing against.
SC-034 and the plan's design section carry the same ordering.

Both specs now name the same detector with the same ordering. 52 FRs, 53 SCs, six
[NEEDS CLARIFICATION] markers. All cross-references resolve. adr lint: 27
records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): actually apply the four rubric items 786afb7 claimed to have applied

786afb7's commit message described six changes. Only two of them landed. The
first script in that commit asserted its way through five edits and aborted on
the sixth; Python writes the file at the end, so the abort discarded all five
in-memory edits silently. I re-ran only the failing piece, then a third script for
the rest, and never re-verified the aborted ones. The commit message asserted
work that was not in the tree.

Caught only because the coordinating session re-sent its ruling and I checked
each item against the file instead of replying "already done" — which was my
first instinct, and would have been wrong.

The failure is this workstream's recurring shape, this time authored by me rather
than found by me: a verification whose scope did not cover the claim it licensed.
The asserts proved each *substring matched*. They proved nothing about whether
the file was written. I read a green run as a green result.

Now applied, each write verified independently against the file rather than by
the script's exit status:

Agreement is published alongside disagreement. Rubric line 184 asks for agreement
and line 135 calls disagreement signal; the rubric uses both framings, they share
a denominator (agreement = 1 - disagreement), so publishing both costs nothing.
Publishing only one was a silent substitution, and the substitution was the
defect rather than the emphasis. No record needed, because nothing about the
rubric changes.

FR-020b and FR-020c each now state that they complete an undefined term rather
than change a defined one, with the derivation inline, per FR-018a's two
conditions. Line 153 names "aggregate confidence" and a 0.7 default and never
defines the quantity; the rubric states the escalation condition in prose and
never gives it a cut point, and pass-disagreement has never been implementable.
Neither has a prior behavior to attach a calibration delta to. Both values still
route through ratification once calibration data exists, as epsilon does — the
condition attached to the ruling and missing from my earlier draft.

FR-024a now cites FR-018a's delta test instead of asserting the ADR requirement
for its own case, so the label-class gap needing a record and the confidence
definition not needing one follow from one rule rather than reading as
inconsistent treatment.

52 FRs, 53 SCs, six [NEEDS CLARIFICATION] markers. All cross-references resolve.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): check that asserted relationships hold, not just that their IDs resolve

Prompted by a factual dispute with the coordinating session over whether four
items had landed in 786afb7. Settled by exact-string counts against the committed
blobs: they had not. But the interesting part is why the coordinator's
verification said otherwise, because it exposes a gap in mine.

Their pattern "completing an undefined" matched FR-018a's own body — "Completing
an undefined term has no prior behavior" — rather than FR-020b and FR-020c citing
it. FR-018a had landed; the citations had not. A pattern loose enough to match a
rule's definition cannot answer whether other requirements cite that rule.

The sharper instance is their FR-024a/FR-018a check, which returned a match on
SC-044:

    FR-024a cites the same test (FR-018a) for the opposite disposition.

SC-044 had landed. FR-024a did not cite FR-018a. So a success criterion asserted
a property the artifact did not have, and the verification matched the assertion
rather than the property. Every ID in that sentence resolved, so my own
cross-reference check — which is what I run before every commit — was green
throughout.

T051a adds the missing check. Where a requirement or success criterion asserts a
relationship rather than a fact — "FR-A cites FR-B", "both specs name the same
detector", "the enumeration covers X" — the relationship must be verified to hold
in the text. Cross-reference resolution proves the IDs exist. It cannot prove the
sentence is true, and this feature has now produced one instance where it wasn't.

A spec asserting a property of itself that it does not have is the defect this
feature exists to prevent, turned inward. ADR-0027's evidence table read "Met" the
same way.

Also corrected T051, which still said "five" [NEEDS CLARIFICATION] items after the
count moved to six when FR-013b's marker was closed — the same count drift Copilot
caught earlier, regressed by my own edit and missed because I verified the marker
count in spec.md and not its restatement in the task that checks it.

52 FRs, 53 SCs, 39 OBSERVE-FAIL tasks, six markers. All cross-references resolve;
SC-044's asserted relationship now holds. adr lint: 27 records, 0 errors, 0
warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): enumerate the pass surface by committed type name, not module reachability

specs/011-* argued to keep the pass surface rather than switch to PassAbsence, and
answered my enumerability objection better than switching would have.

My objection stood: "reachable from packages/evaluator/src/index.ts" is a
transitive property over a module graph, so enumerating it is a judgment call —
precisely what FR-013c says a bounded detector must not rest on. Their answer is
that the fix is not a different source but a different enumeration basis. One
exported type name per pass role — the request-builder's input type and the
response-parser's result type — is precisely nameable, greppable, and closed in a
way "every module reachable from index.ts" is not.

That keeps the committed-state property that made the surface the right source in
the first place, and supplies the enumerability that made the report field look
attractive. Their diagnosis of why it looked attractive is the part worth
recording: the field only ever seemed easier to enumerate because it is a field in
a type, not because it is in output. The enumerability was never a property of
being emitted.

Both locations stay enumerated with either firing as detection, so an evader must
defeat two independent locations rather than one — their belt-and-braces option,
taken because two is strictly harder than one and costs nothing.

SC-034a, FR-013c's enumeration clause, T009, and the plan's design section all
carry the type-name basis. SC-034a needed it specifically: it asserts the
enumeration requirement, and under T051a's assertion-vs-property rule — added one
commit ago from the SC-044 incident — an SC asserting a property must carry it.
The check caught this immediately on the first edit after being written, which is
about the best evidence available that it was worth adding.

52 FRs, 53 SCs, 39 OBSERVE-FAIL tasks, six markers. All cross-references resolve.
adr lint: 27 records, 0 errors, 0 warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): drop the primary/secondary rank between the two detector locations

The rank between the entry-point types and the report-field variant has now been
inverted twice: this feature made the field primary, specs/011-* adopted the
surface, this feature inverted to match, and specs/011-* inverted back once it was
clear the field lives on the committed type rather than in emitted output. Both
inversions were argued well and both arguments were correct about what they
addressed.

Two careful readers inverting a distinction twice is a defect in the framing
rather than a disagreement about the answer, so I checked what actually depended
on the rank. Nothing did. Either firing detects; the gate's behavior, FR-015's
anchor (the earliest commit at which either location is observed), and the
cross-check are identical under either ordering. The rank was decorative, and it
had already produced two live divergences between specs that FR-013b requires to
agree — plus one inside this spec, where the closed-decision section still said
"field primary" after the requirement said "surface primary".

There is now no primary. Two co-equal enumerated locations, either firing is
detection, with both necessity arguments recorded because each covers what the
other does not. Entry-point types: a pass that ships must be invocable, so it
cannot omit an input and result type without not shipping. Report-field variant:
the detector and the metrics then read the same artifact, so a pass that hides by
never reporting results also starves the metrics, whose figures become
evidence-absent or measurement-failed and fail the gate under FR-023b once a pass
is declared — evasion leads to a failure by another route rather than to silence.
Its weakness is that a field can be added speculatively, before any pass exists to
populate it, which the entry-point types do not share.

That coupling is the argument neither ordering round reached, and it is
structural rather than a stronger heuristic: a location with no coupling produces
exactly nothing when evaded, and nothing is what a gate must never produce about
the thing it exists to detect. Enumerating both means an evader must defeat two
independent locations and each location's weakness is covered by the other.

Enumeration stays by committed type name for both, since module reachability is a
transitive graph property that cannot be enumerated without judgment.

52 FRs, 53 SCs, 39 OBSERVE-FAIL tasks, six markers. All cross-references resolve
and SC-034's asserted property holds (T051a). adr lint: 27 records, 0 errors, 0
warnings.

Signed-off-by: Mark Beacom <m@beacom.dev>

* docs(specs): enforce the withdrawn-design scan this feature was bitten by twice

T051b makes a check of what had been advice. When a design is withdrawn or
replaced, scan for the mechanism's own vocabulary — and for descriptions of its
behavior, which contain none of its words — never for the requirement's ID.
Reference integrity passes cleanly on this class while the prose disagrees, which
is precisely how it escaped.

This is not a rule invented from a principle. It is the check this feature's own
history says it needs, and the history is two misses in a row.

The first left plan.md's design-rationale section still arguing for the withdrawn
dependency-graph detector — a reader following the reasoning would have been
persuaded of the superseded design by the document that supersedes it. I fixed
four references there and stated the lesson as "scan for the mechanism's
vocabulary, not the requirement's ID."

That lesson was true and still insufficient, which the second miss proved: four
further references described the mechanism by what it did rather than by its name,
and three contained no word the first scan searched for. The worst was SC-006,
enumerating a deliberate violation the gate must be observed rejecting — a
disagreement that could no longer occur. An implementer building fixtures from it
would have constructed a negative case that cannot fail, which is the defect this
whole feature exists to prevent.

Two rules travel with the sca…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants