Skip to content

feat(catalog): feature 010 Phase G — clean clone, proved network denial, clause-8 gate (T093–T100) - #98

Draft
mbeacom wants to merge 17 commits into
mainfrom
mbeacom-phase-g-clean-clone-and-close-out
Draft

feat(catalog): feature 010 Phase G — clean clone, proved network denial, clause-8 gate (T093–T100)#98
mbeacom wants to merge 17 commits into
mainfrom
mbeacom-phase-g-clean-clone-and-close-out

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 6, 2026

Copy link
Copy Markdown
Owner

The final phase: clean clone, proved network denial, the ADR-0020 clause-8 CI gate, and the honesty close-out. Also closes the four tasks earlier phases honestly left unchecked.

101/101 tasks. bun test 2146 pass / 0 fail. Typecheck, lint, check:deps, check:freeze-hashes, adr lint, and compare-accept-corpus (PASS, 0 FP / 0 FN) all green. Frozen expectations, accept-corpus freeze, vendored corpus, ADRs, and specs/009-* all untouched.

The four carried-over tasks — all closed genuinely, none forced

T058 / T062. The corrected wording (five steps, three reasons) matches the implementation. T062 required asserting the five-step ordering, so the hostile case was built: steps 2↔3 swapped while the diagnostics record still reported the old ordering, so every assertion reading it still passed. 16 of 17 tests passed — one caught it, via a YAML mapping whose ToString is '[object Object]' and throws in JSON.parse. Because that rested on one incidental fixture in a differently-named block, an explicitly named ordering assertion was added.

T078. Closed on the maintainer decision, and made executable rather than recorded in prose: canonicalize yields allRefs of exactly [canonicalId], so two descriptors' refs can only intersect when their canonical ids are equal — and duplicate-canonical-id fires first. Observed failing by giving allRefs a second member; 21 of 22 tests still passed, so the assertion discriminates the construction rather than catching incidental breakage.

T086. Limb 2 now asserts Phase F's recorded outcome (PASS, 24 expected / 25 envelope entities, 0/0) and pins the citation to the live pass.

Two checks that failed their own honesty test first

The T086 citation pin. Written as a bare toContain('compareAcceptCorpus(REPO_ROOT)'), it passed against a harness whose SC-011 block had been gutted — the same call appears in other blocks of that file. Had it only ever been run against a healthy harness it would have been recorded as coverage and been worth nothing. It now slices the SC-011 block out and counts live calls within it.

T100's honesty gate punished the most honest documentation. The first version flagged three passages that already exist in this repo verbatim, including Nothing here is reference-verified (rung 2). Fixing it took three iterations: sentence scope, then paragraph scope (hard-wrapped markdown puts the wrap between nothing and what it negates), then quoted-material handling — because the negative-case READMEs must quote their own violating text, and one quotes the observed output of these very rules firing.

Independently verified by the coordinating session, both directions: appending a real claim ("This package is reference-verified (rung 2) and externally validated") fails the gate, 34 pass / 1 fail. Restoring and appending an honest denial with identical vocabulary ("Nothing produced by this feature is reference-verified (rung 2)…") stays green at 35 pass.

FR-050's second half holds in a weaker form — recorded, not papered over

Every CI step after bun install was wired through a denial wrapper, then the clean-clone verification was actually performed against a real fresh clone. bun test failed 3 tests with EADDRINUSE.

All three are Bun.serve calls in the two files whose job is to prove network denial: their two-sided control needs a loopback listener that a total-denial sandbox correctly refuses. Wrapping the suite denies the control that establishes the denial. Skipping was rejected — §5 makes fail-closed an environment constraint, and sc-016.test.ts asserts the suite doesn't skip.

So bun test is unwrapped, and what actually holds is: no step requires network beyond the install, and every step that can run under a proved denial does. This was only found because the check was performed rather than assumed.

Five defects an adversarial review found that none of the phase's own checks caught

Recorded separately from the four found by observation. The worst:

commandFromArgv scanned the whole argv for --, but Bun strips only the first one — so the CI step -- bun run release:pack -- --skip-build --skip-smoke-install reduced to ['--skip-build','--skip-smoke-install']. release:pack never ran and the tarball verification silently didn't happen.

Plus three silent-absence holes in the clause-8 gate itself (absent/empty recomputedFrozenHashes iterated zero times and passed; an empty adequacy finding was "not literally FAIL"; corpusRef missing from both sides compared undefined !== undefined and "agreed"), and one self-satisfying assertion that searched its own source for a needle present in the assertion line.

All five are one pattern: a check that doesn't run is indistinguishable from a check that passes. All are now covered cases, and the release:pack line is verified reaching release-pack.ts and exiting 0 under denial.

Honesty posture

ADR-0014 rung 1 only — not reference-verified, not externally validated, rung 3 reported absent. No release scheduled, implied, or prepared: both packages at 0.0.0, absent from RELEASE_PACKAGES, and check:clause8 fails the build if that changes (observed).

ADR-0012 gate 3's outcome is recorded as observed, never claimed in advance — the close-out explicitly does not declare it closed, since that is ADR-0012's to do. Gate 4 is recorded unmet and not yet testable: never passed, never failed, with the [NEEDS CLARIFICATION] carried rather than guessed.

Two open items, reported rather than closed

  • The Linux unshare candidates are unit-tested but have not been observed succeeding on a Linux host (this ran on macOS). If neither proves in CI the job fails closed, visibly.
  • T096 has no negative-case directory — its failing observation is embedded in the suite as the tamper test. Stated so nobody counting directories concludes one was lost.

Copilot AI balanced review requested due to automatic review settings August 6, 2026 12:08

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

Completes feature 010 Phase G with offline CI enforcement, clause-8 validation, cross-package checks, and auditable close-out evidence.

Changes:

  • Adds proved network-denial and clean-clone CI checks.
  • Adds clause-8, heuristic, cross-package, and evidence-register guards.
  • Records negative cases and closes Phase G tasks.

Reviewed changes

Copilot reviewed 80 out of 80 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
.github/workflows/ci.yml Adds offline Phase G CI gates.
package.json Registers new check commands.
scripts/check-clean-clone.ts Verifies both packages are present.
scripts/run-network-denied.ts Runs commands under proved denial.
scripts/run-network-denied.test.ts Tests denial runner behavior.
scripts/cross-package-envelope.test.ts Tests generator-consumer round trip.
scripts/check-no-spike-heuristics.ts Detects prohibited comparison heuristics.
scripts/check-no-spike-heuristics.test.ts Tests heuristic detection.
scripts/check-clause8-gate.ts Implements clause-8 gate.
scripts/check-clause8-gate.test.ts Tests clause-8 failures.
scripts/check-observed-failing-register.test.ts Validates evidence register.
scripts/check-honesty-close-out.test.ts Enforces close-out claims.
packages/adapters/catalog-backstage/test/offline-run.test.ts Proves offline generator execution.
packages/adapters/catalog-backstage/test/offline-run-driver.ts Drives sandboxed generation.
packages/adapters/catalog-backstage/test/fixtures/net-probe.mjs Supplies denial probe.
packages/adapters/catalog-backstage/test/sc-003-all-triggers.test.ts Asserts unreachable trigger construction.
packages/adapters/catalog-backstage/test/sc-006.test.ts Tests annotation-step ordering.
packages/adapters/catalog-backstage/test/sc-009.test.ts Closes frozen-corpus limb.
packages/adapters/catalog-backstage/test/sc-016.test.ts Distinguishes denial from absence.
specs/010-catalog-backstage/tasks.md Marks Phase G tasks complete.
specs/010-catalog-backstage/evidence/honesty-close-out.md Records final claims posture.
specs/010-catalog-backstage/evidence/observed-failing-register.md Registers failure evidence.
specs/010-catalog-backstage/evidence/negative-cases/triggers/README.md Documents trigger case 6.
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-6-allrefs-gains-a-second-member.patch Retains trigger mutation.
specs/010-catalog-backstage/evidence/negative-cases/triggers/case-6-allrefs-gains-a-second-member.observed.txt Captures trigger failure.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/README.md Documents heuristic cases.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/restored.observed.txt Captures restored pass.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/case-1-option-b-descriptor-parent.patch Retains option-B mutation.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/case-1-option-b-descriptor-parent.observed.txt Captures option-B failure.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/case-2-option-c-repository-root.patch Retains option-C mutation.
specs/010-catalog-backstage/evidence/negative-cases/spike-heuristic/case-2-option-c-repository-root.observed.txt Captures option-C failure.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/README.md Documents SC-016 cases.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/restored.observed.txt Captures restored SC-016 pass.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/case-1-one-sided-control.patch Retains one-sided-control mutation.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/case-1-one-sided-control.observed.txt Captures control failure.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/case-2-absence-as-evidence.patch Retains absence-evidence mutation.
specs/010-catalog-backstage/evidence/negative-cases/sc-016-denial-not-absence/case-2-absence-as-evidence.observed.txt Captures absence-evidence failure.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/README.md Documents corpus-limb cases.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/restored.observed.txt Captures restored limb pass.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/case-1-recorded-verdict-flipped.patch Retains verdict mutation.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/case-1-recorded-verdict-flipped.observed.txt Captures verdict failure.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/case-2-harness-gutted.patch Retains gutted-harness mutation.
specs/010-catalog-backstage/evidence/negative-cases/sc-009-limb-2/case-2-harness-gutted.observed.txt Captures harness failure.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/README.md Documents register cases.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/restored.observed.txt Captures restored register pass.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/case-1-a-case-with-no-row.patch Retains missing-row mutation.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/case-1-a-case-with-no-row.observed.txt Captures missing-row failure.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/case-2-a-row-with-no-case.patch Retains phantom-row mutation.
specs/010-catalog-backstage/evidence/negative-cases/observed-failing-register/case-2-a-row-with-no-case.observed.txt Captures phantom-row failure.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/README.md Documents denial mechanism.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/restored.observed.txt Captures restored denial pass.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/case-1-permissive-sandbox.patch Retains permissive-sandbox mutation.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/case-1-permissive-sandbox.observed.txt Captures sandbox failure.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/case-2-fetch-call-site.patch Retains network-call mutation.
specs/010-catalog-backstage/evidence/negative-cases/network-denial/case-2-fetch-call-site.observed.txt Captures network-call failure.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/README.md Documents honesty cases.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/restored.observed.txt Captures restored honesty pass.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/case-1-a-real-claim.patch Retains overclaim mutation.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/case-1-a-real-claim.observed.txt Captures overclaim failure.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/case-2-the-same-terms-as-denials.patch Retains honest-denial case.
specs/010-catalog-backstage/evidence/negative-cases/honesty-close-out/case-2-the-same-terms-as-denials.observed.txt Captures denial pass.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/README.md Documents clean-clone verification.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/restored.observed.txt Captures restored checks.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/clean-clone-verification.observed.txt Records fresh-clone run.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/case-1-no-provable-mechanism.patch Retains missing-mechanism mutation.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/case-1-no-provable-mechanism.observed.txt Captures fail-closed result.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/case-2-package-invisible-to-the-build.patch Retains invisible-package mutation.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/case-2-package-invisible-to-the-build.observed.txt Captures package-presence failure.
specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/case-3-suite-wrapped-in-total-denial.observed.txt Records wrapped-suite failure.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/README.md Documents clause-8 cases.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/restored.observed.txt Captures restored gate pass.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-1-nonzero-false-negatives.patch Retains mismatch mutation.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-1-nonzero-false-negatives.observed.txt Captures mismatch failure.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-2-release-prepared.patch Retains release mutation.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-2-release-prepared.observed.txt Captures release-gate failure.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-3-step-b-inherits.patch Retains inherited-verdict mutation.
specs/010-catalog-backstage/evidence/negative-cases/clause8-gate/case-3-step-b-inherits.observed.txt Captures distinct-step failure.
specs/010-catalog-backstage/evidence/negative-cases/annotation-decode/README.md Documents ordering case.
specs/010-catalog-backstage/evidence/negative-cases/annotation-decode/case-4-step-2-3-reorder.patch Retains reordered-step mutation.
specs/010-catalog-backstage/evidence/negative-cases/annotation-decode/case-4-step-2-3-reorder.observed.txt Captures ordering failure.
Suppressed comments (1)

.github/workflows/ci.yml:63

  • The outer shell evaluates && git diff ... after the wrapper exits, and the following bundle check is also entirely unwrapped. These are post-install steps that can run under the proved denial, so they leave holes in the job's claimed network boundary.
      - name: Verify schema emit matches (network denied)
        run: bun scripts/run-network-denied.ts -- bun run schema:emit && git diff --exit-code schema/adr.schema.json
      - name: Verify committed Action bundle matches source
        run: git diff --exit-code packages/ci/dist

Comment thread specs/010-catalog-backstage/tasks.md
Comment thread .github/workflows/ci.yml Outdated
Comment thread scripts/check-honesty-close-out.test.ts Outdated
Comment thread packages/adapters/catalog-backstage/test/sc-009.test.ts Outdated
Comment thread scripts/check-observed-failing-register.test.ts Outdated
Comment thread specs/010-catalog-backstage/evidence/negative-cases/clean-clone-offline/README.md Outdated
Comment thread scripts/check-clause8-gate.ts Outdated
Comment thread scripts/check-clause8-gate.ts
@mbeacom
mbeacom marked this pull request as draft August 7, 2026 02:57
@mbeacom mbeacom self-assigned this Aug 7, 2026
@mbeacom
mbeacom force-pushed the mbeacom-phase-g-clean-clone-and-close-out branch from 8ccdd3c to a8e91a9 Compare August 12, 2026 14:01
mbeacom added a commit that referenced this pull request Aug 12, 2026
A pull request runs this gate from its own merge checkout, so a change that
edits the script or the workflow step invoking it can produce a green dco
status over unsigned commits. Every gate in this repository shares the
property, measured on #98: pull_request workflows execute the pull
request's ci.yml, not main's.

Recorded rather than papered over. Running the script from a trusted base
revision leaves the invoking step equally under the pull request's control,
so it would look like a control without being one — the failure ADR-0016 is
about. Tracked repository-wide in #137, where the option that changes
anything is a required review rather than a change to this file.

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 12, 2026
* ci(dco): enforce DCO sign-off as a required check

ADR-0006 chose a DCO over a CLA because it forecloses a rug-pull while
keeping contribution friction low. Sign-off was practiced by every
contributor and required by both CONTRIBUTING.md and the PR template, but
no ruleset check enforced it. An unsigned commit would have merged.

A repository script rather than the DCO GitHub App, so the gate stays
inside the surface ADR-0007 keeps mechanical and self-contained. It
imports only Node builtins and needs no bun install, so a broken
dependency graph cannot take the sign-off gate down with it.

Accept/reject semantics track the app's, since that is the contract
contributors already know, with two deliberate differences. A sign-off's
name and address must come from one identity; the app takes the name from
either the author or the committer and the address from either, so a
web-UI commit signed "Jane Doe <noreply@github.com>" passes there. And a
bot still has to sign: app accounts are exempt from the identity match
only, because Dependabot signs from support@github.com and cannot equal
its own author address by construction, but presence is still checked.
Every exemption is named in the output, so no commit is skipped silently.

The squash-merge body setting moves from BLANK to COMMIT_MESSAGES in the
same change. A pull-request check certifies the contributor, which is what
the DCO is for, but a blank squash body discards every trailer at merge:
main's own head f74c089 carries no sign-off while every commit proposed to
it carried one. Provenance verified and then thrown away is not provenance.

Per ADR-0016 the check was observed rejecting a real unsigned commit in a
real repository before it counted as coverage, and the negative cases are
permanent. The one that matters most for this class of check is that an
empty commit range is an error rather than a pass: an unfetched or
misspelled base ref makes git log return nothing and drives every count to
zero, which renders identically to a clean run.

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

* fix(dco): require a bot's trailer to name it, and name both identities on failure

Two defects found in review, both observed failing before the fix.

The bot exemption only checked that some sign-off existed. A bot-authored
commit carrying nothing but an unrelated person's Signed-off-by was
exempted and then reported as "signed by the app account as <that
person>" — a report asserting something it had never checked, which is
the ADR-0016 failure the exemption exists inside. Only the address half
needs exempting: Dependabot authors as dependabot[bot] with a noreply
address and signs as dependabot[bot] <support@github.com>, so the name
matches and only the address cannot. The trailer must now name the bot,
and a commit whose signatures name nobody relevant falls through to the
ordinary identity match so it fails with a precise message instead of
being waved through.

The failure diagnostic said "expected a sign-off by <author>" while the
classifier accepts the author or the committer. Where those differ, the
message omitted a valid way to fix the commit; it now names both, and
still names one where they are the same.

Verified against the real Dependabot commit on PR #99, which the tightened
rule still exempts, so no false positive was introduced.

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

* docs(dco): record the PR-checkout limitation the gate cannot close alone

A pull request runs this gate from its own merge checkout, so a change that
edits the script or the workflow step invoking it can produce a green dco
status over unsigned commits. Every gate in this repository shares the
property, measured on #98: pull_request workflows execute the pull
request's ci.yml, not main's.

Recorded rather than papered over. Running the script from a trusted base
revision leaves the invoking step equally under the pull request's control,
so it would look like a control without being one — the failure ADR-0016 is
about. Tracked repository-wide in #137, where the option that changes
anything is a required review rather than a change to this file.

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

* fix(dco): stop a bare Signed-off-by label from swallowing the real trailer

A four-lens deep review found one defect, and it fails in the expensive
direction: a correctly signed commit was reported as unsigned, which on a
required check blocks the merge.

The name and address classes excluded < and > but not newline, so a line
reading exactly "Signed-off-by:" let the lazy name group run down the
message to the next < and absorb the valid trailer beneath it into one
match named "Signed-off-by: Jane Doe". That falsified this parser's own
documented invariant, which claimed an extra match could only ever add a
candidate and never remove a valid one. Confining every class to a single
physical line fixes it and caps the backtracking as a side effect: a
500k-character bracket-free line went from superlinear rescanning to
1.4ms. Observed failing in a real repository before the fix, and the four
new cases were watched failing against the old regex.

The review's other accepted findings, none of which changed behaviour:

Failure messages now distinguish "could not examine" from "found unsigned
commits". An unfetched base made git say "fatal: bad revision", which
reads as a defect in the pull request rather than in the checkout; the
script now says which it is. This is the same reporting rule ADR-0016
states, applied to the check's own failures rather than to its verdicts.

The remediation block now shows the push, not just the rebase, and says
to use --force-with-lease rather than --force, since the tool has just
rewritten history and the obvious next command is the destructive one. It
also explains in one line what sign-off certifies, for a contributor who
has never met a DCO.

CI invokes the check:dco alias like every sibling gate rather than
holding a second reference to the script path. Verified to work with no
node_modules, since the job deliberately runs no bun install.

ADR-0006 gains a dco-signoff-required assertion in frontmatter, mirroring
ADR-0007's clean-clone-builds. In a project about governance-as-code, a
control recorded only in prose is invisible to anything querying the
corpus.

The merge exemption's comment no longer claims more than it can: an evil
merge's own diff is not certified by its parents. Matching the DCO app's
skip is a deliberate contract choice, not a property of merges.

CONTRIBUTING warns browser editors before the wall rather than after,
since the first-PR section steers newcomers toward exactly the docs edits
made in the web UI, which cannot carry a trailer. The PR template's
checklist item points at that section for commits already made.

Also corrects a contradiction inside this same Unreleased block, where
the ratification entry still said in the present tense that no ruleset
check enforces the DCO.

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
mbeacom force-pushed the mbeacom-phase-g-clean-clone-and-close-out branch from 0a18db6 to 6180a9b Compare August 13, 2026 01:03
mbeacom added a commit that referenced this pull request Aug 13, 2026
…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>
mbeacom added a commit that referenced this pull request Aug 14, 2026
…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>
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…
mbeacom and others added 12 commits August 13, 2026 22:41
…al, clause-8 gate (T093–T100)

Phase G, the last phase of feature 010. Also closes the four tasks earlier
phases left honestly unchecked, each judged on its merits.

- **T093** — `scripts/check-clean-clone.ts` asserts both feature-010 packages
  are present and buildable, because `bun run build` is green either way: a
  clone missing one builds three packages instead of four and says nothing.
  `scripts/run-network-denied.ts` proves a qualifying denial mechanism actually
  denies *in this environment* before running anything, and fails closed when
  none can be proved. Every step of `clean-clone-builds` after the install now
  runs through it; `bun install --frozen-lockfile` is the only networked step.
- **T094 / T095** — the generator runs with network **actively denied**, proved
  by a two-sided hermetic control (loopback listener; unsandboxed MUST connect,
  sandboxed MUST be denied) rather than by an absence of observed calls. Spike
  009's `scale-and-security-measurement.md` §5 is cited at its original location
  and not copied. A denied run and a networked run produce byte-identical
  envelopes, so the offline path is the only path.
- **T096** — cross-package round trip: an envelope the generator writes is read
  back from disk and validated by the consumer, with no import edge in either
  direction. Refused at the digest when tampered.
- **T097** — no spike-009 B/C/D heuristic. The claim rests on `deriveOwnership`
  receiving no path at all, which makes options B and C *inexpressible*, not
  merely absent.
- **T098** — the ADR-0020 clause-8 gate, shaped as a prohibition so it enforces
  clause 5 without preparing a release clause 9 defers. ADR-0020's frontmatter
  assertion is inert and is cited as the rule, never as enforcement.
- **T099 / T100** — the observed-failing register and the honesty close-out,
  both machine-checked. The honesty check matches *claims*, not vocabulary, and
  was verified on a real claim and a real denial carrying identical terms.

- **T058 / T062** — re-run against the corrected wording; five steps, three
  reasons. A hostile reason-preserving reorder of steps 2↔3 was observed being
  caught.
- **T078** — checked on the maintainer decision that `duplicate-canonical-ref`
  is unreachable end-to-end **by construction**. That property is now asserted
  rather than described.
- **T086** — SC-009 limb 2 discharged against the vendored accept corpus, with
  the citation to Phase F's live pass pinned.

Recorded in the register: `Bun.spawn` throwing ENOENT turned fail-closed into an
unnamed crash; T086's citation pin passed against a gutted harness; and
`commandFromArgv` rejected its own documented invocation because Bun strips `--`.

ADR-0014 **rung 1 only** — not `reference-verified`, not `externally validated`.
No release is scheduled, implied, or prepared. ADR-0012 gate 3's outcome is
recorded as observed; gate 4 is recorded unmet and not yet testable.

2136 pass / 0 fail. `compare-accept-corpus` still PASS at 0 FP / 0 FN.

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

Observed against a genuinely fresh clone: `bun scripts/run-network-denied.ts -- bun test`
fails 3 tests with `Failed to start server. Is port 0 in use?`. All three are
`Bun.serve({ port: 0, hostname: '127.0.0.1' })` in the two files whose job is to prove
network denial — their two-sided control needs a loopback listener a total-denial sandbox
correctly refuses.

The conflict is structural, not a bug. Skipping those tests was rejected (§5 makes
fail-closed a constraint on the environment, and `sc-016.test.ts` asserts the suite does
not skip). Loosening the profile to permit loopback was rejected for macOS because the
control would then need an external endpoint, reinstating the internet dependency the
loopback control removed.

`bun test` is therefore unwrapped, and FR-050's second half is recorded as holding in the
weaker form it actually holds in: no step requires network beyond the install, and every
step that can run under a proved denial does. Recorded at
`negative-cases/clean-clone-offline/` case 3 and register §4.5.

Found only because T093's clean-clone check was performed against a real fresh clone rather
than assumed from the working tree.

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

A fresh `git clone` into /tmp with no node_modules, at the committed HEAD: install as the
only networked step, then check:clean-clone, typecheck, build, lint, the four repository
checks, compare-accept-corpus and adr lint each under a proved denial, plus the full suite.

2136 pass / 0 fail. compare-accept-corpus PASS at 24 expected, 0 FP, 0 FN.

Every denied step printed its two-sided proof first (`control unsandboxed = CONNECTED:200`,
`control sandboxed = DENIED`), so none of those greens rests on an unverified sandbox.

The capture is registered as a deliberate pass-capture in
check-observed-failing-register.test.ts, alongside honesty-close-out case 2 — both are
evidence FOR a claim rather than failing observations, and the exception is named so it
cannot silently widen.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
None was caught by the checks themselves, which is the point of having had someone read
for them. All five are now covered cases.

1. **`commandFromArgv` discarded the command when the command contained `--`.** Bun strips
   only the FIRST separator, so scanning the whole argv mistook the command's own
   arguments for it. The CI step
   `run-network-denied.ts -- bun run release:pack -- --skip-build --skip-smoke-install`
   reduced to `['--skip-build','--skip-smoke-install']` — `release:pack` never ran and the
   tarball verification silently did not happen. Not a fail-open (denial was still proved
   first), but the step's stated work was replaced. Only a leading separator is honoured
   now; verified against the real CI line, which reaches `release-pack.ts` and exits 0.

2-4. **Three silent-absence holes in the clause-8 gate**, each the shape the gate exists to
   close: an absent/empty/array `recomputedFrozenHashes` iterated zero times and passed; an
   empty `requirement3_adequacyFinding` was 'not literally FAIL' and passed; and `corpusRef`
   missing from BOTH freeze and vendor manifest compared `undefined !== undefined` and so
   'agreed'. Each now requires an affirmative record.

5. **A self-satisfying assertion.** `offline-run.test.ts` searched its own source for the
   env literal — a needle present in the assertion line itself, so it matched itself and
   could never fail. It now inspects every `env:` literal for credential-shaped names; the
   property is separately bound from `sc-016.test.ts`, which reads the file from outside.

Also hardened `inFencedBlock` to match fences by open/close marker rather than backtick
parity, so a four-backtick or `~~~` fence cannot silently suppress the honesty rules for
the rest of a file.

Recorded in the register as §3 items 5-7, listed separately from the four found by
observation because these came from reading rather than from running.

2146 pass / 0 fail. compare-accept-corpus still PASS at 0 FP / 0 FN.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
`clean-clone-builds` has failed on every run of this branch since 2026-08-08,
always at its first denied step, always reporting that no qualifying §5
mechanism could be proved. That report was wrong.

Read the second rejection closely:

    rejected: unshare --net under sudo — unavailable here: --: 1: exec: bun: not found

`--: 1:` is the *inner* `sh` speaking, not `unshare`. `sudo -n unshare --net`
had already succeeded and the namespace existed; only the payload could not be
resolved, because `sudo` replaces PATH with `secure_path`, which does not
contain `/home/runner/.bun/bin` where setup-bun installs Bun. A working
mechanism was discarded on the strength of a broken payload.

Reproduced in ubuntu:24.04 character-for-character, and fixed:

  [C] candidate 2 as shipped, bare-name payload -> `exec: probe: not found`
  [D] candidate 2, absolute-path payload        -> DENIED
  [E] identity inside [D]                       -> root
  [F] [D] plus setpriv back to the invoking uid -> runner, still DENIED

Three defects, in dependency order.

**Payload resolution.** The probe and the command crossed the sandbox boundary
as bare words. They are now resolved first — the probe to `process.execPath`,
the command via `Bun.which` — and an unresolvable name is a usage error out
here rather than a mysterious failure in there.

**Classification.** Availability was inferred from the probe's own failure, so
"my payload is broken" and "this environment cannot deny" were the same
observation. That is this file's own subject matter turned on itself: §5
distinguishes a denial from an absence, and "no mechanism is available here" is
an absence claim that has to be earned. Availability is now established by a
sentinel that touches no network and only has to *run*; a probe that fails under
a demonstrably-working mechanism is reported as a payload failure and never as
a missing mechanism. The three reasons are asserted mutually non-prefixing so
they cannot quietly collapse back into one.

**Privilege.** Under `sudo unshare` the command runs as root, which would leave
root-owned build output that then breaks the *un-sandboxed* steps of the same
job (`git diff --exit-code packages/ci/dist`, `bun test`). `setpriv` hands the
invoking uid/gid back — after `ip link set lo up`, which needs privilege inside
the namespace. That order is asserted, not just the presence of both.

Not a relaxation. §5 names mechanism 2 as "Linux `unshare --net`"; `sudo` only
supplies the privilege the namespace requires and `setpriv` gives it back.
Nothing stops being denied, no threshold moves, and fail-closed is untouched —
if `setpriv` were absent the sentinel fails and the candidate is rejected,
because falling back to root would trade a provable denial for a silent side
effect. Pointedly, §5 also rules out a restricted `PATH` as a mechanism, and
the shipped bug was a PATH-resolution failure *inside* a genuine one.

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

Resolving the command to an absolute path was necessary and not sufficient.
`bun run build` execs its package scripts through bash, and those scripts say
`bun` — so the children need PATH even where the parent did not:

    $ bun run --filter='*' build
    /usr/bin/bash: line 1: bun: command not found

`typecheck` passed in the same run, because its script is `tsc`, which Bun
resolves from node_modules/.bin itself. A fix verified only against the step
that first failed would have looked complete and died on the next one.

Restoring PATH is the opposite of the restricted-PATH convention §5 rejects: it
makes the sandboxed run the same run as the unsandboxed one, and the denial
still comes from the network namespace and nothing else. Asserted as such — the
restored value must equal the caller's real PATH, and must be set before exec.

`offline-run.test.ts` carried its own copy of the candidate list with all three
defects, so the same bug had to be fixed twice. No test in that package reaches
outside it and this is not going to be the first, so the duplication stays and
the invariants are asserted on both sides instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
T093 stood checked while `clean-clone-builds` — the job that enforces exactly
that claim — failed five runs out of five. The verification behind it was taken
on Darwin, where the first candidate mechanism succeeds, so it could not
exercise the Linux fallback CI depends on. The capture named its host plainly;
what was wrong was accepting it as discharging a Linux-only claim.

That capture is retained rather than replaced, with the run of record appended:
GitHub Actions ubuntu-latest, all 18 steps green, the mechanism selected being
the sudo+setpriv candidate and not the macOS one, both halves of the control
recorded on the runner itself, uid 1001 rather than root, 2434 pass / 0 fail.

Three ADR-0016 negative cases, one per defect, each observed failing on the
assertion it was built to violate and each restored to 29 pass / 0 fail:

  case 3  privilege not dropped     -> command keeps root, later un-sandboxed
                                       steps break on root-owned output
  case 4  PATH not restored         -> `bash: line 1: bun: command not found`
  case 5  availability inferred     -> a broken payload reported as a missing
          from the probe               mechanism: the original defect

Case 3 fails two assertions, not one, because the PATH check anchors its
ordering on `setpriv`. Recorded rather than tidied, so a reader comparing cases
3 and 4 is not left wondering why the same assertion appears in both.

`plan.md`'s Adapters row still said "not built" and "No packages/adapters/
catalog-*/** exists", both false. It now records `implemented` — rung 1 only,
with rung 2 and rung 3 stated explicitly open, matching what the feature's own
honesty close-out already says of itself rather than rounding up from it. The
spike-009 carry-forward blocker is recorded as discharged, with the fresh
T014 -> T014a cycle and the retained input-order negative case named.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
§3 claimed seven defects and §4.2 claimed the Linux denial path was unobserved.
Both were true when written; neither is now, and leaving them would have made
the register assert something false about itself.

§3 gains findings 8-10 — the mechanism rejected while working, the command that
would have run as root, and the resolution that was necessary but not
sufficient — listed as a third category, "found by running the job on the
platform that runs it", because that is what distinguishes them. Every check in
§2 was observed failing on macOS, where the first candidate mechanism succeeds
and the Linux fallback is unreachable. ADR-0016 was satisfied to the letter on a
host where the defective path could not execute.

§4.2 is rewritten to say what happened rather than to be quietly deleted. It had
predicted "the first CI run is what will settle it", and it did — against us,
five times, exactly as designed, red immediately and never silent. What it did
not anticipate is that the report would be *wrong*: the gate was not unproven on
Linux, it was misdiagnosing Linux in the vocabulary of an environment limit.

The guard that required §4.2 to say "unobserved" went stale by being satisfied.
Deleting it would have removed the only check that the section says anything
falsifiable; it is replaced with two that cannot be met by softening — §4.2 must
name the CI run that settled it and must still record the five failures, and
§4.3 must state the host-provenance gap with its count. Both were observed
failing against exactly that softening, retained as
`observed-failing-register/` case 3.

§4.3 is new and reports a gap rather than closing it: 2 of 37 negative cases
name the host that produced the observation. Backfilling the other 35 would
manufacture provenance for runs nobody can re-observe, so the convention is left
for the next feature to require at capture time.

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

**Clause 8 printed "no release claimed, scheduled, or prepared" while checking
only package versions and RELEASE_PACKAGES.** `RELEASE_CLAIM_PATTERNS` existed,
with careful negation handling so denying a release would not read as making
one, and was applied to nothing at all. A README sentence asserting npm
publication left the gate green. Now scanned across the three artifacts a
consumer would read to decide whether this ships, with the count in the success
line so the summary cannot outrun the check again silently.

**`engine: custom` in ADR-0020 says which port the assertion would use, not
whether one exists** — and the port's absence is what makes it inert.
Composition code could register a custom engine and the frontmatter would not
move, leaving the gate reporting an assertion as inert after it had gone live.
Read from `packages/cli/src/evaluate.ts` now, and fail-closed: a composition the
gate cannot locate is reported rather than passed over, because "I could not
find it" and "it is not there" is the same confusion this feature keeps meeting.

**The register mapped directories by `includes()`, so prose counted as a row.**
Deleting a table row left a case "covered" by a narrative "see `spike-heuristic/`",
and §3 and §4 are full of those. Rows are parsed now. All 37 directories do have
real rows — no hidden gap, but that was luck rather than enforcement.

Also: sc-009's citation pin counted raw substrings, so a commented-out call held
the floor up. It counts executable occurrences now. The obvious regex was written
first and was wrong — character classes match newlines, so one unbalanced quote
swallowed everything and took the count 5 → 0, breaking the pin it was fixing.
The check is line-scoped so a mistake stays inside a line.

`check:clean-clone` ran unwrapped while the evidence README claimed every
post-install step was routed through the denial wrapper. Wrapped; and the
`git diff` bundle check too. One exemption remains and is now stated everywhere
it matters rather than in one buried paragraph.

**T093 is unchecked.** Its second conjunct — network "permitted only during
install" — is false: `bun test` runs with ambient network. Wrapping it was tried
on both platforms and fails differently on each, which is what makes the
exemption structural rather than a macOS quirk: sandbox-exec denies loopback (3
failures), while Linux `unshare --net` brings `lo` up so that failure does not
occur and 11 still fail, because the denial-proving tests must nest a sandbox
inside the one wrapping them. Retained as case 3b. Narrowing the exemption to
those two files needs a path allowlist whose staleness means tests silently not
running — the defect `check:clean-clone` exists to catch. Closing T093 needs an
ADR rescoping FR-050 or a task split, not more code.

Cases 4 and 5 retained under `clause8-gate/`, both observed failing and restored.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
A four-lens adjudicated panel (adversarial, architect, operator,
evidence-integrity; 28 findings, 1 corroborated across three lenses). The
findings that mattered are all the same shape, and it is the shape this feature
keeps meeting: a check or a document asserting more than it can see.

**The cross-check that did not exist.** `offline-run.test.ts` justified its
duplicated candidate list by naming a cross-check in `sc-016.test.ts`. That file
reads `offline-run.test.ts` and the spike contract; nothing read the script. The
two lists had already diverged — the script's namespace candidates bring `lo` up
and its suite asserts it, the adapter's did neither — so a denied run there could
also have been a broken one, and dropping `setpriv` would have passed every
assertion while the generation ran as root. Raised independently by three lenses.
The cross-check now exists and reads both files; `lo` is up on both sides. The
recorded rationale was wrong too: the binding constraint is the *import* boundary
(`envelope-shape-locality.test.ts`), not a convention against reading files that
two siblings in the same directory already break.

**RELEASE_PACKAGES was checked through a 304-character window.** The lazy
`[^=]*=\s*\[([\s\S]*?)\]` stopped at the first `]` in the file — the one closing
the first entry's `expectedFiles`. Four of five real entries were already
invisible, so a catalog package appended in the ordinary place would have passed
while the gate printed that both were absent. The unit test missed it by
inserting at the head, the one position inside the window; it appends now.

**The prose scan was fail-open and untested.** A missing target was skipped
silently while the summary printed a hard-coded 3, so moving a scanned artifact
deleted the coverage and kept the claim. And the test sandbox contained none of
the three targets, so the scan was a no-op in every test including the baseline.
Missing targets are findings, the count comes from files actually read, and the
sandbox carries the targets with claim, denial, moved-target and unparseable-JSON
cases.

**Negation was a two-token lookbehind on one of three patterns.**
`check-honesty-close-out.test.ts` already records why that fails: honest denials
put the negation further away. "Neither package is published to npm" and "No
release is scheduled for any version" — both verbatim T100 denial fixtures —
would have turned the build red, and the cheapest repair is deleting the denial.
Sentence-scoped now, with a denial case asserted.

**The fail-closed message blamed the environment for our own faults.** One
headline and §5's "available in the execution environment" printed for all three
failure classes, and in the control-failure class with zero candidate lines —
the same misdirection that cost five runs, relocated to the headline. Branched.

Documentation corrected where it outran the code: §4.6 and the case README said
one unwrapped step when there are three (`bun test` structurally, plus two
pending a workflow-scoped push); the honesty close-out said `check:clause8`
enforces `exports`/`dist`/`"//release"`, which it never reads; the verification
table called `check:clean-clone` denied when its own capture shows it unwrapped;
two test-count headers had rotted (12→29, 18→30).

Also: `0 fail` was a substring test that `10 fail` satisfied, in the guard whose
job is to reject exactly that — anchored; the generation run degraded to no
sandbox when none was proved, so a test named for running inside one passed with
the network open — throws now; `readJson` threw an unlabelled SyntaxError from
inside the wrapper — reported as a finding naming the file.

Settled by runtime evidence rather than argument: Bun resolves `exited` to 137
for a SIGKILLed child, so the signal-to-exit-0 risk does not materialize. The
guard is kept anyway, because being wrong means a green gate for a command that
never ran.

2374 pass / 0 fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
The prior citation was run 31656503096 at 6180a9b, before PR review changed
run-network-denied.ts's failure-path messaging. The argument that the delta
could not affect the observation was sound but is not evidence; run
31660000133 at 04349fa is, on the tree being merged.

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

Three post-install steps ran unwrapped while the evidence README claimed every
subsequent step was routed through the denial wrapper. Two of the three were
incidental — `check:clean-clone` only because of where it sat in the job, and the
Action-bundle `git diff` for no reason at all. The third was subtler and is the
one review caught: the schema step's `&& git diff --exit-code` ran *outside* the
wrapper under a step whose name said "(network denied)". All three are wrapped
now, so the ledger closes by construction rather than by enumeration.

`bun test` remains the single exemption, and it is structural: the suite contains
the two-sided controls that prove the denial, and a control cannot establish one
from inside itself. Observed on both platforms, failing differently on each —
macOS `sandbox-exec` denies loopback (3 failures); Linux `unshare --net` brings
`lo` up so that failure does not occur and 11 still fail, because the
denial-proving tests must nest a sandbox inside the one wrapping them. T093 stays
unchecked on account of it.

**Pinned to `ubuntu-24.04`.** This job's viability rests on three properties of
the runner image rather than on anything in this repository: AppArmor's
unprivileged-userns restriction (why candidate 1 is refused and the sudo
candidate is selected), passwordless `sudo -n` with a `secure_path` excluding
`~/.bun/bin`, and `unshare`/`setpriv` from util-linux. The 22.04 → 24.04
migration is what introduced the first. Because the gate is fail-closed, an
ambient image bump would turn this red on every PR and push at once, and the fix
would live in a workflow file — needing a `workflow`-scoped token while branch
protection built on this job blocks everything else. Pinning makes that bump a
reviewed change. A break-glass table is recorded in the evidence README, keyed on
the failure text the wrapper actually prints, so it does not have to be
reconstructed under that pressure.

The other jobs stay on `ubuntu-latest` deliberately: none depends on image
internals, so pinning them would add bumps to review without covering a risk.

Verified before pushing: the whole job reproduced from a clean clone in
ubuntu:24.04 with the runner's AppArmor restriction simulated so the sudo
candidate is the one selected — 15/15 steps pass, including all three
newly-wrapped ones, with no root-owned path in the tree afterwards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom and others added 4 commits August 13, 2026 22:41
The 2026-08-12 capture stays; it is accurate for what it recorded and is
superseded rather than replaced, because the job changed after it — three
unwrapped steps were wrapped and the image was pinned.

The pin is now observable rather than argued for: the log's `Image: ubuntu-24.04`
line was previously the resolution of `ubuntu-latest` and could move without a
commit. Thirteen steps run under a proved denial, including the schema step whose
`&& git diff` had been outside the wrapper while the step's name said otherwise.

`bun test` remains the single exemption and T093 stays unchecked on account of it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
The table under "The verification itself" reported nine steps; the capture it
cites contains seven. `lint`, `check:deps`, `check:freeze-hashes`,
`check:clause8` and `check:no-spike-heuristics` were listed as `denied / all ok`
and were never run in that session, and `check:clean-clone` was marked denied
when it ran unwrapped — the file shows it with none of the proof lines every
wrapped step prints.

A table reporting results its own capture does not contain is the failure this
evidence family exists to make impossible, so the rows are cut to the capture
rather than the capture described more generously. The CI run of record is the
authority for what the job does today; this capture is a 2026-08-05 Darwin run
and is now labelled as covering seven commands.

Also closes the ADR-0016 obligation the tightened pass-capture guard created.
`toContain('0 fail')` was satisfied by `10 fail`, and the companion
`/\b1 fail\b|\b[2-9] fail\b/` did not match it either — no word boundary between
`1` and `0` — so a capture recording ten failures was accepted as evidence of a
pass by the assertion whose job is to reject exactly that. Observed failing
against that mutation and retained as `observed-failing-register/` case 4.

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

The negative-case README had grown a break-glass runbook and an image-pinning
rationale. Neither is a negative case, and the directory is an archive of
observations: putting a living constraint in it means either the archive rots or
it stops being an archive. Discoverability ran backwards too — nobody debugging a
red build at 3am looks under a feature's retained negative cases — and the CI job
outlives feature 010, so the guidance would be stranded in 010's tree when 011
lands.

The rationale now lives only in the `ci.yml` comment beside `runs-on:`, where
whoever changes that line will see it.

The break-glass table is deleted rather than moved. It restated what
`run-network-denied.ts` already prints at the moment of failure — the branching
added earlier in this review names which of the three classes you are in and what
to look at. A prose table duplicating a diagnostic is strictly worse than the
diagnostic, because it is the copy that goes stale. The README says so instead of
carrying the table.

Also collapsed the one genuine duplication the same question exposed: the
platform-by-platform observation for `bun test` was told in full in both
observed-failing-register.md 4.6 and the case README. 4.6 owns the disclosure and
now points at the case; the case owns the evidence, because it is the directory
that retains the captures.

Net -40/+30 lines, and the README loses a section it should not have had.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
FR-050 read "network access is permitted **only** during dependency
installation" — a claim about *availability*. Principle II's operative clauses
test *dependence*: "MUST **require** no … network access", "**Network-dependent**
tests and runtime behavior are forbidden". FR-050 restated the principle using a
framing the principle does not use, and presented it as a faithful derivation.

One step of `clean-clone-builds` then failed the requirement's wording while
satisfying every clause the principle actually states. The wording was the
defect, so the wording is corrected. The rest of FR-050 — no credential, no
running service, nothing *required by* build, test, or generator invocation —
stands unchanged.

`bun test` is the one post-install step of fourteen that does not additionally
run under a proved denial, and it cannot: the suite contains the two-sided
controls that establish the denial, and a control cannot prove one from inside
it. Observed on both platforms, failing differently — 3 tests on macOS, where
`sandbox-exec` denies loopback outright; 11 on Linux, where `lo` comes up and the
denial-proving tests instead fail to nest a sandbox inside the one wrapping them.
That difference is what makes the obstruction structural rather than a macOS
quirk. It has a network reachable and never uses one; a test that reached the
network would be an FR-050 defect regardless.

T093 is closed on the corrected requirement and on the runner evidence — run
31761606849 on the pinned ubuntu-24.04 image, both halves of the control recorded
on the runner, running as uid 1001 rather than root.

Coverage is stated as thirteen of fourteen everywhere it appears, never "every
step is denied": rounding the count up claims coverage the run does not have.
Counted from the workflow twice, independently, because the number now appears in
a requirement.

`observed-failing-register.md` §4.6 is now the single source for the exemption;
`tasks.md`, the workflow comment, and the case README point at it rather than
restating it. Three copies would be three places to drift, and this feature has
twice been bitten by a description outliving what it described.

§4.6 also records the test this nearly failed. The first attempt to resolve the
FR-050 wording was a constitutional ADR interpreting Principle II and recording a
bounded exemption; it was ratified and then reverted. Every fact in it was already
true and already written down before it existed — the two-platform signature, the
nesting obstruction, the rejected allowlist. It added a ratification step, not a
finding, and the tell was that the analysis never changed. If a record would
contain no fact not already established elsewhere, it is ratification theatre.

2441 pass / 0 fail.

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-phase-g-clean-clone-and-close-out branch from f2de8f0 to 13a1f9b Compare August 14, 2026 02:45
`inFencedBlock` stored `fence[0]`, so the opening run's *length* was
discarded and only its marker character survived. A ``` line therefore
closed a ```` block — which is exactly how one quotes a fenced block
inside a fenced block, the construct the negative-case READMEs are built
out of.

Both directions were wrong, and the second is the dangerous one:

- the quoted content lost its exemption, so the honesty rules fired on
  the fixture a document exists to display;
- where such a block held an odd number of line-leading ``` markers, the
  real ```` closer was read as a fresh *opening*, leaving a fence open to
  end of file and silently exempting every claim below it. A real
  "reference-verified (rung 2) and externally validated" passed the gate.

That second case is this phase's own thesis turned on one of its own
checks: a check that doesn't run is indistinguishable from a check that
passes. The docstring already warned about the marker-character half
while the code implemented only that half, so the comment moves with the
fix rather than being left to overclaim.

Closing now takes CommonMark's two conditions — same character, run at
least as long as the opening one. `~~~` behaviour is unchanged.

Observed failing before it counted as coverage (ADR-0016): with the
implementation reverted and the tests intact, both new cases fail while
the other 35 in the file still pass, so they discriminate this defect
rather than catching incidental breakage. With the fix: 37 pass / 0 fail.
Full suite 2441 -> 2443, 0 fail; typecheck, lint, check:clause8,
check:no-spike-heuristics, check:freeze-hashes, check:deps and adr lint
all green.

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

Copy link
Copy Markdown

Decisions governing this change

  • 0007 — Isolate integrations as optional adapters and build only against public surfaces
    • via path: .github/workflows/**
    • via path: packages/adapters/**
  • 0009 — Pin affects resolution semantics and bind entity refs to pluggable catalogs
    • via path: packages/adapters/catalog-*/**
  • 0010 — Use Bun as the package manager and test runner while publishing Node-targeted artifacts
    • via path: .github/workflows/**
    • via path: package.json
  • 0012 — Bind catalog entities to owned paths with an explicit annotation
    • via path: packages/adapters/catalog-*/**
  • 0013 — Reconcile adapter isolation and catalog binding with the offline snapshot generator
    • via path: packages/adapters/catalog-*/**
  • 0014 — Stage phase-landing evidence across a three-rung validation ladder
    • via path: plan.md
    • via path: specs/**/spec.md
    • via path: specs/**/tasks.md
  • 0015 — Validate descriptors against Backstage field formats before canonicalizing identity
    • via path: packages/adapters/catalog-*/**
  • 0016 — Require every check to be observed failing before it counts as coverage
    • via path: packages/adapters/*/test/**
  • 0017 — Keep dependency audit scope explicit and release-scoped
    • via path: .github/workflows/ci.yml
  • 0020 — Rescope SC-010 and authorize work toward the Backstage catalog adapter
    • via path: packages/adapters/catalog-backstage/**
  • 0025 — Ship badges as recipes over existing output, not a new CLI surface
    • via path: .github/workflows/ci.yml
    • via path: package.json

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