# RHA Wrapper — Cursor Instructions (ACTIVE) — Execution epoch 2026-07-26L

Shipped and pruned: sets 1–28, 29a/b/c, 30, 31a/b/c, 31d (planner + cross-run corpus compounding), 31e, 32, 33, 34 (report evidence), 35a–e, 36a–g, 37a–i (repair relay, solution strata, mock-API mechanical, empty-draw, superbuffer pages, ideation entropy, failure stack, tone map, continue-project), 38a–d (program-parse gate, ddmin, parse-poison loop, planner-bypass), 39a–c (total error catalog), R1/R2/R3/R4, SET 40 (CSP parse-gate fail-open + sandbox probe, born-red, build-failed, vitest harness surface, adaptive desync/hang evidence, concept 3-sentence tune), SET 41 (loud persist + clone-safe records + viewer honesty), SET 42b/d/e (edge-wait challenge+asset coherence, `/assets/*` no SPA HTML + immutable headers + Pages Function guard, landing ethos/name scrub + build identity guard; LICENSE.txt copyright = `rha.software (the RHA Wrapper author)`), SET 42f (pinned GREEN tone fixtures: filled/frozen/converged/healed/rebound/un-parked), SET 42g (fifth tone FRICTION / dark orange + superbuffer-assembled yellow pins), SET 42h (build-phase tone sweep), SET 43a–f (mechanical permutation-select, spec-constant repair + constants page, export-conformance at vet, clade drafts response-shape/format-string/pagination, handler-candidate promotion, substrate hit-rate + classKey coverage rule), SET 44 (excluded vocabulary — scanner honesty, TRACEABILITY section), SET 45a–c (substrate mark + metrics strip, substrate handoff page, WITH/WITHOUT page efficacy), SET 47a–g/f (GATE_UNAVAILABLE fail-open + sandbox module probe, suite scaffold-wire at freeze, ownership-first import resolver, live vault version label, export-conformance dedupe, project live-epoch metrics, brand→home link), SET 48 (run-entry → RUN view by id, never admin/install), SET 49 (brand same-tab harden + post-build boot check), SET 50 (web stale-bundle banner + version.json poll), SET 51 (register-at-start / checkpoint / finalize persist + SW ACK + header fallback), SET 52 (App.tsx restore to vault-1.7.20 surface + friction tone widen + SET 49 boot markers; App.tsx wiring for 41c/45a/47d/48/50/51 accepted losses — modules kept), SET 53 (brand→home/about same-tab re-applied on restored App.tsx), SET 55 (landing epigraph: move before footer + “Beware of he who…” SMAC wording), SET 56a (single source of live truth + stale-running sweep), SET 56b/c (1.4-era run surface contract + reporting conformance). Live-verified through vault 1.7.33; **1.7.34 ships SET 56d**. Doctrine (substrate first; LLM = permutation supplier, algorithm selects; free draw last; prompt = intent oracle) remains binding.

## A. Version bump — standing duty
Any change under `extension/`, `web/`, `src/tier1/`, `src/core/`: bump `extension/manifest.json` patch version.

## A2. Commit + tag at every release — standing duty
At every manifest bump, BEFORE deploy: commit the working tree and tag `vault-<version>`. Uncommitted releases are unbisectable releases.

## A3. Auto-approve all gates — standing duty
Execute every instruction set with all gates auto-approved — HIL/approval gates and editor/tool confirmation prompts alike. Never stall an execution waiting for interactive approval. (Applies to this and every future set, whether or not the set restates it.)

## B. Build, deploy, replace extension files
1. Bundle-reachable code changed → `npx vite build --config vite.tier1.config.ts` (abort on failure).
2. `npm run build:web`
3. `npx wrangler pages deploy --branch main --commit-dirty=true`
4. Edge-wait: `npx tsx scripts/edge-wait.ts <NEW VER>` (zip version + no challenge bootstrap on `/app` + every hashed asset MIME-coherent + dead `/assets/x-deadbeef.js` → 404 no-store)
5. Atomic replace: unzip → `~/rha-provider-vault.new`, verify version, `mv` swap, delete old.
6. Andy ⟳'s the card — never mid-run.

---

# **UNIVERSAL RULE for every SET below:** Line references (e.g. `runner.ts:1725`) drift as
# earlier SETs land. Do NOT navigate by line number. Instead grep for the exact string quoted
# in the instruction. Example: instead of "go to runner.ts:2620", do
# `grep -n "redrawnAtGate = new Set" src/tier1/runner.ts` and work from what you find.
#
# **BASELINE for every "wall time drops N%" claim:** live run 08d1e099 on vault v1.7.71
# (2026-07-28T18:04, gemma-4-e2b, spec = wiki-clone with F1..F8):
#   Total 58m09s. IR 22:10. Build 4:10. RHA 31:18. Auth 0s.
#   IR CONTRACT 63% (5/8 frozen). BUILD 100%. RHA 63% heal rate (5 healed, 20 unsuccessful).
#   Watcher 103 syntheses/103 readings. Observatory 108 polls. Meta-watcher 27 syntheses.
#   substrate hit-rate 0/110 fixes (0%). corpse rejections 6. parse-poison incidents 3.
#
# ACTIVE QUEUE — six sets, ordered by value. Written explicitly so Cursor can execute end-to-end.
# Each set carries a full DOCTRINE COMPLIANCE REVIEW header. Every code snippet is the actual code
# to write, not a sketch. Every acceptance test is runnable and its predicate is stated. Every
# release follows standing duties A/A2/A3/B. Never deploy or vault-swap while
# tier1:live.running is true. Order: 92 → 93 → 94 → 95. Ship each as its own vault release with
# its own git tag so a rollback isolates one set at a time.

---

# =============================================================================
# CURSOR EXECUTION MANIFEST — read this first, then execute each SET in order
# =============================================================================
#
# You are Cursor, the execution owner for this project. Claude wrote the SETs
# below; you land them. Each SET is self-contained: doctrine compliance review
# at the top, then explicit executable instructions with code snippets.
#
# Order of execution (each SET ships as its own vault release with its own git
# tag, so a rollback isolates one landing):
#
#   1. SET 92 — freeze-gate test-rha owner-boundary + no silent branches
#              (releases as vault-1.7.72)
#   2. SET 93 — born-red drops stub-derived throw cases before surgery
#              (releases as vault-1.7.73)
#   3. SET 94 — compute repair: digest cache, spacing bypass, substrate cap,
#              world-advance skip, DELETE meta-watcher LLM path
#              (releases as vault-1.7.74)
#   4. SET 96a — CPU merge of watcher + superbuffer into `assembleContext`
#              (releases as vault-1.7.75; UNCONDITIONAL)
#   5. SET 96b-CANARY — fold watcher call into candidate 0. RUN CANARY FIRST;
#              ship only if candidate-0 acceptance rate within 5% of baseline
#              (releases as vault-1.7.76 IF the canary clears; otherwise SKIP)
#   6. SET 97 — substrate-first fill (matchConsumableTemplate → rebindCladeBody
#              → methodAnchorBody BEFORE the K-ladder)
#              (releases as vault-1.7.77)
#   7. SET 95 — LLM-as-transmission: typed-hole prompts + overreach|body
#              rejection. LAST because it changes prompt shape everywhere and
#              needs 92-97's telemetry to measure honestly.
#              (releases as vault-1.7.78 through 1.7.83 for 95a/b/c/d/e)
#   8. SET 98 + SET 101 — RHA wall-time dominators from live run fab5aeb4:
#              catalog-drift mechanical handler, stub-not-implemented routing,
#              observatory poll gate, L2 short-circuit, quarantine terminal.
#              (releases as vault-1.7.84 through 1.7.87) — EXECUTE BEFORE SET 100.
#   9. SET 100 — heal-then-reject emit + partial-label honesty + px-only
#              residue collapse + encode plan-gate refusal for
#              no-verifiable-residue prompts. Doctrine 7/12/24 + blind spot A.
#              (releases as vault-1.7.88 through 1.7.91 for 100a/b/c/d)
#  10. SET 102 — wire the existing 0-token regex-literal healer
#              (applyDelimiterOracle) into both program-parse write-acceptance
#              gates (build + RHA repair) so "Invalid regular expression"
#              crashes heal deterministically instead of forcing a full re-draw.
#              (releases as vault-1.7.92)
#
# For every SET, the release pipeline is (standing duties A/A2/A3/B):
#   1. Bump extension/manifest.json patch version
#   2. Rebuild tier1 bundle: `npx vite build --config vite.tier1.config.ts`
#   3. Build web: `npm run build:web` (must end `check-web-boot OK`)
#   4. Full suite: `npx vitest run tests` (all green including the new test files)
#   5. Both typechecks: `npx tsc --noEmit && npx tsc -p tsconfig.web.json --noEmit`
#   6. Commit + git tag `vault-<version>` BEFORE deploy
#   7. Deploy: `npx wrangler pages deploy web/dist --project-name rha-software --branch main --commit-dirty=true`
#   8. Edge-wait: poll `https://rha.software/api/extension.zip` with cache-busters
#      until manifest version matches; grep the bundle for a SET-specific marker
#      to prove the code is actually in the zip
#   9. Atomic vault swap: unzip to `~/rha-provider-vault.new/`, `mv` swap,
#      never rm-in-place
#  10. Andy reloads the extension when idle
#
# NEVER deploy or vault-swap while `tier1:live.running` is true. The source-tree
# work can proceed regardless; the release steps wait for job idle.
#
# Rollback protocol per SET: if a live run after landing shows the falsifier
# regressed (specifically SET 91's `IR CONTRACT >= 50%` on the wiki-clone spec
# or the SET-specific falsifier stated in each doctrine review), roll back to
# the prior vault tag and reopen the SET.
#
# ------------------------------------------------------------------------------
# SUMMARY TABLE
# ------------------------------------------------------------------------------
#
# SET | Goal (one line)                              | New files                              | Existing modified                      | New test file(s)                             | Release tag       | Key falsifier
# ----|----------------------------------------------|-----------------------------------------|-----------------------------------------|-----------------------------------------------|-------------------|-----------------------------------------------
# 92  | freeze-gate test-rha: 1 owner, no silent br  | —                                       | runner.ts, watcher.ts, metaWatcher.ts   | set92-testrha-conservation, set92-testrha-mutation | vault-1.7.72     | testRhaDraws = threw + rejected + acc-cleared + acc-still-sick
# 93  | born-red drops stub-derived throw cases      | —                                       | bornRed.ts, runner.ts, vanilla.ts       | set93-born-red-stub-derived                   | vault-1.7.73     | metrics.bornRedStubDerivedDrops >= 1 on synthetic spec; no surgery event for that case
# 94  | compute + delete meta-watcher path           | —                                       | runner.ts, watcher.ts, localLlm.ts, substrateHandoff.ts, superbuffer.ts, DELETE metaWatcher.ts | set94-watcher-cache, set94-substrate-cap, set94-world-advance, set94-meta-deleted | vault-1.7.74 | watcherCacheHits >= 20; watcherWorldStaleSkips ~ # all-failed batches; grep metaWatcher returns 0
# 96a | one assembleContext CPU pass                 | assembleContext.ts                       | runner.ts (5 IR-fill call sites)        | set96-context-bundle                          | vault-1.7.75     | no `synthesizeForDraw('ir'` in runner.ts; watcher counters MOVED to meta-watcher-only after 96a lands
# 96b | fold watcher LLM into candidate 0 (CANARY)   | scripts/set96-canary.ts                  | runner.ts (candidate 0 gets preamble)   | set96b-canary                                 | vault-1.7.76 IF canary clears | watcherInfusedCandidates ~ fillBatches; watcherStandaloneCalls_on_fill_path = 0
# 97  | substrate exhaustion BEFORE the K-ladder     | substrateFill.ts                         | runner.ts (fill batch, case-relay, born-red surgery) | set97-substrate-first-fill                    | vault-1.7.77     | substrateFillHits >= 3 on wiki-clone spec; IR wall time drops >= 3 min vs 22:10 baseline
# 95a-e | LLM writes bodies, CPU writes frames       | frame.ts + IR_SYSTEM_HOLE / BUILD_SYSTEM_HOLE prompts | runner.ts (6 LLM call sites), prompts.ts | set95-hole-only, set95-overreach              | vault-1.7.78..83  | avg response tokens per case-fill <= 300 (was ~900); zero `grammar fallback: accepted freeform` lines
#
# ------------------------------------------------------------------------------
# FILE OWNERSHIP AFTER ALL SETs LAND
# ------------------------------------------------------------------------------
#
# NEW files that must exist:
#   src/tier1/assembleContext.ts       (SET 96a — CPU merge)
#   src/tier1/substrateFill.ts         (SET 97 — substrate-first)
#   src/tier1/frame.ts                 (SET 95b — typed-hole skeletons)
#   scripts/set96-canary.ts            (SET 96b — benchmark harness)
#   tests/tier1-set92-testrha-conservation.test.ts
#   tests/tier1-set92-testrha-mutation.test.ts
#   tests/tier1-set93-born-red-stub-derived.test.ts
#   tests/tier1-set94-watcher-cache.test.ts
#   tests/tier1-set94-substrate-cap.test.ts
#   tests/tier1-set94-world-advance.test.ts
#   tests/tier1-set94-meta-deleted.test.ts
#   tests/tier1-set96-context-bundle.test.ts
#   tests/tier1-set96b-canary.test.ts
#   tests/tier1-set97-substrate-first-fill.test.ts
#   tests/tier1-set95-hole-only.test.ts
#   tests/tier1-set95-overreach.test.ts
#
# DELETED files that must not exist:
#   src/tier1/metaWatcher.ts           (SET 94e — deleted entirely; only the
#                                       corpse-rejection hash cache and its emit
#                                       survive, and both live in runner.ts /
#                                       metaSb.ts, not metaWatcher.ts)
#
# ------------------------------------------------------------------------------
# STANDING INVARIANTS Cursor MUST preserve across all SETs
# ------------------------------------------------------------------------------
#
# 1. DOCTRINE.md is binding. Read it before diagnosing, fixing, or writing a
#    Cursor instruction (see CLAUDE.md preamble). The 24 maxims apply to every
#    change. Every Cursor commit that touches src/ must be traceable to a
#    doctrine-compliant justification in the commit message.
#
# 2. Small local models are the THESIS. Never recommend a bigger model as the
#    remedy for a failure. Tighten the algorithm instead. See SET 20 in
#    DOCTRINE.md.
#
# 3. CPU takes deterministic low-complexity work; LLM takes semantic pattern
#    recognition. AST/regex extraction is CPU. Idiom/trope/scheme identification
#    on top of it is LLM. Never fold either way silently. See memory
#    `cpu-vs-llm-allocation.md`.
#
# 4. Conservation of evidence (doctrine 7). Every action produces logging or
#    output confirmation. No silent branch, no bare `catch {}`, no batch of
#    intentions stamped at one timestamp with no per-unit outcome. If it changed
#    state, spent a token, or decided not to, it says so.
#
# 5. Auto-approve all gates (standing duty A3). Do not stall on interactive
#    approval prompts. Ship each SET without waiting for approval unless a
#    live-run falsifier explicitly regressed.
#
# 6. Never restart the engine or reload the extension during an active job.
#    In-memory state dies (see CLAUDE.md gotchas). The release pipeline WAITS
#    for job idle.
#
# 7. Line references in the SETs below WILL DRIFT as you land earlier SETs.
#    ALWAYS grep for the exact string quoted in the instruction. Do not
#    navigate by line number.
#
# 8. Every SET's regression fixture must be CLASS-based when it doubles the
#    LlmClient (SET 88 lesson: object-literal doubles miss unbound-receiver
#    bugs). See DOCTRINE.md §I, blind spot G.
#
# 9. The 78e36cc1 collapse (0/10 features frozen on v1.7.70) is the reference
#    failure for SET 91's freeze-gate work. If a live run reproduces
#    `IR CONTRACT 0%` on any of the SETs below, that SET regressed SET 91 and
#    MUST roll back.
#
# =============================================================================
# END CURSOR EXECUTION MANIFEST
# =============================================================================

# ACTIVE — SET 92. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# (This supersedes the earlier SET 90 draft. Since SET 89 v2 and SET 91 both landed, some line
# references drifted; SET 92 is written against the current tree.)

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- Every started action inside the freeze-gate test-rha loop emits ONE terminal disposition,
  legibly and unambiguously (doctrine 7).
- Every superbuffer assembly at this site is emitted through the same owner as fill
  (doctrine 6): same `superbuffer: assembled (...)` log line format, same counter increment.
- A ladder labeled `try N/K` behaves like N-of-K or the label changes to say `miser` and the
  semantics are stated up front (doctrine 12).
- A case that never received a draw is not reported the same as a case whose draws all failed
  (doctrine 5): silent skip prohibited.

Work unit and identity:
- Work unit = ONE freeze-gate redraw attempt for ONE case (`(featureId, caseName, attempt)`).
- Ladder work unit = ONE case's full ladder run (up to K attempts).
- `redrawnAtGate` is a per-pass set, not per-lifetime.

Canonical state and owner:
- Superbuffer assembly at test-rha MUST route through the same helper the fill path uses. Fill
  emits `assembly.logLine` and increments `superbufferDraws` at runner.ts ~L1725; RHA repair at
  ~L4619; test-rha (runner.ts ~L2643-2657) currently does an inline `formatSuperbufferForPrompt(
  assembleSuperbuffer({...}))` inside an `errors[]` array — no event, no counter. Extract one
  `emitSuperbufferAssembly` helper, route all three sites.

Path inventory:
- triggers: freeze-gate test-rha per case (runner.ts ~L2620). Outer freeze pass may enter this
  block multiple times per feature.
- writers: `parts.cases` mutation at ~L2666; `redrawnAtGate.add` at ~L2621; `sick` re-partition
  at ~L2676.
- readers: subsequent freeze evaluation; subsequent test-rha passes in the same feature; suite
  conformance; metrics strip (`testRhaDraws`).
- fallbacks/reconstructions: the case-drop path at ~L2687-2695 reads leftover `sick` and pushes
  `dropped at freeze-gate — sick-red unrepairable` for every case still sick — including cases
  that never got attempted. Primary silent-skip path.

Deterministic predicate (per attempt, in order):
1. `caseHit = caseForFailure(target.caseName)` → if undefined, emit `test-rha: case "<target>"
   skipped — no case handle` and terminate this case with disposition `skipped-no-handle`.
2. `redrawnThisPass.has(caseHit.name)` → if true, emit `test-rha: case "<name>" skipped —
   already redrawn this pass` and terminate with `skipped-already-redrawn`.
3. `await runLlmAction(...)` → on throw, emit `test-rha: draw threw (try N/K, t=<temp>) —
   <error.message.slice(0,200)>` and terminate this attempt with `attempt-threw`; continue.
4. Body checks: on empty or `!acceptCaseBody(body)`, emit `test-rha: draft rejected (try N/K)
   — <reason>` and terminate this attempt with `attempt-rejected`; continue.
5. On accept: emit BOTH `test-rha: draft accepted (try N/K, t=<temp>)` AND, after
   re-measurement, ONE of `test-rha: case "<name>" cleared` OR `test-rha: case "<name>" still
   sick — continuing ladder`. Terminate the attempt with
   `attempt-accepted-cleared` or `attempt-accepted-still-sick`.
6. Ladder exhaustion (K attempts, none cleared) → `test-rha: case "<name>" exhausted after K
   attempts` with disposition `case-exhausted`; this case joins the drop set.

Failure dispositions (per attempt AND per case):
- Per attempt: `attempt-threw | attempt-rejected | attempt-accepted-cleared |
  attempt-accepted-still-sick`.
- Per case: `case-cleared | case-exhausted | skipped-no-handle | skipped-already-redrawn |
  case-dropped-unrepairable` (last only after `case-exhausted`; never a first disposition).
- Every case-level disposition is emitted BEFORE the outer `problems.push(...)`. The drop loop
  reads a `caseDisposition: Map<string, string>` — a case with any disposition other than
  `case-exhausted` MUST NOT be pushed as "sick-red unrepairable".

Metrics and conservation equations:
- Add to `Tier1Metrics`: `testRhaSuperbufferAssemblies`, `testRhaDrawThrew`,
  `testRhaDraftRejected`, `testRhaDraftAcceptedCleared`, `testRhaDraftAcceptedStillSick`,
  `testRhaSkippedNoHandle`, `testRhaSkippedAlreadyRedrawn`, `testRhaCaseExhausted`.
- `testRhaDraws` (existing) counts total attempts.
- Conservation: `testRhaDraws = testRhaDrawThrew + testRhaDraftRejected +
  testRhaDraftAcceptedCleared + testRhaDraftAcceptedStillSick`.
- Cases entered = `testRhaCaseCleared + testRhaCaseExhausted + testRhaSkippedNoHandle +
  testRhaSkippedAlreadyRedrawn` (add `testRhaCaseCleared` too).

Causal hypothesis:
- Silent branches at this site cause the reported symptom ("no superbuffer, weren't invoked
  3 times"). Four sources: (a) inline superbuffer bypasses owner → no `assembled` line and no
  counter; (b) `try N/K` label only fires after full acceptance → reader can't distinguish
  "attempt 1 succeeded" from "attempts 2,3 never ran"; (c) `catch { /* ... */ }` swallows the
  LLM error without a line; (d) `redrawnAtGate.has` and undefined `caseHit` each drop a case
  with no explanation.

Falsifier:
- Predicted post-fix observations on a mock run that (i) supplies a `caseForFailure` name that
  will not resolve; (ii) throws on attempt 2; (iii) accepts on attempt 3:
  - Log contains exactly one `skipped — no case handle` line for case (i).
  - Log contains exactly one `draw threw` line naming the exception for (ii).
  - Log contains `draft accepted (try 3/3, t=1)` followed by `case "<name>" cleared` for (iii).
  - `testRhaDraws === 3`, `testRhaDrawThrew === 1`, `testRhaDraftAcceptedCleared === 1`,
    `testRhaSkippedNoHandle === 1`.
- Fix insufficiency: any log line `dropped at freeze-gate — sick-red unrepairable` for a case
  whose disposition was `skipped-no-handle` or `skipped-already-redrawn` proves fix insufficient.

Production-shaped proof:
- CLASS-based `LlmClient` double (doctrine 15) — objects that use `this`, like `LocalLlmClient`.
- Mutation test: delete each new `emit(...)` line one at a time; assert the corresponding 92g
  assertion fails.

Terminal certification:
- `casesEnteredTestRha` on the report has an entry in `caseDisposition` for every case;
  missing entries are a runtime assertion failure surfaced via `problems[]`.

Release blockers:
- Any `catch (` in this block without an `emit(...)` line inside the same block.
- Any `problems.push(... sick-red unrepairable ...)` reached from a case whose disposition is
  not `case-exhausted`.
- Any test-rha LLM call site whose superbuffer is not built through `emitSuperbufferAssembly`.
- Any log-line mismatch with the conservation equations above.

---

Executable instructions (Cursor):

## 92a — extract `emitSuperbufferAssembly` (owner boundary; doctrine 6)

In `src/tier1/runner.ts`, add this helper near the other emit helpers (search for
`const emit = `):

```ts
type SbStageContext = 'fill' | 'repair' | 'test-rha' | 'case-relay' | 'born-red';
const emitSuperbufferAssembly = (
  stage: 'ir' | 'build' | 'rha',
  featureId: string,
  assembly: { logLine: string },
  ctx: SbStageContext,
): string => {
  emit(stage, featureId, assembly.logLine);
  metrics.superbufferDraws++;
  if (ctx === 'test-rha') metrics.testRhaSuperbufferAssemblies++;
  return formatSuperbufferForPrompt(assembly as Parameters<typeof formatSuperbufferForPrompt>[0]);
};
```

Route the three existing assembly sites through it:
- Fill (~L1725-1727): replace `emit(...)` + `formatSuperbufferForPrompt(assembly)` + counter
  with `irSuperbuffer = emitSuperbufferAssembly('ir', feature.id, assembly, 'fill')`.
- RHA repair (~L4619-4621): same, `ctx='repair'`.
- Test-rha (~L2643-2657): remove the inline `formatSuperbufferForPrompt(assembleSuperbuffer(...))`
  from the `errors[]` array and instead do
  ```ts
  const trSb = assembleSuperbuffer({ /* same args */ });
  const trBuffer = emitSuperbufferAssembly('ir', feature.id, trSb, 'test-rha');
  ```
  then push `trBuffer` into the errors array.

Also update the case-relay (runner.ts ~L5729) and born-red-surgery (~L2438) sites for parity
(they emit but don't share the helper; make them share).

**Acceptance:** `grep -n "formatSuperbufferForPrompt(assembleSuperbuffer" src/tier1/runner.ts`
returns zero hits. Every superbuffer assembly at any site emits exactly one
`superbuffer: assembled (...)` line.

## 92b — legible per-attempt outcomes (doctrine 7)

Replace the try/catch inner block at runner.ts ~L2622-2679 with the following structured
per-attempt path. Emit lines exactly as shown. Increment the paired counter.

```ts
for (let attempt = 0; attempt < PERMUTATION_TEMPS.length; attempt++) {
  metrics.testRhaDraws++;
  const temp = PERMUTATION_TEMPS[attempt]!;
  const label = `try ${attempt + 1}/${PERMUTATION_TEMPS.length}, t=${temp}`;
  let fixed: Awaited<ReturnType<typeof opts.llm.fixTestCases>>;
  try {
    fixed = await runLlmAction({ /* unchanged args */ });
  } catch (err) {
    metrics.testRhaDrawThrew++;
    const msg = err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200);
    emit('ir', feature.id, `test-rha: draw threw (${label}) — ${msg}`);
    caseDisposition.set(caseHit.name, 'attempt-threw');
    continue;
  }
  const body = fixed.cases[0]?.body ?? '';
  if (!body.trim() || !acceptCaseBody(body)) {
    metrics.testRhaDraftRejected++;
    const reason = !body.trim() ? 'empty body' : 'failed acceptCaseBody';
    emit('ir', feature.id, `test-rha: draft rejected (${label}) — ${reason}`);
    caseDisposition.set(caseHit.name, 'attempt-rejected');
    continue;
  }
  parts = {
    imports: parts.imports,
    cases: parts.cases.map(c => (c.name === caseHit.name ? { ...c, body } : c)),
  };
  const asm = assembleTestFile(feature, parts.imports, parts.cases);
  content = wireSuiteScaffoldImports(asm, feature.testFile, spec).content;
  emit('ir', feature.id, `test-rha: draft accepted (${label})`);
  brRun = await opts.runSuite(stubs, { path: feature.testFile, content });
  br = scoreBornRed(brRun);
  sick = partitionSick();
  const stillSick = sick.some(s => caseForFailure(s.caseName)?.name === caseHit.name);
  if (!stillSick) {
    metrics.testRhaDraftAcceptedCleared++;
    metrics.testRhaCaseCleared = (metrics.testRhaCaseCleared ?? 0) + 1;
    emit('ir', feature.id, `test-rha: case "${caseHit.name}" cleared`);
    caseDisposition.set(caseHit.name, 'case-cleared');
    break;
  }
  metrics.testRhaDraftAcceptedStillSick++;
  emit('ir', feature.id, `test-rha: case "${caseHit.name}" still sick — continuing ladder`);
}
// Ladder exhausted without cleared.
if (caseDisposition.get(caseHit.name) !== 'case-cleared') {
  metrics.testRhaCaseExhausted++;
  emit('ir', feature.id,
    `test-rha: case "${caseHit.name}" exhausted after ${PERMUTATION_TEMPS.length} attempts`);
  caseDisposition.set(caseHit.name, 'case-exhausted');
}
```

**Acceptance:** a mock scenario (LLM throws on attempt 2, accepts on attempt 3) produces exactly
these four lines in order: `draw dispatched or draft accepted (try 1)`, `draw threw (try 2)`,
`draft accepted (try 3)`, `case "<name>" cleared`.

## 92c — no silent skip (doctrine 5, doctrine 7)

Before the guarded gate at runner.ts ~L2620, declare `const caseDisposition = new Map<string,
string>();` (per test-rha invocation). Then evaluate the two skip preconditions explicitly:

```ts
if (!caseHit) {
  emit('ir', feature.id,
    `test-rha: case "${target.caseName}" skipped — no case handle`);
  metrics.testRhaSkippedNoHandle++;
  caseDisposition.set(target.caseName, 'skipped-no-handle');
  continue;
}
if (redrawnThisPass.has(caseHit.name)) {
  emit('ir', feature.id,
    `test-rha: case "${caseHit.name}" skipped — already redrawn this pass`);
  metrics.testRhaSkippedAlreadyRedrawn++;
  caseDisposition.set(caseHit.name, 'skipped-already-redrawn');
  continue;
}
redrawnThisPass.add(caseHit.name);
```

Rename `redrawnAtGate` → `redrawnThisPass` and RESET it at the top of each freeze-gate pass
(inside the `for (let trPass = 0; trPass < 2 && sick.length; trPass++)` loop, immediately
after entering the pass body — currently the Set is declared OUTSIDE that loop).

At the drop-loop (~L2687-2695), replace the unconditional `problems.push(...unrepairable...)`
with:

```ts
for (const nm of dropNames) {
  const disp = caseDisposition.get(nm) ?? 'case-exhausted';
  if (disp === 'case-exhausted') {
    problems.push(`${feature.id}: case "${nm}" dropped at freeze-gate — sick-red unrepairable`);
    emit('ir', feature.id, `test-rha: case "${nm}" dropped at freeze-gate — sick-red unrepairable`);
    caseDisposition.set(nm, 'case-dropped-unrepairable');
  } else {
    // The case reached a terminal disposition upstream; do not misreport.
    problems.push(`${feature.id}: case "${nm}" dropped at freeze-gate — ${disp}`);
    emit('ir', feature.id, `test-rha: case "${nm}" dropped at freeze-gate — ${disp}`);
  }
}
```

**Acceptance:** a mock where `caseForFailure` returns undefined for one target logs
`skipped — no case handle` and the drop loop DOES NOT push that case as "sick-red unrepairable".

## 92d — honest ladder label (doctrine 12)

Immediately before the per-case loop starts (inside the trPass loop, after the sick partition),
emit once per test-rha invocation:

```ts
emit('ir', feature.id,
  `test-rha: miser ladder [K=${PERMUTATION_TEMPS.length}] (stops on first cleared redraw)`);
```

Keep `try N/K` on per-attempt lines. **Acceptance:** the label appears exactly once per
test-rha invocation.

## 92e — new counters wired in metrics (doctrine 11)

In `src/tier1/runner.ts`, extend the `Tier1Metrics` interface with the new counters listed
above. Initialize them all to 0 in the `metrics` object literal. Update
`src/tier1/vanilla.ts` and `tests/tier1-set3.test.ts` (the metrics fixtures) with the same
zeroed fields.

## 92f — conservation test (doctrine 7, doctrine 11)

New file `tests/tier1-set92-testrha-conservation.test.ts`. Uses a class-based `LlmClient`
double. Two scenarios:

1) Scenario A — throw on attempt 2, accept on attempt 3, cleared on re-measurement.
   Assert final metrics satisfy:
   ```
   testRhaDraws === testRhaDrawThrew + testRhaDraftRejected +
     testRhaDraftAcceptedCleared + testRhaDraftAcceptedStillSick
   ```
   Assert `testRhaSkippedNoHandle === 1` when `caseForFailure` is stubbed to return `undefined`
   for one specific case name.
   Assert the log contains `superbuffer: assembled (...)` at test-rha stage exactly
   `testRhaSuperbufferAssemblies` times.

2) Scenario B — LLM always throws. Assert `testRhaDraws === testRhaDrawThrew` and
   `testRhaCaseExhausted === casesEntered`. Assert no line matches
   `/backing off|retrying in|not re-attempted|retired/`.

## 92g — mutation coverage (doctrine 15)

New file `tests/tier1-set92-testrha-mutation.test.ts`. Wrapper harness that intercepts
`emit(...)` and can suppress any one specific line by regex. For each of the six new emit sites
(draw threw / draft rejected / draft accepted / case cleared / case still sick / skipped-*),
run the equivalent 92f scenario with that emit suppressed and assert 92f's conservation test
fails. This is the born-red proof that these emits are load-bearing.

### Gates for SET 92
`npx tsc --noEmit` clean; `npx tsc -p tsconfig.web.json --noEmit` clean; `npx vitest run tests`
all green including 92f/92g; `npx vite build --config vite.tier1.config.ts` REQUIRED;
`npm run build:web` ends `check-web-boot OK`; bump `extension/manifest.json`; standing duties
A/A2/A3/B.

---

# ACTIVE — SET 93. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# Fixes the born-red vacuous-after-surgery defect that killed F1/F3/F6 in run 78e36cc1.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- A stub-derived throw-expecting case (`expect(() => new X()).toThrow('stub: not implemented')`)
  is NOT retained as sick-red. It is dropped at born-red with disclosure, so surgery is not asked
  to convert a fundamentally unsatisfiable contract into a behavioral one.
- A vacuous case that surgery cannot fix (after K attempts) is DROPPED WITH DISCLOSURE, not
  used to refuse the whole suite freeze. Combined with SET 91b partial-certified support, a
  feature whose ONLY case died at surgery lands with 0 behavioral certs and status
  `partial-certified` iff any structural anchor exists, or `refused` otherwise.
- The 78e36cc1 F1/F3/F6 outcome (`suite still vacuous after surgery — freeze refused`) becomes
  observable as either `certified` (real repair worked), `partial-certified` (only anchor
  survived), or `refused` (nothing survived) — never a silent failure of the whole feature.

Work unit and identity:
- Work unit = ONE born-red surgery attempt on ONE vacuous case. Ladder work unit = one case's
  K-attempt run.
- Case identity = `${feature.id}#${case.name}`. Doctrine 4.

Canonical state and owner:
- Born-red decides which cases enter surgery. Canonical state: the `vacuousNames` set at
  runner.ts ~L2410. Owner: `bornRed.ts` for classification, runner.ts for the surgery loop.
- The change: `bornRed.ts` exports a new predicate `isStubDerivedThrowCase(body, feature, spec)`
  and the runner uses it to EXCLUDE such cases from `vacuousNames`. Instead of entering surgery,
  those cases are dropped at born-red with the disclosure line
  `born-red: <n> stub-derived throw case(s) dropped — cannot be de-vacuum'd (stub is the contract)`
  and their names are added to a `droppedAtBornRed: Set<string>` that removes them from `parts.cases`.

Path inventory:
- triggers: `!hasRedCase(brRun)` branch at runner.ts ~L2398.
- writers: `parts.cases` mutation post-surgery ~L2467; `vacuousNames` at ~L2411.
- readers: subsequent SET 89 gateway (`commitFrozenSuite`) sees the reduced `parts.cases`.
- fallbacks/reconstructions: none — this is a pre-freeze operation.

Deterministic predicate for `isStubDerivedThrowCase`:
Return true iff ALL of:
1. `analyzeThrowExpectation(body).hasThrowAssertion === true` (SET 89's existing analyzer).
2. `analyzeThrowExpectation(body).isSoleAssertion === true` (the throw is the whole contract).
3. `analyzeThrowExpectation(body).negated === false` (not a `not.toThrow` shape scaffold).
4. `isStubDerivedLiteral(analyzeThrowExpectation(body).expectedLiteral, spec, feature) === true`
   (SET 89's existing check).

The four together mean: this case is a `.toThrow("stub: not implemented")`-family assertion
whose entire contract is "the stub throws." That contract is trivially satisfied by the stub
existing and impossible to satisfy when the stub is replaced by a real implementation. Surgery
cannot help — the case must be dropped.

Failure dispositions:
- Per case: `dropped-stub-derived-throw` (new; emitted before the vacuous → surgery gate).
- No changes to surgery-loop dispositions.

Metrics and conservation equations:
- Add: `bornRedStubDerivedDrops`.
- Invariant: `bornRedStubDerivedDrops` is bounded above by the count of cases in `parts.cases`
  before the born-red measurement.

Causal hypothesis:
- 78e36cc1's F1/F3/F6 all logged
  `born-red: 1 throw-expecting case(s) retained (undecidable vs throwing stubs — verified rejection clade)`
  followed by `born-red: N vacuous case(s) → surgery` and then
  `born-red: suite still vacuous after surgery — freeze refused`. The throw-expecting retention
  and the vacuous surgery are TWO DIFFERENT CASES in the same suite; surgery attempts the
  vacuous ones and fails because the ONLY red assertion in the suite is the stub-derived throw,
  which cannot be `hasRedCase`d against a real implementation.
- SET 89a (from the original SET 89 plan) was supposed to handle this; the implementation
  retained rather than dropped. This set is the correct implementation.

Falsifier:
- Predicted post-fix observation on a spec where feature `F1` has one case
  `expect(() => new Model()).toThrow('stub: not implemented')`:
  - Log contains `born-red: 1 stub-derived throw case(s) dropped — cannot be de-vacuum'd`.
  - `bornRedStubDerivedDrops === 1`.
  - The case is NOT sent to surgery. If it was, the log would contain
    `born-red: N vacuous case(s) → surgery` naming that case.
- Fix insufficiency: any run where a case whose only assertion is
  `.toThrow('stub: not implemented')` reaches surgery. Grep the log for the surgery event
  naming a case whose body matches the predicate above.

Production-shaped proof:
- Vitest fixture in `tests/tier1-set93-born-red-stub-derived.test.ts` — inputs a case body
  matching the predicate, asserts drop-at-born-red + counter increment + no surgery event.
- Regression against the 78e36cc1 spec: rerun (either mock-scoped or on the deployed vault)
  should show F1/F3/F6 with either partial-certified or refused (based on remaining cases),
  NOT `suite still vacuous after surgery — freeze refused`.

Terminal certification:
- `metrics.bornRedStubDerivedDrops` reflected in the run report and the export log. A run with
  ZERO stub-derived throw cases is fine; the metric merely can't misfire.

Release blockers:
- Any case matching the predicate reaching surgery.
- Any regression in tests that previously asserted throw-expecting retention (they must be
  updated to assert drop with the new disclosure — this is a semantics change, exactly like
  SET 91 updated the SET 89 tests).

---

Executable instructions (Cursor):

## 93a — new predicate in `src/tier1/bornRed.ts`

Add:

```ts
import type { FeatureNode, FeatureSpec } from '../core/types';
import { analyzeThrowExpectation, isStubDerivedLiteral } from './caseCertification';

/**
 * SET 93 — a case is "stub-derived throw" iff its ENTIRE contract is that
 * a throwing stub throws. Such a case is trivially green while the stub
 * exists and impossible to satisfy once the stub is replaced by a real
 * implementation. Surgery cannot rescue it — drop at born-red.
 */
export function isStubDerivedThrowCase(
  body: string,
  feature: FeatureNode,
  spec: FeatureSpec,
): boolean {
  const ta = analyzeThrowExpectation(body);
  if (!ta.hasThrowAssertion) return false;
  if (!ta.isSoleAssertion) return false;
  if (ta.negated) return false;
  return isStubDerivedLiteral(ta.expectedLiteral, spec, feature);
}
```

## 93b — drop stub-derived throws at born-red in `runner.ts`

Locate the `!hasRedCase(brRun)` branch (runner.ts ~L2398). Immediately after the
`camoCount` computation and BEFORE the `vacuousNames` computation, insert:

```ts
// SET 93 — a stub-derived throw case cannot be de-vacuum'd: its contract IS
// "the stub throws," which becomes permanently false when a real impl lands.
// Drop such cases at born-red with disclosure; do not send them to surgery.
const stubDerivedDrops = parts.cases.filter(c =>
  !brRun.failures.some(f => f.name.includes(c.name))
  && isStubDerivedThrowCase(c.body, feature, spec));
if (stubDerivedDrops.length) {
  metrics.bornRedStubDerivedDrops += stubDerivedDrops.length;
  emit('ir', feature.id,
    `born-red: ${stubDerivedDrops.length} stub-derived throw case(s) dropped — cannot be de-vacuum'd (stub is the contract)`);
  const dropSet = new Set(stubDerivedDrops.map(c => c.name));
  parts = { imports: parts.imports, cases: parts.cases.filter(c => !dropSet.has(c.name)) };
  if (!parts.cases.length) {
    return { status: 'deferred', reason: 'all cases stub-derived-throw dropped at born-red' };
  }
  // Re-assemble and re-measure so downstream sees the reduced suite.
  const asm = assembleTestFile(feature, parts.imports, parts.cases);
  content = wireSuiteScaffoldImports(asm, feature.testFile, spec).content;
  brRun = await opts.runSuite(stubs, { path: feature.testFile, content });
  br = scoreBornRed(brRun);
}
```

Then let the existing `vacuousNames` computation proceed on the reduced set of cases.

Import `isStubDerivedThrowCase` at the top of `runner.ts`.

## 93c — counter wiring

Extend `Tier1Metrics` with `bornRedStubDerivedDrops: number`. Initialize to 0 in `metrics`
literal. Update `src/tier1/vanilla.ts` and `tests/tier1-set3.test.ts` fixtures.

## 93d — regression test

New file `tests/tier1-set93-born-red-stub-derived.test.ts`:

```ts
import { describe, expect, it } from 'vitest';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import type { FeatureSpec, LlmClient } from '../src/core/types';

function specWithStubDerivedThrow(): FeatureSpec {
  return {
    title: 't', prompt: 'p',
    features: [{
      id: 'F1', name: 'Model', description: 'Model class', parentId: null,
      deps: [], files: ['src/model.ts'], testFile: 'tests/model.test.ts',
      edgeCases: [], envVars: [],
      exports: [{ file: 'src/model.ts', symbol: 'Model' }],
      stubs: [{ path: 'src/model.ts',
        content: `export class Model { constructor() { throw new Error('stub: not implemented'); } }` }],
    }],
  } as unknown as FeatureSpec;
}

class ClassLlm {
  async encodeFeatures() { return specWithStubDerivedThrow(); }
  async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; }
  async fixTestCases(_s: unknown, _f: unknown, req: { name: string }[]) {
    const n = req[0]!.name;
    return { cases: [{ name: n, kind: 'behavior' as const,
      body: `expect(() => new Model()).toThrow('stub: not implemented');` }] };
  }
  async generateImplementation() {
    return [{ path: 'src/model.ts',
      content: `export class Model {}` }];
  }
  async repair() {
    return [{ path: 'src/model.ts',
      content: `export class Model {}` }];
  }
}

describe('SET 93 — stub-derived throw cases drop at born-red', () => {
  it('never enters surgery; drop line emitted; counter increments', async () => {
    const res = await runTier1Job({
      prompt: 'p',
      llm: new ClassLlm() as unknown as LlmClient,
      runSuite: makeEvalSuiteRunner(),
    });
    const msgs = res.log.map(e => e.message);
    expect(msgs.some(m => /born-red: 1 stub-derived throw case\(s\) dropped/.test(m))).toBe(true);
    expect(res.metrics.bornRedStubDerivedDrops).toBeGreaterThanOrEqual(1);
    expect(msgs.some(m => /born-red: .* vacuous case\(s\) → surgery/.test(m))).toBe(false);
  });
});
```

### Gates for SET 93
Same as SET 92. Order relative to 92: 92 lands first (owner-boundary work is unrelated but
clean-slate first), then 93. Both should land before the next live run.

---

# ACTIVE — SET 94. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# Compute repair: watcher digest-input-hash cache + watcher-fill pipelining + substrate cap.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- Watcher LLM calls that would compute the same synthesis over an unchanged digest are
  eliminated, not rate-limited: same input → reuse the last reading, emit
  `digest unchanged since node <N> — reading reused (advisory)`. Doctrine 22 (fix survives
  reconstruction) via canonical-state identity; doctrine 16 (no invented thresholds) because
  the cache is decided by input equality, not by a count or a timer.
- The `ir → watch → ir` sandwich spacing that adds 500ms per draw is collapsed to a single
  transition. Provider stage boundary is preserved (encode/build/etc) but same-phase
  synthesis is not treated as a stage transition.
- Substrate handoff page is capped at input by most-recent-first slice so it cannot crowd out
  `union` and `corpus` in the packing loop.
- Watcher does NOT fire between failed generations. Failure nodes accumulate deterministic
  corpse-rejection evidence; they do not warrant re-synthesizing the world for the LLM. A
  deterministic ledger-shape predicate (`hasWorldAdvancedSince`) gates re-firing — never a
  threshold on how much time or how many nodes.
- The metasupercache is CORPSE-REJECTION-ONLY. The meta-watcher LLM call and the `meta`
  superbuffer page are DELETED entirely. Metasupercache never informs an LLM of anything;
  it only precludes bad candidate matches from being validated by one. Each case has 2
  triplets × 3 = 6 draw slots per run; at most 5 can be corpse-rejected before the class is
  structurally exhausted — an assertion (`corpseRejectionCeilingBreaches`) fires as a
  diagnostic beacon if the ceiling is breached, meaning an upstream defect is re-issuing
  the class beyond the ladder budget.

Work unit and identity:
- Cache entry = `(featureId, stage, digestHash)`. Same triple → same reading. Digest hash is
  deterministic (`buildEpochDigest` output is a pure function of the ledger + artifact).

Canonical state and owner:
- Owner: `synthesizeForDraw` in `runner.ts` (~L1077-1136). Caller sites unchanged. Cache
  lives in a module-level Map on `watch: { reading, health, cache }`.

Path inventory:
- triggers: five callers (fill L1706, fallback fill L1987, build L3043, repair L4566,
  case-relay L5729). None change.
- writers: `watch.reading` and now `watch.cache`. Sole writer: `synthesizeForDraw`.
- readers: `formatUnionPage` (unchanged), the fill-path superbuffer assembly (unchanged).

Deterministic predicate:
1. Compute `digest = buildEpochDigest(...)` (unchanged, pure).
2. Compute `hash = deterministicHash(JSON.stringify(digest))` — cheap, non-crypto, string-hash.
3. If `watch.cache.has((featureId, stage, hash))` → reuse the cached reading, increment
   `metrics.watcherCacheHits`, emit `watcher synthesis @ <who>: digest unchanged since node <N>
   — reading reused (advisory)`, return the union page from the cached reading. Do NOT call
   the engine.
4. Else → call the engine as today; on success, store the reading under the key; on failure,
   do NOT store anything (a failure has no legitimate reading to cache).

The cache is UNBOUNDED (max ~1 entry per unique digest); a run touches perhaps 100-300 unique
digests. Bounding is unnecessary.

Failure dispositions:
- New per-call disposition: `read-from-cache`. All other paths unchanged.

Metrics and conservation equations:
- Add: `watcherCacheHits`, `watcherCacheMisses`.
- Invariant: `watcherSyntheses = watcherReadings + watcherCacheHits + watcherEngineFailures +
  <unreadable>` (approximate; exact partition unchanged).

Causal hypothesis:
- 78e36cc1 had 68 watcher calls, each ~5-7s. Consecutive draws for the same feature see the
  ledger move by 1-3 nodes; the digest is largely unchanged; the model returns almost the same
  reading. The wasted cost is ~4-5 minutes of wall time and ~66k tokens. Caching by input
  identity is doctrine-clean (no threshold) and eliminates the redundant call class.

Falsifier:
- Predicted post-fix observations on a rerun of the same spec:
  - `watcherCacheHits ≥ 20` (was 0).
  - `watcherWorldStaleSkips` roughly equal to the count of all-failed permutation-select
    batches (typically 5-15 in a spec of this size).
  - Total wall time drops by ≥ 40% for the IR phase (94a + 94d combined).
  - At least one log line matches `digest unchanged since node <N> — reading reused`.
  - At least one log line matches `skipped — no world advancement since node <N>`.
  - `grep -rn "metaWatcher\|summarizeMeta" src/` returns zero hits after deletion.
  - The `meta` slot is gone from `SUPERBUFFER_PAGES.repair`; no `meta:<N>` line appears in
    the `superbuffer: assembled` events for repair-stage draws.
  - Corpse-rejection counter unchanged relative to today (the deterministic mechanism is
    exactly what 94e preserves — deleting the LLM-facing rendering does not weaken it).
- Fix insufficiency:
  - `watcherCacheHits === 0` on a run with > 20 watcher triggers. Hash or lookup is wrong.
  - `watcherWorldStaleSkips === 0` on a run with > 5 all-failed batches. Predicate is not
    firing; probably `lastSyntheticStep` is not being updated after success.
  - Any reference to `metaWatcher`, `summarizeMeta`, `formatMetaPage`, or the `'meta'` page
    slot survives in `src/`. 94e was not fully applied.

Production-shaped proof:
- Unit test on `synthesizeForDraw`-like scenario: call it twice with the same ledger state,
  assert the engine is called exactly once.
- Integration test on `runTier1Job` with a counting `watchEpoch` — assert
  `engineCalls < watcherSyntheses`.

Terminal certification:
- Metrics conservation asserted by `tests/tier1-set94-watcher-cache.test.ts` (below).

Release blockers:
- Any case where the same `(featureId, stage, digestHash)` triple triggers two engine calls.

---

Executable instructions (Cursor):

## 94a — digest cache in `synthesizeForDraw`

At the top of `runTier1Job` (near the `watch` state declaration ~L1073), extend the shape:

```ts
const watch: {
  reading: ReturnType<typeof parseWatchReport>;
  health: AgentHealth;
  cache: Map<string, ReturnType<typeof parseWatchReport>>;
} = { reading: null, health: createAgentHealth(), cache: new Map() };

const digestCacheKey = (stage: string, featureId: string | null, digest: object): string => {
  // Cheap deterministic string hash; identity of the JSON representation.
  const json = JSON.stringify(digest);
  let h = 5381 >>> 0;
  for (let i = 0; i < json.length; i++) h = ((h << 5) + h + json.charCodeAt(i)) >>> 0;
  return `${stage}|${featureId ?? '_'}|${h.toString(36)}|${json.length}`;
};
```

Inside `synthesizeForDraw`, after `const digest = buildEpochDigest(...)`:

```ts
const cacheKey = digestCacheKey(stage, featureId, digest);
const cached = watch.cache.get(cacheKey);
if (cached !== undefined) {
  metrics.watcherCacheHits++;
  // Prefer the cached reading over `watch.reading` when returning the union page
  // for THIS draw, but do not clobber `watch.reading` (which reflects the last
  // fresh reading and is used by other emit paths).
  const localReading = cached;
  emit(stage, featureId,
    `watcher synthesis @ ${stage}${featureId ? `/${featureId}` : ''}: digest unchanged since node ${digest.nodes} — reading reused (advisory)`);
  return formatUnionPage(digest, localReading);
}
metrics.watcherCacheMisses++;
```

On the success branch (after `noteSuccess(watch.health)` and `disposition = 'read'`),
immediately after `watch.reading = fresh`, add:

```ts
watch.cache.set(cacheKey, fresh);
```

Do NOT cache on failure (`unreadable` returns null, cache remains untouched).

**Acceptance:** rerun the mock scenario twice with an identical ledger — the engine gets called
once, the second `synthesizeForDraw` returns from cache with the `reused` line.

## 94b — collapse the `ir → watch → ir` transition (compute)

`STAGE_TRANSITION_SPACING_MS` is applied whenever `op` changes between calls. For synthesis
calls (`op === 'watch'`) that sit inside the IR phase, the "phase transition" is spurious —
we're in the same algorithm phase, we're just consulting the advisory agent. Introduce an
opt-in bypass:

In `src/core/localLlm.ts`, extend `DrawOpts` with `bypassStageSpacing?: boolean`. In the
`call<T>` body, wrap the existing stage-transition spacing:

```ts
if (this.lastOp != null && this.lastOp !== op && !opts?.bypassStageSpacing) {
  this.emit(`stage-transition spacing applied (${this.lastOp}→${op})`);
  await sleep(STAGE_TRANSITION_SPACING_MS);
}
```

In `src/core/localLlm.ts`, update `watchEpoch` to pass `bypassStageSpacing: true`:

```ts
async watchEpoch(digestPrompt: string, opts?: DrawOpts): Promise<string> {
  const out = await this.call<...>(
    'watch', null, WATCH_SYSTEM, digestPrompt, WATCH_SCHEMA, 'state of the union',
    { ...opts, temperature: opts?.temperature ?? 0.2, bypassStageSpacing: true },
  );
  ...
}
```

**Acceptance:** rerun the 78e36cc1 mock — the log contains no `stage-transition spacing
applied (ir→watch)` or `stage-transition spacing applied (watch→ir)` lines.

## 94c — substrate handoff page cap (doctrine 11)

In `src/tier1/substrateHandoff.ts`, in `formatSubstrateHandoffPage`, cap output at 800 chars
by taking the MOST RECENT attempts first. Attempts are stored in reverse-chronological order
after this change; the formatter takes the first N until the char budget is exhausted, then
appends `… (N older attempts elided)` if any were truncated.

The current formatter iterates attempts oldest-first; add a sort by attempt sequence descending
before rendering, then respect an 800-char cap:

```ts
export function formatSubstrateHandoffPage(h: SubstrateHandoff | null | undefined): string {
  if (!h || !hasSubstrateHandoffHistory(h)) return '';
  const CAP = 800;
  const lines: string[] = ['Page substrate — handoff history (most recent first):'];
  // Reverse-chronological: the packer already has spec/contract in front, so
  // truncating oldest first preserves the most decision-relevant evidence.
  const attempts = [...(h.attempts ?? [])].reverse();
  for (const a of attempts) {
    const next = `- ${a.handler} → ${a.status}${a.reason ? ` (${a.reason})` : ''}`;
    if (lines.join('\n').length + next.length + 1 > CAP) {
      const elided = attempts.length - (lines.length - 1);
      lines.push(`… (${elided} older attempt(s) elided at cap)`);
      break;
    }
    lines.push(next);
  }
  return lines.join('\n').slice(0, CAP);
}
```

**Acceptance:** feed a synthetic handoff with 50 attempts; the rendered page ≤ 800 chars
AND contains an `elided at cap` marker.

## 94d — no watcher between failed generations (Andy, 2026-07-28: "we capture dead bodies in the metasupercache … dead bodies don't inform generation as effectively and shouldn't be injected")

Andy's insight: failure evidence enters the DETERMINISTIC corpse-rejection cache (`runDrawHashes`,
`metaCorpseHashes`) — that is where it exerts causal force, by refusing repeat draws without
execution. Feeding failure nodes back into the LLM watcher is a separate mechanism that costs
tokens and produces weaker signal (the model is bad at learning from anti-examples).

**Predicate** — `hasWorldAdvancedSince(lastSyntheticNodeStep)`:
Since the ledger step at which the last watcher synthesis fired, does ANY subsequent row have
a type in `{ test, code, resolution, spec, lesson }` OR a status in `{ pass, healed }`?
- Yes → world advanced; the watcher fires as today.
- No → world only accumulated failure evidence; the watcher SKIPS with the line
  `watcher synthesis @ <who>: skipped — no world advancement since node <N> (deterministic corpse cache carries failure evidence)`
  and returns the LAST cached union page (from `watch.reading`).

This is NOT a threshold under doctrine 16 — the criterion is a deterministic state predicate
on the ledger (a shape membership test), not a count or a timer. It composes cleanly with
94a's digest cache: on world-advancement, 94a's cache first-hits when the digest is truly
unchanged; on non-advancement, 94d short-circuits earlier without even computing the digest.

**Implementation** (`src/tier1/runner.ts`, inside the `watch` state and `synthesizeForDraw`):

```ts
const watch: {
  reading: ReturnType<typeof parseWatchReport>;
  health: AgentHealth;
  cache: Map<string, ReturnType<typeof parseWatchReport>>;
  /** SET 94d — step of the ledger at the last successful watcher call. */
  lastSyntheticStep: number;
} = { reading: null, health: createAgentHealth(), cache: new Map(), lastSyntheticStep: 0 };

// SET 94d — 'lesson' is EXCLUDED: the watcher itself records lesson nodes on success,
// so including it here would create a feedback loop where every successful watcher call
// triggers 'world advanced' for the next trigger. Live log 08d1e099 shows 103 lesson nodes
// mostly from watcher successes; that must not count as world advancement.
const ADVANCING_TYPES = new Set<LedgerNodeType>(['test', 'code', 'resolution', 'spec']);
const ADVANCING_STATUSES = new Set(['pass', 'healed']);

const hasWorldAdvancedSince = (rows: readonly LedgerRow[], step: number): boolean => {
  for (const r of rows) {
    if (r.step <= step) continue;
    if (ADVANCING_TYPES.has(r.type)) return true;
    if (r.status && ADVANCING_STATUSES.has(r.status)) return true;
  }
  return false;
};
```

At the top of `synthesizeForDraw`, immediately after `if (rows.length <= 1) return null;`:

```ts
if (watch.lastSyntheticStep > 0 && !hasWorldAdvancedSince(rows, watch.lastSyntheticStep)) {
  metrics.watcherWorldStaleSkips++;
  emit(stage, featureId,
    `watcher synthesis @ ${stage}${featureId ? `/${featureId}` : ''}: skipped — no world advancement since node ${watch.lastSyntheticStep} (deterministic corpse cache carries failure evidence)`);
  // Return the last cached union page (if any) so downstream still gets an
  // advisory union; if there is none yet, return null and let the caller
  // proceed without one.
  const digest = buildEpochDigest({ epoch: stage, rows, artifact, featureId });
  return formatUnionPage(digest, watch.reading);
}
```

On EVERY successful synthesis (both engine-fresh and cache-hit paths), update:

```ts
watch.lastSyntheticStep = rows[rows.length - 1]?.step ?? watch.lastSyntheticStep;
```

Add `watcherWorldStaleSkips: number` to `Tier1Metrics` and initialize to 0. Same for the
fixture files.

**Acceptance:** on a mock scenario where the ledger accumulates 5 `error` nodes between two
watcher triggers, the second trigger emits `skipped — no world advancement since node <N>`
and the engine is NOT called. Conversely, on a mock where a `code` node lands between
triggers, the second trigger fires the engine as usual.

**Falsifier:** rerun a live spec. Compare `watcherWorldStaleSkips` count against the count of
`permutation-select: … picked … (0/1 passing)` events. The former should be roughly equal to
or greater than the latter (every all-failed batch that precedes a new batch skips the
watcher). If `watcherWorldStaleSkips === 0` on a run with > 5 all-failed batches, the
predicate is not firing.

## 94e — DELETE the meta-watcher LLM path entirely (Andy, 2026-07-28)

Andy's rule: *"metasupercache should never inform an LLM of anything. It should only be used
to preclude bad candidate matches from being validated by an LLM."* The metasupercache stays
as CORPSE-REJECTION-ONLY (deterministic hash set that skips the LLM call). Everything that
existed to render metasupercache content INTO an LLM prompt is deleted.

**What survives (unchanged):**
- `metaCorpseHashes: Set<string>` — the cross-run reject cache
- `runDrawHashes: Set<string>` — the within-run reject cache
- `noteMetaCandidate()` — appends the hash for future corpse detection
- `metaRecords[]` — raw candidate records at whatever lifetime the host persists
- The `corpse detected [<class>]: identical draw rejected without execution` emit — this is a
  LOG line reporting the CPU-side rejection, not model content, so it stays

**What is DELETED:**
- File `src/tier1/metaWatcher.ts` in its entirety — `synthesizeMeta`, `summarizeMeta`,
  `buildMetaDigest`, `formatMetaPage`, `formatMetaWatchEvent`, `MetaReport`, `MetaDigest`,
  `META_PAGE_CAP`, `parseMetaReport`, `metaWatchPrompt`.
- All imports of the above in `src/tier1/runner.ts` (search: `metaWatcher`).
- `synthesizeMeta` state block and all three call sites in `runner.ts` (search:
  `synthesizeMeta`, currently at ~L912-940 and ~L4569).
- Counters: `metaWatcherSyntheses`, `metaWatcherReadings`, `metaWatcherEngineFailures`.
- Method `summarizeMeta` and constants `META_WATCH_SYSTEM`, `META_SCHEMA` in
  `src/core/localLlm.ts`.
- The `'meta'` slot in `SUPERBUFFER_PAGES.repair` (`src/tier1/superbuffer.ts`).
- All `meta-watcher: …` log lines and their event-tone rules.

**Falsifier:** grep the tree after deletion:
- `grep -rn "metaWatcher" src/` returns zero hits
- `grep -rn "summarizeMeta" src/` returns zero hits
- `grep -rn "'meta'" src/tier1/superbuffer.ts` returns zero hits inside `SUPERBUFFER_PAGES`
- Full suite + typechecks stay green

**Additional assertion — corpse-rejection ceiling:** each case has 2 triplets × 3 = 6 draw
opportunities per run. At most 5 of those can be corpse-rejected before we treat the class
as structurally exhausted. Add a runtime assertion inside the corpse-detection path:

```ts
// After incrementing the per-case corpse count
if (perCaseCorpseCount[caseName] > 5) {
  emit(stage, feature.id,
    `corpse-ceiling exceeded: case "${caseName}" hit ${perCaseCorpseCount[caseName]} rejections in one run — upstream is re-issuing this class more aggressively than the ladder-shape justifies`);
  // Do NOT throw — the metasupercache is doing its job, the defect is upstream.
  // This line is a diagnostic beacon.
}
```

New counter: `corpseRejectionCeilingBreaches`. Zero on a healthy run. Nonzero = an upstream
defect where a case is being re-drawn beyond its 6-slot budget.

## 94f — regression tests

New file `tests/tier1-set94-watcher-cache.test.ts`. Assertions listed in falsifier.
New file `tests/tier1-set94-substrate-cap.test.ts`. Assertions listed in 94c acceptance.
New file `tests/tier1-set94-world-advance.test.ts`. Assertions listed in 94d acceptance.
New file `tests/tier1-set94-meta-deleted.test.ts`. Asserts the four falsifier greps in 94e all return zero hits from a compiled-tree fixture, plus a runtime test that a corpse-rejection event fires WITHOUT any `meta-watcher:` line following it.

### Gates for SET 94
Same as SET 92. Order: 92 → 93 → 94. All three should land before we ship a live run for the
big architectural move (95).

---

# ACTIVE — SET 95. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# The transmission stops doing engine work. Typed-hole skeletons enforced with `overreach|body`
# rejection. This is the biggest algorithmic move on the queue — Cursor should execute it in
# small increments (95a → 95b → 95c → 95d) and ship each as its own vault release.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- Every LLM draw at the case-body level receives a HOLE-ONLY prompt: the CPU has already
  emitted the `describe/it` wrapper, the imports, the mocks, and any deterministic scaffolding.
  The model returns only the assertion permutation. Response tokens drop from ~800-1200 to
  ~80-200 per case.
- Every LLM draw at the file-body level receives a signature-and-imports-only stub, with the
  function bodies as `⟨HOLE⟩` markers. The model returns only the bodies. Response tokens drop
  from ~1500-2500 to ~300-500 per file.
- A response that EXCEEDS the sum of hole sizes by more than 1.5× is REJECTED as
  `overreach|body` — a new finding class. Under doctrine 12, the transmission has a work unit
  and any output beyond it is a boundary violation.

Work unit and identity:
- Case-body work unit = ONE hole per case (`⟨ASSERTION⟩`). Frame identity =
  `assembleCaseFrame(feature, case, spec)` → deterministic bytes with a single marker.
- File-body work unit = ONE hole per exported function (`⟨BODY:<symbol>⟩`). Frame identity =
  `assembleImplFrame(feature, spec)` → deterministic bytes with N markers.

Canonical state and owner:
- Frame owner: new `src/tier1/frame.ts` module with `assembleCaseFrame` and `assembleImplFrame`
  as sole public exports. NO OTHER module reproduces boilerplate. If a fill site currently
  builds its own `describe(...)` string, it stops.
- Splice owner: same module, `spliceHole(frame, holeName, body)` → returns the frame with the
  named hole replaced by the body verbatim. Deterministic. Idempotent when the hole is
  already filled with the same body.

Path inventory:
- triggers: every `opts.llm.fixTestCases` and `opts.llm.generateImplementation` call site.
  Enumerate BEFORE changing any: fill (~L1706), fallback fill (~L1987), born-red surgery
  (~L2438), test-rha (~L2620), case-relay (~L5729), build (~L3043), repair (~L4566).
- writers: same list. Change: each call receives a `frame` and a `holeSpec` in `errors[]`;
  the response is spliced into the frame; the spliced result replaces the case body.
- readers: `assembleTestFile`, `wireSuiteScaffoldImports` unchanged — they operate on the
  spliced result exactly as they do today.
- fallbacks: shell-lock (SET 65) was the FIRST attempt at this. It shipped default-on but 40 of
  42 grammar events lands as `grammar fallback: accepted freeform`. SET 95 replaces shell-lock:
  no fallback path, no `accepted freeform` — the model returns hole-body or the draw is
  rejected as `overreach|body` and the ladder tries again at a hotter temperature.

Deterministic predicate for `overreach|body`:
1. Draw response is parsed to extract the hole body (regex `^\s*⟨(?:ASSERTION|BODY:[^⟩]+)⟩\s*([\s\S]+?)\s*⟨/END⟩\s*$` or an even simpler contract — see 95a for the exact wire
   format).
2. If extraction fails → `overreach|body` rejection with reason `no hole marker in response`.
3. `bodyBytes = extracted.length`; `budgetBytes = sum of hole sizes at draw time × 1.5`.
   If `bodyBytes > budgetBytes` → `overreach|body` rejection with reason
   `response ${bodyBytes} > budget ${budgetBytes}`.
4. If response contains any `describe(`, `it(`, `test(`, `import ` outside the hole → same
   rejection, reason `boilerplate reproduction outside hole`.

Failure dispositions:
- Per draw: `accept | overreach|body`. Adds one class to the finding taxonomy; must be added
  to `defectRegistry` classification.

Metrics and conservation equations:
- Add: `overreachBodyRejections`.
- Response-token invariant (asserted in a test):
  `average(response.tokens) < 300` for case fills, `< 600` for file builds. If we exceed on a
  representative benchmark, SET 95 didn't reduce the transmission's work.

Causal hypothesis:
- The transmission is currently generating boilerplate (~150 tokens per case) plus signatures
  (~50 tokens) plus assertion (~50 tokens). The CPU already has the first two — sending them
  through the model is pure waste and pure error surface. Enforcing hole-only output makes the
  first two structurally impossible.

Falsifier:
- Predicted post-fix observations on a rerun of any live spec:
  - Average response tokens per case fill ≤ 300 (was ~900).
  - `overreachBodyRejections` > 0 (proves the enforcement fires at least occasionally).
  - `grammar fallback: accepted freeform` events go to ZERO (shell-lock retired).
- Fix insufficiency: any freeform response accepted. Grep the log for the phrase.

Production-shaped proof:
- Two tests: `tests/tier1-set95-hole-only.test.ts` (a class-based LlmClient that returns a
  hole-only response — assert it is accepted and spliced correctly) and
  `tests/tier1-set95-overreach.test.ts` (same client returns a full freeform response with
  boilerplate — assert it is rejected as `overreach|body`).

Terminal certification:
- Response-token invariant + zero `accepted freeform` in a canonical mock run.

Release blockers:
- Any call site that still passes the full prompt (imports + describe + it + assertion) to the
  LLM.
- Any live run whose average per-case response tokens exceeds 400. That means CPU boilerplate
  is still being generated by the model.

---

Executable instructions (Cursor):

## 95a — wire format for hole-only responses

Choose ONE wire format and enforce it everywhere. Recommended:

```
The model is instructed to respond with EXACTLY:
⟨HOLE⟩
<body content only>
⟨/END⟩
```

Rationale: unambiguous start/end markers; simple regex extraction; the model has to make an
active choice to include boilerplate outside the markers. Both markers appear in prompt
templates so the model is trained on the pair.

Update the system prompts in `src/core/prompts.ts`:
- `IR_SYSTEM_HOLE` (new): the system prompt for hole-only case-body draws.
- `BUILD_SYSTEM_HOLE` (new): the system prompt for hole-only file-body draws.

Both prompts state:
> Your entire response must be ONE `⟨HOLE⟩ … ⟨/END⟩` block. The frame around your response is
> already correct; do not repeat it. Do not include `import`, `describe`, `it`, `test`, or
> function-declaration keywords in the body. Return the smallest correct code that satisfies
> the constraint.

## 95b — `src/tier1/frame.ts`

New module with `assembleCaseFrame`, `assembleImplFrame`, `spliceHole`, `extractHole`,
`isOverreach`. Full implementations, with tests.

Signatures:
```ts
export interface CaseFrame {
  bytes: string;              // the frame with the ⟨HOLE⟩ marker in place
  holeName: 'ASSERTION';
  holeBudgetBytes: number;    // typically 400
}
export function assembleCaseFrame(feature: FeatureNode, caseName: string, kind: IRCaseGen['kind']): CaseFrame;
export interface ImplFrame {
  bytes: string;
  holes: { name: string; symbol: string; budgetBytes: number }[]; // one per exported symbol
}
export function assembleImplFrame(feature: FeatureNode, spec: FeatureSpec): ImplFrame;
export function spliceHole(frame: string, holeName: string, body: string): string;
export function extractHole(response: string): { body: string | null; reason?: string };
export function isOverreach(response: string, budgetBytes: number): { over: boolean; reason?: string };
```

## 95c — route ONE call site through the new module (canary)

Start with the fill site (runner.ts ~L1706). Replace the current call construction with:
1. Build `frame = assembleCaseFrame(feature, target.name, target.kind)`.
2. Build the LLM prompt using the frame + the superbuffer (already assembled via
   `emitSuperbufferAssembly` from SET 92).
3. Call `opts.llm.fixTestCases` with system prompt `IR_SYSTEM_HOLE` and body prompt that
   presents the frame with the hole and states the assertion budget.
4. On response: `const { body, reason } = extractHole(response)`. If `body === null` →
   `overreach|body` rejection, increment `metrics.overreachBodyRejections`, continue ladder.
5. If `isOverreach(response, frame.holeBudgetBytes).over` → same rejection.
6. Otherwise `finalBody = spliceHole(frame.bytes, 'ASSERTION', body)`; that replaces the case
   body in `parts`.

Do NOT change the other five call sites in 95c. Ship the canary. Verify with a live run that
average per-case response tokens drop. Then 95d rolls out to the remaining sites.

## 95d — roll out to remaining call sites

In order: fallback fill, born-red surgery, test-rha, case-relay, build, repair. Each site
gets its own commit and its own metric assertion in the regression test.

## 95e — retire shell-lock

Once 95d lands, the shell-lock validation path (currently emitting `grammar fallback: ...
accepted freeform`) becomes unreachable. Delete the fallback branch; keep the grammar
validation as a POSITIVE conformance check (unchanged) but the `accepted freeform` path is
removed. **Acceptance:** grep for `grammar fallback: accepted freeform` returns zero hits.

### Gates for SET 95
Same as SET 92. Order: 95a → 95b → 95c (canary, ship), then 95d in pieces (each site is a
separate commit and possibly a separate release), then 95e. If the canary in 95c does NOT
show reduced tokens, STOP and re-plan; do not proceed to 95d.

---


---

---

# ACTIVE — SET 96. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# 96 SPLITS INTO TWO INDEPENDENT LANDINGS after Andy's CPU/LLM allocation clarification
# (2026-07-28): "The watcher literally traverses an AST tree and looks for idioms. That is
# a high intelligence task. if we had it relegated to the CPU earlier, that probably excised
# the intelligence from the pipeline. But superbuffer construction is very literal. that
# would be a CPU task."
#
# **96a (CPU-only merge) — SHIPS UNCONDITIONALLY.** Merging `synthesizeForDraw`'s CPU work
# with `assembleSuperbuffer` into one `assembleContext` call is pure win: same processor,
# same work, done in one pass. The standalone watcher LLM call stays.
#
# **96b (LLM fold into candidate 0) — CANARY-GATED.** Folding the watcher's dedicated LLM
# turn into a fill draw gives the small model TWO intelligence tasks in one prompt
# (synthesize patterns + write assertion). Under the CPU/LLM allocation principle each
# intelligence task deserves its own turn. Ship 96b only if a mock benchmark shows
# candidate-0 acceptance rate stays within 5% of the pre-fold baseline. If acceptance rate
# degrades, do NOT ship 96b — the token/time saving is not worth diluting the watcher's
# semantic role.
#
# See memory entry `cpu-vs-llm-allocation.md`. Andy, 2026-07-28:
#   "could we bundle the watcher calculation into an LLM call? … the watcher may even inject
#    useful entropy into 1 of the 3 loops. One loop is watcher entropy infused 2/3 are fully
#    decoupled." + "or the superbuffer construction should also execute or be executed by the
#    watcher task."
#
# The two suggestions are one move at different levels: the watcher's *CPU* work merges into
# the superbuffer's, and its *LLM* work merges into one of the K fill candidates. After this
# set lands, `synthesizeForDraw` no longer issues a standalone LLM call for the watcher on the
# fill path; SET 94's 94a (digest cache) and 94d (world-advance skip) become dead-code for the
# watcher and continue to apply only to the meta-watcher.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome (SPLIT — read both):

**96a — CPU-only merge (unconditional):**
- Superbuffer assembly and the watcher's CPU-side work (digest computation, artifact read,
  ledger read) merge into ONE `assembleContext` call that reads the ledger + artifact
  exactly once. Returns `{ pages, superbufferText, digest, unionText, digestPreamble }`.
- The standalone watcher LLM call STAYS — it is a semantic intelligence task
  (idiom/trope/scheme recognition) and belongs on the LLM. Do NOT relegate it to a CPU
  regex-over-AST — that would excise the intelligence exactly as memory
  `cpu-vs-llm-allocation.md` warns.
- Small CPU cleanup win (one ledger traversal instead of two); no wall-time change from the
  LLM's perspective. The value is architectural clarity, not compute.

**96b — LLM fold into candidate 0 (canary-gated; DO NOT SHIP without acceptance proof):**
- IF a mock benchmark shows candidate-0 acceptance rate under the fold stays within 5% of
  the pre-fold baseline, ship: candidate 0 of the K=3 fill ladder receives the digest
  preamble as prompt context, replacing the standalone watcher call on the IR-fill path.
  Saves ~5-6 seconds per case × ~50 cases = ~5 min wall time on the 78e36cc1 spec.
- IF acceptance rate degrades > 5%, DO NOT SHIP. The two-tasks-in-one-prompt dilution has
  cost more intelligence than the fold saved compute. Roll back to standalone watcher +
  SET 94's caching-and-skip approach as the compute story.
- Doctrine 20 satisfied only when the benchmark clears. Doctrine 21 (advice-not-execution)
  satisfied either way: the fill draw's ADOPTION oracle (parse + conformance + execution)
  is unchanged.

Work unit and identity:
- New unit `AssembleContextTask(featureId, stage)` = { superbuffer pages, deterministic
  digest, cached-or-none reading }. Pure CPU. Idempotent for the same ledger + artifact hash.
- The task returns a `contextBundle`. Candidate 1 of a fill batch receives the bundle plus a
  system-prompt suffix instructing the model to "note the pattern in the digest, then write
  the assertion." Candidates 2..K receive the bundle without the suffix.

Canonical state and owner:
- New owner: `src/tier1/assembleContext.ts` — the single call that produces a `ContextBundle`.
- Callers unchanged in shape: the five call sites that today invoke
  `synthesizeForDraw → assembleSuperbuffer` sequentially now invoke `assembleContext` once,
  destructure `{ superbufferText, digest, cachedReading }`, and pass all three into
  `runLlmAction`.
- `synthesizeForDraw` (in `runner.ts`) shrinks to a shim that only fires for the meta-watcher
  (rha stage) and the build stage. The IR-fill path never calls it.

Path inventory:
- triggers: fill (~L1706), fallback fill (~L1987), born-red surgery (~L2438), test-rha
  (~L2620 after SET 92), case-relay (~L5729). All five become `assembleContext` callers.
- writers: none — this is a pure read. The advisory `watch.reading` state on the runner
  becomes optional cache only (populated by candidate 1's response digest — see 96c).
- readers: same as today (superbuffer packing).
- fallbacks/reconstructions: if candidate 1 fails and candidate 2 accepts, the watcher-infused
  entropy was consumed but not stored; that's fine. If ALL K fail, the next case's candidate 1
  gets a fresh digest.

Deterministic predicate (per candidate):
- Candidate index 0 (first in the K-ladder): prompt = `IR_SYSTEM_HOLE` (SET 95) + digest
  preamble ("The ledger records the following node activity: <digest>. Note the pattern, then
  write the assertion for `<case name>`.") + superbuffer + case frame + hole marker.
- Candidate index 1..K−1: prompt = `IR_SYSTEM_HOLE` + superbuffer + case frame + hole marker.
  No digest preamble; no synthesis role.
- On acceptance of candidate 0, cache the response's implicit reading (see 96c) so downstream
  cases can reuse it without re-preamble. On acceptance of candidate 1..K−1, do NOT cache
  (they were not asked to synthesize).

Failure dispositions: unchanged. Candidate 0's acceptance path is identical to any other
candidate — parse/conformance/execution oracle stays sovereign.

Metrics and conservation equations:
- Add: `watcherInfusedCandidates` (count of candidate-0 draws that ran with the digest
  preamble), `watcherStandaloneCalls` (count of standalone watcher calls — should approach 0
  on the IR-fill path; nonzero only from meta-watcher and build stages).
- Invariant: `watcherStandaloneCalls_on_fill_path === 0` after 96 lands.
- 94a's `watcherCacheHits` becomes meta-only. 94d's `watcherWorldStaleSkips` same. Rename them
  in the metrics strip to `metaWatcherCacheHits` / `metaWatcherWorldStaleSkips` for honesty.

Causal hypothesis:
- 68 watcher calls in 78e36cc1 × ~6s = ~7 minutes of pure LLM latency for a run that also had
  46 fill batches. If those 68 calls become 0 (with candidate 0 absorbing the synthesis role),
  wall time drops by ~7 minutes on that spec. Combined with SET 94's other parts, the IR
  phase should shrink from ~25m to ~15-17m.

Falsifier:
- Predicted post-fix observations:
  - No log line matches `^\[ir\] llm call — watch:` on the IR-fill path (still allowed on
    the RHA path for the meta-watcher).
  - `watcherInfusedCandidates` roughly equals `fillBatches` (~46 on the 78e36cc1 spec).
  - `watcherStandaloneCalls` on fill path === 0.
  - Wall time on IR phase drops by ≥ 40% relative to the last measured 1.7.70/1.7.71 run.
- Fix insufficiency:
  - Any `^\[ir\] llm call — watch:` line on the fill path proves the fold didn't fully happen.
  - `watcherInfusedCandidates === 0` proves the digest preamble is not being emitted.
  - Wall time drop < 20% proves the CPU merge didn't save the round-trips.

Production-shaped proof:
- Class-based `LlmClient` that counts calls per `op`. Test asserts on a mock fill: `op === 'watch'`
  count is 0, `op === 'ir'` count equals the K-ladder draws, and the first candidate's prompt
  contains the digest preamble while the second/third do not.
- Regression on the 78e36cc1 spec via mock: `watcherInfusedCandidates > 0`,
  `watcherStandaloneCalls_on_fill_path === 0`.

Terminal certification:
- After 96 lands, delete the IR-fill path's `synthesizeForDraw` call sites. Keep the function
  itself for the RHA meta-watcher and build-stage callers. Verify with grep.

Release blockers:
- Any IR-fill call site that still calls `synthesizeForDraw`.
- Any candidate-0 prompt without the digest preamble (identifiable via a mock).
- Any run where `watcherStandaloneCalls` on the fill path is nonzero.

---

Executable instructions (Cursor):

## 96a — new module `src/tier1/assembleContext.ts`

```ts
/**
 * SET 96 — one-shot context assembly.
 *
 * Replaces the sequential pair `synthesizeForDraw(...)` + `assembleSuperbuffer(...)` for
 * the IR-fill path. Reads the ledger + artifact exactly once, produces the deterministic
 * digest AND the superbuffer AND (when available) the last cached watcher reading.
 *
 * NO LLM CALL. The synthesis-role that the standalone watcher previously performed is
 * folded into the FIRST candidate of the fill K-ladder — see 96b for the call-site
 * change.
 */
import type { FeatureSpec, FeatureNode, GeneratedFile } from '../core/types';
import type { IRParts } from '../core/irScaffold';
import type { WatchEpoch, EpochDigest, WatchReport } from './watcher';
import { buildEpochDigest, formatDigest, formatUnionPage } from './watcher';
import { assembleSuperbuffer, formatSuperbufferForPrompt, type SuperbufferAssembly } from './superbuffer';
import type { SubstrateHandoff } from './substrateHandoff';
import type { Tier1Lesson } from './substrate';
import type { Observation } from './observatory';
import { formatObservatoryPage, ObservationLedger } from './observatory';
import type { LedgerRow } from '../core/types';

export interface ContextBundle {
  /** Deterministic digest (never null; the "world" as of this call). */
  digest: EpochDigest;
  /** Rendered union page — carries the cached WATCH reading if any, else digest-only. */
  unionText: string | null;
  /** Full superbuffer assembly (for logLine + counter increment via emitSuperbufferAssembly). */
  assembly: SuperbufferAssembly;
  /** Superbuffer bytes ready to splice into the LLM prompt. */
  superbufferText: string;
  /** Digest preamble text for candidate-0's prompt — the "watcher-infused" bit. */
  digestPreamble: string;
}

export interface AssembleContextOpts {
  stage: WatchEpoch;
  featureId: string;
  spec: FeatureSpec;
  feature: FeatureNode;
  parts: IRParts | null;
  stubs: GeneratedFile[];
  lessons: Tier1Lesson[];
  siblingParts: Map<string, IRParts>;
  target: string;                                // as in assembleSuperbuffer
  substrateHandoff?: SubstrateHandoff | null;
  conceptText?: string | null;
  ledgerRows: readonly LedgerRow[];
  artifact: GeneratedFile[];
  /** Optional cached reading from a prior candidate-0 acceptance. */
  cachedReading?: WatchReport | null;
  /** Observations (SET 85) — never LLM-called from here. */
  observations?: readonly Observation[];
}

export function assembleContext(opts: AssembleContextOpts): ContextBundle {
  const digest = buildEpochDigest({
    epoch: opts.stage,
    rows: opts.ledgerRows,
    artifact: opts.artifact,
    featureId: opts.featureId,
  });
  const unionText = formatUnionPage(digest, opts.cachedReading ?? null);
  const assembly = assembleSuperbuffer({
    spec: opts.spec,
    feature: opts.feature,
    parts: opts.parts,
    stubs: opts.feature.stubs,
    lessons: opts.lessons,
    siblingParts: opts.siblingParts,
    target: opts.target,
    stage: 'fill',
    conceptText: opts.conceptText ?? null,
    substrateHandoff: opts.substrateHandoff ?? null,
    unionText,
  });
  const superbufferText = formatSuperbufferForPrompt(assembly);
  const digestPreamble = [
    'The pipeline ledger records this activity so far:',
    formatDigest(digest).slice(0, 800),
    '',
    'Note the recurring patterns above before writing your response.',
  ].join('\n');
  return { digest, unionText, assembly, superbufferText, digestPreamble };
}
```

**Acceptance:** `assembleContext` called twice with the same inputs returns byte-identical
`superbufferText` and `digestPreamble`. Pure.

## 96b-CANARY — thread `assembleContext` through the fill site AND fold the watcher into candidate 0 (measured, not assumed)

### Canary protocol (BEFORE any code change)

Build a mock benchmark harness (`scripts/set96-canary.ts`) that runs `runTier1Job` twice on
a fixed spec + fixed seed with a deterministic `LlmClient` double that measures per-candidate
acceptance rate. Run A = pre-fold shape (standalone watcher, candidate 0 gets no preamble).
Run B = post-fold shape (no standalone watcher, candidate 0 gets the digest preamble).

Report `candidate0.accepted / candidate0.total` for both. If B < A − 5%, DO NOT ship 96b.
Emit the numbers to stdout and STOP; open a review comment on the change.

If B ≥ A − 5%, proceed with the code changes below and ship 96b as its own release. The
canary re-runs in CI as `tests/tier1-set96b-canary.test.ts` on every future change to any
of the five fill call sites, so a future regression that degrades candidate-0 acceptance
under the fold trips the same guard.

### Code changes (only after the canary clears)

At each of the five sites (fill / fallback fill / born-red surgery / test-rha / case-relay):
1. Remove the `const union<X> = await synthesizeForDraw('ir', feature.id, ...)` line.
2. Replace the following `assembleSuperbuffer({ ..., unionText: union<X> })` construction
   with a `const ctx = assembleContext({ ... })` call.
3. Use `emitSuperbufferAssembly('ir', feature.id, ctx.assembly, '<site>')` (from SET 92)
   to preserve the log line and counter increment.
4. Pass `ctx.superbufferText` as the fill payload today's code puts in place of the old
   `formatSuperbufferForPrompt(assembly)` value.
5. **Candidate 0 (first in the K-ladder) additionally receives the `ctx.digestPreamble`**
   spliced into the errors[] or system-prompt-preamble slot of `runLlmAction`. Candidates
   1..K−1 do not. This is the "watcher-entropy-infused" bit.

Pattern for each candidate loop:

```ts
for (let attempt = 0; attempt < PERMUTATION_TEMPS.length; attempt++) {
  metrics.<counter>++;
  const isWatcherInfused = attempt === 0;
  if (isWatcherInfused) metrics.watcherInfusedCandidates++;
  const errors = [
    /* existing per-case error strings */,
    ctx.superbufferText,
    isWatcherInfused ? ctx.digestPreamble : '',
  ].filter(Boolean);
  const fixed = await runLlmAction({
    method: '<existing>', target: feature.id, phase: 'ir',
    fn: () => opts.llm.fixTestCases(
      spec, feature,
      [{ name: caseHit.name, kind: caseHit.kind, errors }],
      [], parts!.imports,
      { /* existing opts */, temperature: PERMUTATION_TEMPS[attempt] },
    ),
  });
  /* ... existing acceptance path ... */
}
```

**Acceptance:** grep for `synthesizeForDraw\('ir'` returns zero hits in `runner.ts`.
`synthesizeForDraw` itself remains (used by RHA meta-watcher / build).

## 96c — cache candidate-0's implicit reading (optional; ship even without it)

Candidate 0 accepts as a fill response — it returns a case body, not a WATCH_SCHEMA reading.
There is no explicit reading to cache. This is FINE: the cost of re-emitting the digest
preamble on the next case is one extra ~800-char string in a prompt that already runs 1200+
tokens. No LLM round-trip is added. So 96c is a no-op initially — no cache is populated on
this path, and `synthesizeForDraw`'s cache remains meta-watcher-only.

If a future set wants to give the model a chance to explicitly synthesize (e.g., via a
mixed-schema response that returns `{ union: string; cases: [...] }`), that's SET 96e. Not
scoped here.

## 96d — deprecate SET 94a's watcher-facing bits

After 96b lands, in `synthesizeForDraw`:
- 94a's `digestCacheKey` + `watch.cache` remain but are ONLY hit on the RHA meta-watcher and
  build stages. Rename the metric fields to `metaWatcherCacheHits` and
  `metaWatcherWorldStaleSkips` for accuracy.
- 94d's `hasWorldAdvancedSince` predicate stays and continues to gate the meta-watcher.

## 96e — regression tests

New file `tests/tier1-set96-context-bundle.test.ts`:
- Pure test of `assembleContext` idempotence.
- Mock `runTier1Job` with a class-based `LlmClient` that counts calls per `op`. Assert
  `op === 'watch'` count is 0 during an IR-fill batch (nonzero only in RHA phase).
- Assert candidate 0's prompt contains the string
  `The pipeline ledger records this activity so far:` and candidates 1..K−1 do not.
- Assert `metrics.watcherInfusedCandidates > 0` and
  `metrics.watcherStandaloneCalls_on_fill_path === 0`.

### Gates for SET 96
Same as SET 92. Order: 92 → 93 → 94 → 96a (unconditional) → 96b-CANARY (measure, then ship IF proof) → 95 (95's LLM-as-transmission changes the prompt
shape further, and 96 stays compatible: candidate 0 splices the preamble INTO the errors[]
which is orthogonal to hole-only response enforcement). If 96 is not implemented, 94a/94d
still deliver the compute win via caching rather than folding.

### Interaction with SET 94
- 94a's watcher cache becomes meta-watcher-only after 96 lands. Not deleted; renamed.
- 94b's `bypassStageSpacing` remains applied to `watchEpoch` — the meta-watcher (still a
  standalone call) benefits.
- 94c (substrate cap) unchanged.
- 94d's world-advance predicate becomes meta-watcher-only after 96 lands.
- 94e (metasupercache: corpse-rejection-only, meta-watcher deleted) unchanged. After 94e
  lands, `synthesizeMeta` and its call sites are gone entirely, so 96's `assembleContext`
  does not need to consider the meta-watcher path at all.

If you ship 96 before 94, drop 94a and 94d from the 94 landing (they'd be dead code for the
fill path from day one). Keep 94b/94c/94e. Recommended order remains **94 first, then 96**,
because 94a/94d still deliver a partial win for the meta-watcher and are cheap to keep.

---

# ACTIVE — SET 97. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# Substrate-first fill. Doctrine 1 in practice, not in name. Validated by live run 08d1e099:
# `substrate hit-rate: 0/110 fixes (0%) · refusals 6 · no-handler 97` — every substrate
# mechanism is wired as a POST-FAIL rescue but never runs BEFORE the LLM draw.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- Every planned case (post-plan, pre-LLM-draw) runs a substrate exhaustion pass. If any
  substrate mechanism (`matchConsumableTemplate`, `rebindCladeBody`, `methodAnchorBody`)
  produces a body that BOTH passes `acceptCaseBody` AND fails against stubs (red), that body
  fills the case at 0 tokens and the K=3 LLM ladder is skipped for it entirely.
- The LLM only sees cases the substrate could not handle.
- `substrate hit-rate at fill` (new metric) rises from 0% to ≥ 30% on the same spec (F1..F8
  clade shapes: `class-anchor`, `uses-dep`, `determinism`, `rejects-empty`, `roundtrip`,
  `model-schema` — many of these are structurally satisfiable without the LLM).

Work unit and identity:
- Work unit = ONE planned case, subject to substrate exhaustion before the K-ladder starts.
- Identity: `${feature.id}#${case.name}`. Substrate hit iff body accepts and is red.

Canonical state and owner:
- Owner: a new helper `substrateFillOne(caseGen, feature, spec, stubs, runSuite)` in
  `src/tier1/substrateFill.ts`. Sole entry for pre-LLM substrate fill at the FILL path.
- Callers: the fill-batch loop (grep `plan: N cases (\d+ substrate-templated`), the case-relay
  loop, and born-red surgery entry.
- Post-fail substrate paths (`rebindCladeBody` at rescue sites, `rebindCladeBody` at RHA
  IR-case surgery) STAY as-is; SET 97 adds the PRE-LLM path, does not remove the fallback.

Path inventory:
- triggers: fill batch (grep `fill batch: targets`); case-relay (grep `case-relay: refilling`);
  born-red surgery (grep `born-red: .* vacuous case\(s\) → surgery`).
- writers: `parts.cases` per feature, before the K-ladder starts.
- readers: the existing K-ladder — which now sees a REDUCED case list (only cases substrate
  did not fill).
- fallbacks/reconstructions: post-fail rescue paths unchanged.

Deterministic predicate (in `substrateFillOne`):
1. Try `matchConsumableTemplate(caseGen, feature, spec)` — returns { body, id } | null.
2. If null, try `rebindCladeBody({ ...caseGen, clade: classifyCaseClade(caseGen, ownedSymbol) })`.
3. If null, try `methodAnchorBody(caseGen, feature)` when the case name matches a method
   anchor (`isMethodAnchorName`).
4. If any returns a non-null body:
   a. `if (!acceptCaseBody(body)) continue` — malformed body, next mechanism.
   b. Assemble a single-case suite and run against stubs. If the suite RUNS and the case
      FAILS (red), the body is a legitimate substrate fill. If it PASSES (green vs stubs =
      vacuous), REJECT the substrate fill and let the LLM try — vacuous is worse than novel.
   c. If red, emit `case "<name>" filled from substrate: <mechanism> (0 tok)`, increment
      `metrics.substrateFillHits`, return `{ body, mechanism }`.
5. If all null or all vacuous, return null. The K-ladder runs as today.

Note on vacuous check: `rebindCladeBody` for a dep-seam or class-anchor case returns a body
whose only assertion is structural (`expect(new X()).toBeDefined()`). Against a THROWING stub
that fails red (the `new` throws). Against a non-throwing stub, it passes green — vacuous.
The runtime check catches both cases correctly. This is the critical safeguard.

Failure dispositions:
- Per mechanism attempt: `substrate-fill-empty | substrate-fill-vacuous | substrate-fill-red`
  (only the last accepts).
- Per case: `filled-by-substrate | escalated-to-llm` (the K-ladder runs for the escalated
  case, unchanged).

Metrics and conservation equations:
- Add: `substrateFillHits`, `substrateFillEscalations`, `substrateFillEmptyMechanisms`,
  `substrateFillVacuousMechanisms`.
- Conservation: `substrateFillHits + substrateFillEscalations = casesPlannedForFill`.
- New report line at end of run: `substrate fill hit-rate: N of M cases (P%) filled at 0
  tokens via substrate — L LLM draws avoided`. Distinct from the existing
  `substrate hit-rate: 0/110 fixes (0%)` which is the RHA-repair substrate metric.

Causal hypothesis:
- Live run 08d1e099 shows `rebindCladeBody` firing successfully at LATE paths — F1's "Empty
  input for creation" rebound via clade at 28m42s, F4's "Query length boundaries" rebound
  at 40m05s. Both worked at 0 tokens. Both would have worked if tried BEFORE the LLM.
- Instead, F1's "Empty input" burned 3 initial LLM draws + 3 fallback + 3 case-relay = 9 LLM
  draws before the late rebind saved it. ~1 minute of wall time per case × ~10 clade-shaped
  cases per spec = 10 minutes of wall time recoverable per run.

Falsifier:
- Predicted post-fix observations on the same 08d1e099 spec:
  - `metrics.substrateFillHits ≥ 3` (at minimum: one class-anchor, one dep-seam, one
    determinism/roundtrip case).
  - Log contains ≥ 3 lines matching `case ".+" filled from substrate: (matchConsumableTemplate|rebindCladeBody|methodAnchorBody) \(0 tok\)`.
  - IR-phase wall time drops by ≥ 3 minutes vs baseline 22:10 (measured on that spec).
- Fix insufficiency:
  - `substrateFillHits === 0` on the 08d1e099 spec. Either the mechanisms are not being called
    or the vacuous check rejects everything. Debug by logging each mechanism's return per case.
  - IR wall time drop < 90s. The clade shapes did not match; either the planner-emitted
    `clade` is wrong or the substrate mechanisms have narrower coverage than expected.

Production-shaped proof:
- Unit test: `substrateFillOne` with a fixture spec containing a class-anchor case + a
  throwing stub. Assert result is `{ body, mechanism: 'rebindCladeBody' }`. Assert
  `substrateFillHits === 1` in a mini-runner integration.
- Live-parity test: mock `LlmClient.fixTestCases` to count calls; assert it is called strictly
  fewer times than the total cases planned on a spec whose plan has ≥ 3 clade-shaped cases.

Terminal certification:
- `substrateFillHits > 0` on a run that produced any substrate-shaped cases; a run report with
  substrate fill hit-rate line; test suite green.

Release blockers:
- Any run where the substrate could have filled a case (post-hoc: the rescue path fires at 0
  tokens) and the pre-LLM path did NOT fill it (evidenced by the LLM having been called for
  it). This is a runtime assertion tester: correlate `case rebound via clade` post-hoc lines
  against fill-batch LLM call counts for the same case name.

---

Executable instructions (Cursor):

## 97a — new module `src/tier1/substrateFill.ts`

```ts
/**
 * SET 97 — substrate-first fill. Doctrine 1 in practice.
 *
 * Every planned case runs its substrate mechanisms BEFORE the LLM sees it. Only cases the
 * substrate cannot handle (or handles vacuously against stubs) escalate to the K-ladder.
 */
import type { FeatureSpec, FeatureNode, GeneratedFile } from '../core/types';
import type { IRCaseGen } from '../core/irScaffold';
import { assembleTestFile } from '../core/irScaffold';
import { wireSuiteScaffoldImports } from './suiteScaffoldWire';
import type { SuiteRunner } from './testExec';
import { acceptCaseBody } from './superbuffer';
import {
  METHOD_ANCHOR_CLADE, classifyCaseClade, isMethodAnchorName,
  methodAnchorBody, rebindCladeBody,
} from './clades';
import { matchConsumableTemplate, countMatchingTemplatesForPlan } from './planSuite';  // adjust import path

export type SubstrateFillMechanism =
  | 'matchConsumableTemplate'
  | 'rebindCladeBody'
  | 'methodAnchorBody';

export interface SubstrateFillHit {
  body: string;
  mechanism: SubstrateFillMechanism;
  templateId?: string;
}

export interface SubstrateFillOpts {
  spec: FeatureSpec;
  feature: FeatureNode;
  caseGen: IRCaseGen & { symbol?: string };
  stubs: GeneratedFile[];
  runSuite: SuiteRunner;
  imports: import('../core/irScaffold').IRImport[];
}

export async function substrateFillOne(
  opts: SubstrateFillOpts,
): Promise<SubstrateFillHit | null> {
  const { spec, feature, caseGen, stubs, runSuite, imports } = opts;

  const tryMechanism = async (
    mechanism: SubstrateFillMechanism,
    body: string | null,
    templateId?: string,
  ): Promise<SubstrateFillHit | null> => {
    if (!body || !acceptCaseBody(body)) return null;
    const singleCase = { ...caseGen, body };
    const wired = wireSuiteScaffoldImports(
      assembleTestFile(feature, imports, [singleCase]),
      feature.testFile, spec,
    ).content;
    let run;
    try {
      run = await runSuite(stubs, { path: feature.testFile, content: wired });
    } catch { return null; }
    if (run.crashed) return null;
    if (run.total !== 1) return null;
    // Red iff the one case failed against stubs. Green = vacuous = reject.
    if (run.failures.length !== 1) return null;
    return { body, mechanism, ...(templateId ? { templateId } : {}) };
  };

  // 1. matchConsumableTemplate (planner-time template match)
  if (caseGen.symbol) {
    const t = matchConsumableTemplate({ name: caseGen.name, kind: caseGen.kind, symbol: caseGen.symbol } as any);
    if (t) {
      const hit = await tryMechanism('matchConsumableTemplate',
        t.body.replace(/SYM/g, caseGen.symbol), t.id);
      if (hit) return hit;
    }
  }

  // 2. rebindCladeBody (clade template rebind)
  {
    const clade = classifyCaseClade({ name: caseGen.name, kind: caseGen.kind, body: '' }, caseGen.symbol);
    const body = rebindCladeBody({ ...caseGen, clade, body: '' });
    const hit = await tryMechanism('rebindCladeBody', body);
    if (hit) return hit;
  }

  // 3. methodAnchorBody (method-anchor case with signature-derived body)
  if (isMethodAnchorName(caseGen.name)) {
    const body = methodAnchorBody(caseGen, feature);
    const hit = await tryMechanism('methodAnchorBody', body);
    if (hit) return hit;
  }

  return null;
}
```

## 97b — wire into the fill batch loop in `runner.ts`

Locate the fill batch loop by:
```
grep -n "fill batch: targets" src/tier1/runner.ts
```
BEFORE the K-ladder starts for each batch (immediately after the batch is assembled and its
target case list is known), iterate and try substrate exhaustion:

```ts
// SET 97 — substrate-first fill. Try the substrate mechanisms BEFORE any LLM draw.
const escalatedCases: PlannedCase[] = [];
for (const h of batch) {
  const hit = await substrateFillOne({
    spec, feature,
    caseGen: { name: h.name, kind: h.kind, body: '', symbol: h.symbol } as IRCaseGen & { symbol?: string },
    stubs, runSuite: opts.runSuite,
    imports: parts?.imports ?? plan.imports,
  });
  if (hit) {
    trialParts.cases.push({
      name: h.name, kind: h.kind, body: hit.body,
      clade: classifyCaseClade({ name: h.name, kind: h.kind, body: hit.body }, h.symbol),
    });
    metrics.substrateFillHits++;
    emit('ir', feature.id,
      `case "${h.name}" filled from substrate: ${hit.mechanism} (0 tok)`);
    continue;
  }
  escalatedCases.push(h);
  metrics.substrateFillEscalations++;
}
// Rebind `batch` to escalated cases only; the K-ladder runs only for cases substrate could
// not handle.
batch = escalatedCases;
if (!batch.length) {
  // All cases filled by substrate — skip the K-ladder entirely.
  emit('ir', feature.id, `fill batch: all ${trialParts.cases.length} case(s) filled from substrate (0 tok)`);
  continue;  // to the next fill batch
}
```

## 97c — wire into case-relay path

Locate by:
```
grep -n "case-relay: refilling" src/tier1/runner.ts
```
Same pattern: try `substrateFillOne` first; only escalate to the K-ladder if substrate misses.

## 97d — wire into born-red surgery entry

Locate by:
```
grep -n "born-red: .* vacuous case" src/tier1/runner.ts
```
Same pattern. If a case's substrate fill produces a red body, it un-vacuums without an LLM
call. Only genuinely uncoverable cases enter surgery.

## 97e — metrics + reporting

Extend `Tier1Metrics`:
```ts
substrateFillHits: number;
substrateFillEscalations: number;
substrateFillEmptyMechanisms: number;
substrateFillVacuousMechanisms: number;
```
Initialize to 0 in `metrics`, in `vanilla.ts`, in `tests/tier1-set3.test.ts`.

Add at end-of-run event emit:
```
substrate fill hit-rate: ${substrateFillHits} of ${substrateFillHits + substrateFillEscalations}
cases filled at 0 tokens via substrate — approximately ${substrateFillHits * 3} LLM draws avoided
```

## 97f — regression test

New file `tests/tier1-set97-substrate-first-fill.test.ts`:
- Unit test on `substrateFillOne` with a class-anchor case + throwing stub. Assert
  `{ mechanism: 'rebindCladeBody' }` and no LLM was called.
- Integration test on `runTier1Job` with a spec whose plan has 3 clade-shaped cases + 3 novel
  cases. Assert `metrics.substrateFillHits >= 3` AND the counting `LlmClient` records fewer
  `fixTestCases` calls than 6 × K.
- Mutation test: if the vacuous-body rejection in `tryMechanism` is deleted (accept green
  against stubs), assert that a vacuous body enters `parts.cases` — proves the safeguard
  is load-bearing.

### Gates for SET 97
Same as SET 92. Order: 92 → 93 → 94 → 96a → 96b-CANARY → 97 → 95. (97 comes late because it
touches every fill call site — landing it after the pipeline stabilizes reduces integration
risk.)

### Follow-up (do NOT include in SET 97)
- Live run 08d1e099 shows `catalog-drift: 53 · UNCOVERED`, the largest single class of
  failure this run. The message shape is
  `"Class constructor X cannot be invoked without 'new'"` — a mechanical missing-new-keyword
  issue. Build a substrate handler for it as SET 98. Predicted saving: ~10 min per run.
- F1's "Article" attempted ~15 times across cycles (initial K=3 + metaloop K=3 + fallback K=3
  + case-relay K=3 + case-relay K=3). Every draw vacuous, every draw dropped. The fill LADDER
  has no per-case exhaustion cap distinct from the metaloop cap. Track as SET 99.

---

# ACTIVE — SET 100. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# Trigger: live-run log excerpts from Andy on vault-1.7.75 wiki-clone probe.
#   (a) Heal succeeds → 0/1 passing with no emit between them.
#   (b) `[encode] partially excluded: 9999px (line 241)` — no mechanism ever uses "partial".
#   (c) Lone px-only bullet continuations get scored as independent assertable items.
#   (d) Wikipedia-clone prompt has enough unverifiable-only items that encode should refuse
#       at plan-gate, not spend LLM tokens drawing a spec whose cases will all plan-gate out.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- A heal that succeeds but whose product then fails `surfaceConformanceErrors` or
  `acceptCaseBody` emits ONE rejection event naming which predicate rejected and why
  (doctrine 7, blind spot A). The permutation ladder can therefore stop early or escalate
  informed instead of blind.
- The word "partially excluded" is used ONLY when a `behavioralSplit` retained testable
  behavior. Every other capability-fenced line reads `excluded:` (doctrine 12 — names do not
  confer semantics that the code does not implement).
- A bulleted item whose ENTIRE non-marker payload is a capability-clause substring (e.g. a
  bare `9999px`, `1024px`, `800px`) is treated as whole-line excluded regardless of the
  `hasAssertableStructure` predicate — because the residue after excising the clause is empty
  (doctrine 4 — per-member certification: nothing left to certify).
- Encode refuses at plan-gate BEFORE any LLM tokens are spent when the pre-encode scan shows
  zero surviving assertable items across the prompt's numbered/bulleted list. The refusal is
  ONE named terminal disposition (doctrine 19) with a specific reason string
  (`refused: prompt has no verifiable items — <n> items scanned, <n> capability-fenced, 0
  assertable residue`). Doctrine 17 — fail-open runtime is not fail-open certification.

Work unit and identity:
- 100a: ONE fill-candidate scoring pass (`(featureId, holeName, candidateIndex)`).
- 100b: ONE deferred hit's exclusion-label emission (`(scanSite, capabilityHit)`).
- 100c: ONE numbered/bulleted line's assertability decision (`(promptLine, capabilityHit)`).
- 100d: ONE encode entry-gate decision on the whole prompt (`(promptId)`).

Canonical owner (doctrine 6):
- 100a: `src/tier1/runner.ts` fill loop (around the `surfaceConformanceErrors` + `acceptCaseBody`
  check). No new owner; add ONE emit inside the existing rejection branch.
- 100b: `src/tier1/runner.ts` pre-encode scan emit (~line 1352) and plan-gate emit
  (~line 1620). One label predicate: `hitIsHonestlyPartial(hit) === hit.partial && !!hit.behavioralSplit`.
- 100c: `src/tier1/substrate.ts` `scanCapabilities` — after `extractCapabilityClause`, compute
  `residue = body.replace(clause, '').trim()` stripped of punctuation/spaces; if
  `residue.length < ASSERTABLE_RESIDUE_MIN` (a stated constant, not an eligibility threshold —
  see §16 note below), flip the `partial` decision to whole-line exclude.
- 100d: NEW predicate `promptHasVerifiableResidue(rawPrompt)` in `src/tier1/substrate.ts`,
  called from `runner.ts` immediately before the `encode` LLM action (~line 1367).

Path inventory (doctrine 9):
- `emit` sites for fill-candidate acceptance: runner.ts:1927 (healed), runner.ts:1932 (silent
  reject — TARGET SITE for 100a).
- `emit` sites for exclusion labeling: runner.ts:1347 (behaviorSplit), runner.ts:1353
  (partial), runner.ts:1356 (excluded); plan-gate mirrors at runner.ts:1626/1629/1612.
  ALL FIVE emit sites move behind the shared label predicate (100b).
- `scanCapabilities` is the only capability scanner. `hasAssertableStructure` is the only
  assertability predicate. Both live in substrate.ts.
- Encode entry: runner.ts:1367 `actionLedger.runAction({ kind: 'llm:encodeFeatures', … })`.
  Add gate BEFORE this action. No other encode entrypoint exists.

Deterministic predicate (doctrine 10):
- 100a: `wasRejectedAfterHeal(candidate) = healedThisPass(candidate) && (surface.length > 0 || !acceptCaseBody(body))`.
- 100b: `isHonestlyPartial(hit) = hit.partial && !!hit.behavioralSplit`.
- 100c: `hasAssertableResidue(body, clause) = residue(body, clause).length >= ASSERTABLE_RESIDUE_MIN`.
- 100d: `promptHasVerifiableResidue(rawPrompt) = itemLines(rawPrompt).some(l => !scanCapabilities(l).length || hasAssertableResidue(l, extractCapabilityClause(l, hit.capability)))`.

Failure dispositions (doctrine 7):
- 100a: emit `case "<name>" candidate <ci>/<K> rejected after heal — <predicateName>: <detail>`.
  Example: `case "Article" candidate 1/3 rejected after heal — surface-conformance: unallowed identifier "fetch"`
  or `case "Article" candidate 1/3 rejected after heal — accept-body: empty assertion block`.
- 100b: no new emit; rewritten label routes through predicate.
- 100c: emit `excluded: "<item>" — requires <capability>; not testable in this runtime (line <n>)`
  when the residue is empty (join the existing full-exclude path).
- 100d: emit `refused: prompt has no verifiable items — <total> items scanned, <fenced>
  capability-fenced, 0 assertable residue`. Run terminates as `refused` with this reason.
  Zero LLM cost. Zero downstream cascade.

Metric equations (doctrine 11):
- 100a: `metrics.healedButRejected++` inside the new emit branch; no double-counting because
  `findings++` already accounts for the numeric fitness.
- 100b: NO metric change (label-only).
- 100c: `metrics.deferredResidueEmpty++` when residue predicate flips a `partial` to whole.
- 100d: `metrics.encodeRefusedNoResidue++` on the refusal branch; `metrics.deferredTotal`
  reflects the fenced items counted during the gate (no double-scan).

Causal hypothesis (doctrine 24):
- 100a: heal succeeded (syntax invariant), but `surfaceConformanceErrors` / `acceptCaseBody`
  own DIFFERENT invariants. Blind spot A treated heal presence as heal validity across
  invariants. One real cause named; but also inspect whether `surface` frequently rejects on
  the same identifier across candidates — if so, that's a separate substrate opportunity
  (tracked as SET-100-adj, not bundled here).
- 100b/c: the "partial" flag was introduced for `behavioralSplit`; computed-style has no split
  path (substrate.ts:2007 gates split to `redis`/`database`). Label leaked past its mechanism.
- 100d: encode currently spends tokens producing a spec whose ENTIRE plan will plan-gate out
  case-by-case (runner.ts:1639-1642 `abandoned — all planned cases excluded`). The refusal is
  DERIVABLE from the pre-encode scan; drawing the spec first is pure waste.

Falsifier BEFORE fix (doctrine 14):
- 100a: new test `tests/tier1-set100a-heal-reject-emit.test.ts` — construct a case where
  heal succeeds and `surfaceConformanceErrors` returns hits; assert exactly ONE event
  matching `/candidate \d+\/\d+ rejected after heal — surface-conformance:/` on the event log
  after fill runs. RED before the fix, GREEN after.
- 100b: new test `tests/tier1-set100b-partial-label-honesty.test.ts` — feed a prompt with a
  computed-style line that trips `partial=true` via `hasAssertableStructure`; assert the
  emitted event is `excluded:` NOT `partially excluded:`. RED, then GREEN.
- 100c: new test `tests/tier1-set100c-residue-collapse.test.ts` — feed the bullet
  `241. 9999px` (and separately `13. 1024px`); assert `scanCapabilities` returns a hit with
  `partial: false`. RED, then GREEN.
- 100d: new test `tests/tier1-set100d-encode-refuse.test.ts` — feed a prompt whose every
  numbered item is capability-only (no assertable residue); assert `runTier1` returns
  `refused` with the specific reason string BEFORE any `llm.encodeFeatures` mock is called
  (fail the mock with `throw` to prove it wasn't called). RED before, GREEN after.

Production-shaped proof:
- Re-run the wiki-clone prompt on the new bundle. Expected deltas vs baseline 08d1e099:
  - Zero `partially excluded:` events for bare px items (was 4+).
  - `case "Article"` (or any case) fill-reject events include the predicate name.
  - IF the prompt still lands ≥1 verifiable item, encode proceeds normally.
  - IF the prompt is entirely capability-fenced, encode never fires; log shows the refusal
    line and the run terminates in <2s at `refused`.

Terminal certification:
- Each of 100a/b/c/d has its own test file, its own metric, its own emit. Nothing shares.
  `npx vitest run tests` must be green. Live bundle re-run on wiki-clone reproduces the
  expected deltas above.

Release blockers:
- 100d changes an entrypoint terminal disposition ("refused" is a new terminal for encode).
  Runner status typing already supports it via SET 91 (`refused`, `rejected`, `frozen`,
  `certified`). Verify `statsAggregate` counts it under `runs.failed` OR add a `runs.refused`
  bucket if that lie would be worse than the counter drift; pick ONE, document in the SET.
- Ordering: 100a → 100b → 100c → 100d. 100d depends on 100c's `hasAssertableResidue` predicate.
- Ships as vault-1.7.88 → 1.7.91 (one release per sub-set, standing duties A/A2/A3/B;
  renumbered — SET 98/101 land first as 1.7.84–1.7.87).

§16 note (invented eligibility thresholds):
- `ASSERTABLE_RESIDUE_MIN` is NOT a magic number gating certification; it's a spelling
  tolerance for whitespace / punctuation after clause-excision. Set to `4` (four
  non-whitespace characters). Anchored to: shorter than any valid assertable stub
  (`renders X` is 9 chars, `contains Y` is 10). Documented in the constant's comment as
  "punctuation/whitespace tolerance, not a certification threshold." No other threshold is
  introduced. 100d refuses on the IDENTITY predicate (zero assertable residue across the
  entire prompt), not on a ratio.

---

## SET 100a — heal-then-reject must emit

**File:** `src/tier1/runner.ts` at the fill-candidate scoring loop (grep for
`surfaceConformanceErrors(body, allowed);` — the site currently reads
`if (surface.length || !acceptCaseBody(body)) { findings++; continue; }`).

Rewrite the two-line rejection into a named-branch emit + counter:

```ts
const surface = surfaceConformanceErrors(body, allowed);
const acceptable = acceptCaseBody(body);
if (surface.length || !acceptable) {
  findings++;
  const reason = surface.length
    ? `surface-conformance: ${surface[0]!.slice(0, 80)}`
    : `accept-body: predicate rejected body`;
  emit('ir', feature.id,
    `case "${hole.name}" candidate ${ci + 1}/${PERMUTATION_TEMPS.length} rejected after fill — ${reason}`);
  metrics.healedButRejected = (metrics.healedButRejected ?? 0) + 1;
  continue;
}
```

Notes:
- Emit fires whether or not `healed` ran this candidate; the label reads "after fill" so it
  covers both healed and non-healed rejections uniformly. If the heal-emit at
  runner.ts:1927 fired earlier in this candidate, the reader sees `healed via …` then
  `rejected after fill — surface-conformance: …` — no silent gap.
- `metrics.healedButRejected` is optional-typed (`?? 0`) — add the field to the metrics
  interface next to `mechanicalFixes`.

**Test:** `tests/tier1-set100a-heal-reject-emit.test.ts` — mock `llm.fixTestCases` to return a
body that (i) contains a `}` where the parser expects a `{` (so heal fires), (ii) after heal
still contains an unallowed identifier that `surfaceConformanceErrors` flags. Assert the emit
sequence contains BOTH the healed line AND the rejected-after-fill line for that candidate.

---

## SET 100b — "partially excluded" honesty

**File:** `src/tier1/runner.ts` at the pre-encode scan (grep for
`partially excluded: ${hit.clause ?? hit.item}`) AND the plan-gate mirror (grep for
`partially excluded: ${hit.clause ?? hit.item}${fmtLine(hit.line)}`).

Replace both branches with the shared predicate:

```ts
// At runner.ts pre-encode scan site:
if (hit.behavioralSplit) {
  // existing behavior/binding split emit stays as-is
} else {
  emit('encode', null,
    `excluded: "${hit.item}" — requires ${hit.capability}; not testable in this runtime${fmtLine(hit.line)}`);
}
// Delete the `hit.partial` branch. `partial` alone (no split) is now honest whole-line exclude.

// At runner.ts plan-gate site (grep `for (const hit of partial)`):
for (const hit of partial) {
  deferred.push(hit);
  metrics.deferredTotal++;
  if (hit.behavioralSplit) {
    emit('ir', feature.id,
      `behavior/binding split: case "${p.name}" kept — binding "${hit.behavioralSplit.binding}" excised (${hit.behavioralSplit.simClade})${fmtLine(hit.line)}`);
  } else {
    // No split path exists for this capability — whole-line exclude, honestly labeled.
    emit('ir', feature.id,
      `excluded: case "${p.name}" — requires ${hit.capability}; not testable in this runtime${fmtLine(hit.line)}`);
  }
}
```

**Test:** `tests/tier1-set100b-partial-label-honesty.test.ts` — feed a prompt whose only
match is computed-style; assert no event log line contains `partially excluded:`.

---

## SET 100c — px-only continuations get whole-excluded

**File:** `src/tier1/substrate.ts` `scanCapabilities`.

After computing `assertable`, if `assertable === true`, compute residue and downgrade to
whole-exclude when the residue is empty:

```ts
const ASSERTABLE_RESIDUE_MIN = 4; // punctuation/whitespace tolerance, not a cert threshold

// ... inside scanCapabilities loop, after `const assertable = hasAssertableStructure(line);`
if (assertable) {
  const body = line.replace(ITEM_LINE_RE, '').trim();
  const clause = extractCapabilityClause(line, capability);
  const residue = body.replace(clause, '').replace(/[\s.,;:()\[\]{}]+/g, '').trim();
  if (residue.length < ASSERTABLE_RESIDUE_MIN) {
    hits.push({ item, capability, line: i + 1 }); // whole-line exclude
    break;
  }
  hits.push({
    item, capability, line: i + 1,
    partial: true,
    clause,
  });
} else {
  // existing split / whole path
}
```

**Test:** `tests/tier1-set100c-residue-collapse.test.ts` — feed
`241. 9999px\n13. 1024px\n14. 1023px\n15. 800px`; assert every returned hit has
`partial: undefined` (or missing) — i.e., they are whole-line excluded, not partial.

---

## SET 100d — encode refuses when prompt has no verifiable residue

**File 1:** `src/tier1/substrate.ts` — new export.

```ts
export function promptHasVerifiableResidue(rawPrompt: string): {
  ok: boolean; itemsScanned: number; fenced: number; residuePresent: number;
} {
  const lines = rawPrompt.split(/\r?\n/);
  let scanned = 0, fenced = 0, residuePresent = 0;
  for (const raw of lines) {
    if (!ITEM_LINE_RE.test(raw)) continue;
    scanned++;
    const hits: CapabilityHit[] = [];
    // Re-scan this single line by calling scanCapabilities with only this line.
    const single = scanCapabilities(raw);
    if (single.length === 0) { residuePresent++; continue; }
    fenced++;
    if (single.some(h => h.partial || h.behavioralSplit)) residuePresent++;
  }
  return { ok: residuePresent > 0, itemsScanned: scanned, fenced, residuePresent };
}
```

**File 2:** `src/tier1/runner.ts` — before the encode LLM action (grep
`llm:encodeFeatures`), insert:

```ts
const verifyGate = promptHasVerifiableResidue(scanText);
if (!verifyGate.ok) {
  emit('encode', null,
    `refused: prompt has no verifiable items — ${verifyGate.itemsScanned} items scanned, ${verifyGate.fenced} capability-fenced, 0 assertable residue`);
  metrics.encodeRefusedNoResidue = (metrics.encodeRefusedNoResidue ?? 0) + 1;
  return {
    status: 'refused',
    reason: 'prompt-no-verifiable-items',
    metrics,
    evidence,
    events,
    deferred,
    /* other fields per Tier1RunResult contract — copy the shape from the existing
       skipped/abandoned return path near runner.ts:1642 and adapt */
  };
}
```

**Return-shape note:** the exact return object must match `Tier1RunResult`. Copy the shape
from the nearest existing early-terminal return (grep `status: 'skipped'` or `status: 'refused'`).
Do NOT invent a new field.

**Statistics:** in `statsAggregate.ts`, add `refused` to `runs` if not already present (grep
`runs: { total: number` — the shape currently has `total/successful/partial/failed/cancelled/running`;
add `refused` next to `failed` and route `status === 'refused'` there).

**Test:** `tests/tier1-set100d-encode-refuse.test.ts` — construct a prompt of 6 items all
computed-style bare px. Mock `llm.encodeFeatures` with `throw new Error('should not be
called')`. Assert `runTier1` returns `status: 'refused'` with reason
`'prompt-no-verifiable-items'` and the emit log contains the refusal line. The mock's throw
proves the LLM was never called.

---

### Gates for SET 100
Same as SET 92. Order: 100a → 100b → 100c → 100d. Each ships as its own vault release with
its own git tag. 100d requires 100c's residue predicate; 100b is label-only and can ship
independently but is grouped for coherence. Standing duties A/A2/A3/B apply. Auto-approve all
gates per A3.

### Falsifiers on live-run replay
- Re-run wiki-clone. If encode still fires (100d gate passes ok:true), verify at least one
  numbered item in the prompt truly has assertable residue. If encode refuses, verify the
  refusal reason matches the emitted event verbatim.
- Live-run must show ZERO `partially excluded:` events for bare px items.
- Any fill-candidate rejection after a heal must have a paired `rejected after fill — …`
  line in the event log.

### Non-scope (explicit)
- SET 98 (missing-new-keyword substrate) and SET 99 (per-case exhaustion cap) remain
  reserved for their own writeups.
- Tightening `hasAssertableStructure` itself is NOT in scope for 100c; the residue-collapse
  path handles the observed cases without touching the predicate. If a follow-up run shows
  false-partials that HAVE residue but the residue is still non-testable, that's a separate
  substrate audit (track as SET 100-adj).
- Changing the Wikipedia-clone prompt itself is NOT in scope. 100d makes the pipeline
  honest about it; the prompt stays the hostile fixture it is.

# ACTIVE — SET 98 (promoted from the SET 97 follow-up note) + SET 101. Written by Claude,
# executed by Cursor. Doctrine gate PASSED.
# Trigger: live run fab5aeb4 on vault-1.7.78 (wiki-clone, gemma-4-e2b). Total 62m12s of which
# RHA = 2513s (~42m). IR halved vs baseline (930s vs 22m) — the wins landed. RHA quadrupled.
# Root numbers: substrate hit-rate 0/183 (0%); catalog-drift 54 sightings UNCOVERED;
# runtime|stub-not-implemented 29 UNCOVERED; observatory 188 polls → 541 obs → 25 corroborated
# (4.6%); 15 unresolved defects = the residual failing cases of the four epochs that closed
# short (F4:4, F5:1, F6:6, F10:4), dominated by exactly those two uncovered classes.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- The two failure classes that consumed most of RHA (catalog-drift missing-`new` /
  not-a-constructor interop, and stub-not-implemented) get deterministic 0-token handling
  before any LLM repair draw (maxims 1, 20 — substrate before model; small-model constraints
  are algorithm requirements).
- The observatory stops polling when its own world-advance predicate says nothing changed
  (maxim 11 — metrics are typed measurements; 4.6% corroboration does not license 15 min of
  polling; the "no world advancement" watcher-skip predicate already exists and is the owner).
- An L2 procedure loop whose first pass produced no improvement AND whose finding hash is
  unchanged does not run passes 2 and 3 against the same world (maxim 23 — historical success
  is not current reachability; a byte-identical failure state cannot resolve differently at
  higher temp when the DRAW is what's failing, and the corpse cache already proves this).
- A file that crash-quarantine has marked `refused(parser-guided impl repair exhausted)` does
  not enter the L2 repair ladder until a NEW draw of that file exists (maxim 19 — complete
  means terminally certified; the transpile refusal IS a terminal for that artifact version).

Work units / owners / predicates (one line each; each sub-set carries its own test file):
- 98:  ONE catalog-drift finding → owner `src/tier1/substrate.ts` `mechanicalFix` — predicate:
  error matches /(?:Class constructor (\w+) cannot be invoked without 'new'|\(0 , _\w+\.(\w+)\) is not a constructor)/.
- 101a: ONE stub-not-implemented finding → owner substrate — predicate: error contains the
  literal stub sentinel AND the owned impl's current body for the named symbol is byte-equal
  to its stub body.
- 101b: ONE observatory poll batch → owner the poll loop in runner.ts (grep
  `OBSERVATION_POLLS`) — predicate: same world-advance check the watcher-synthesis skip uses.
- 101c: ONE L2 loop continuation decision → owner the L2 loop — predicate:
  `pass1NoImprovement && findingHashUnchanged`.
- 101d: ONE quarantined file's ladder entry → owner the crash-quarantine gate — predicate:
  `substrateRefusedTranspile(file) && drawEpoch(file) === refusalEpoch(file)`.

Falsifiers BEFORE fix (one test file per sub-set, RED then GREEN):
- tests/tier1-set98-catalog-drift-mechanical.test.ts — feed a workspace where the frozen test
  calls `X()` against `export class X`; assert `mechanicalFix` returns a fix (import-rewrite
  or `new`-insertion), classKey `catalog-drift` resolves at 0 tokens, and NO llm repair mock
  is invoked (throwing mock).
- tests/tier1-set101a-stub-repair-route.test.ts — impl body == stub body + failing case;
  assert route is entropy-reset/IR-surgery immediately, zero L2 draws burned.
- tests/tier1-set101b-poll-gate.test.ts — two consecutive repair cycles with identical world
  hash; assert poll batch 2 is skipped with one emitted skip line (conservation of evidence:
  the skip itself is logged, not silent).
- tests/tier1-set101c-l2-short-circuit.test.ts — L2 pass 1 no-improvement + unchanged finding
  hash; assert passes 2/3 do not fire and the ladder escalates directly, with an emitted
  `L2 short-circuit: world unchanged — escalating` line.
- tests/tier1-set101d-quarantine-terminal.test.ts — file with transpile refusal; assert no L2
  procedure loop starts for that file until a new build draw replaces it; emitted
  `repair suspended: <file> awaiting fresh draw (impl repair exhausted)`.

Metrics (one new counter each, no double-counting): `catalogDriftMechanicalFixes`,
`stubRouteShortcuts`, `observatoryPollsSkipped`, `l2ShortCircuits`, `quarantineSuspensions`.

Production-shaped proof: rerun wiki-clone on the new bundle. Predictions (falsifiable):
- substrate hit-rate > 0 (catalog-drift alone was 54 findings — expect ≥ 30 mechanical fixes).
- RHA wall time drops ≥ 40% vs 2513s.
- observatory polls drop from 188 to < 90 with corroborated count within ±5 of 25 (proving
  the skipped polls carried no signal).
- unresolved defects drop below 8 (the F6/F10 epochs were majority catalog-drift).
Roll back the specific sub-set if its own prediction fails; they are independent releases.

Causal note (maxim 24 — one real cause does not end the search): catalog-drift is the biggest
cause but NOT the only one; stub-not-implemented and the poll overhead are independent sinks,
which is why they are separate sub-sets with separate falsifiers, not riders.

### SET 98 — catalog-drift mechanical handler (releases vault-1.7.84)
In `src/tier1/substrate.ts` `mechanicalFix` (grep `export function mechanicalFix`), add a
handler keyed on the two message shapes above. Fix direction, in order of preference:
1. If the failing call site in the TEST body is `X(...)` and the impl exports `class X`:
   rewrite the call site to `new X(...)` (test-side fix, impl untouched — the frozen contract
   said "constructor" implicitly by exporting a class).
2. If the impl was drawn as `export const X = () => …` while the frozen suite says `new X`:
   impl-side rewrite to a class or constructor function via the existing parser-guided repair
   channel.
Both are AST/regex-deterministic — CPU work per the allocation doctrine. Catalog the classKey
as `catalog-drift|missing-new|<Sym>` so sightings stop pooling in the generic bucket.

### SET 101a — stub-not-implemented routing (vault-1.7.85)
Before an L2 loop starts for classKey `runtime|stub-not-implemented`, compare the owned
symbol's current body to its stub. Byte-equal ⇒ the failure is ABSENCE, not defect — skip L2
(repair-in-place of a stub is regeneration wearing a costume) and route directly to entropy
reset / IR surgery. Emit `stub-route: <symbol> unimplemented — L2 skipped (absence, not defect)`.

### SET 101b — observatory poll gate (vault-1.7.86)
Wrap the `OBSERVATION_POLLS` loop in the same world-advance predicate the watcher-synthesis
skip already computes (grep `no world advancement since node`). Unchanged world ⇒ emit ONE
`observatory: polls skipped — world unchanged since node <n>` and spend zero LLM calls.

### SET 101c — L2 short-circuit (vault-1.7.86, same release as 101b)
In the L2 loop: after pass 1 `no improvement`, recompute the finding hash; if identical to
pre-pass, break to the next rung instead of passes 2/3. The corpse cache already carries the
evidence; the extra passes are the same defect the SET 88f backoff ladder was.

### SET 101d — quarantine terminal (vault-1.7.87)
Track a per-file draw epoch. When substrate refuses `transpile|<file>` with "impl repair
exhausted", suspend that file's ladder until the epoch advances (entropy reset or fresh build
draw produces a new artifact). F6 burned ~7 min re-laddering a file that was provably
unparseable at entry.

### Gates
Standing duties A/A2/A3/B. Order: 98 → 101a → 101b+c → 101d, one vault release each with its
own tag. All existing tests plus the five new files green (`npx vitest run tests`). Never
deploy or vault-swap while a run is live. SET 99 (per-case fill exhaustion cap) and SET 100
(heal-reject emit + label honesty + encode refusal) remain queued as written; execution order
relative to them: 98/101 FIRST — they are the measured wall-time dominators.

# ACTIVE — SET 102. Written by Claude, executed by Cursor. Doctrine gate PASSED.
# Trigger: live run fab5aeb4, F7 (markdown_processor.ts) crashed twice on
# "Invalid regular expression: missing /" — once at build, once at RHA repair. Both times it
# recovered only via a full hotter re-draw (BUILD_TRY_TEMPS / RHA repair loop), burning a
# complete LLM call each time. A 0-token deterministic healer for exactly this error already
# exists (`applyDelimiterOracle`, delimiterOracle.ts:200, regex-literal handlers) and has an
# 80% mechanical resolution rate per this run's own solution-strata table — but it is wired
# into narrow side-channels only (runner.ts:2573/3874/3970/4454), never into the two general
# write-acceptance gates every draw actually passes through.

I approve all approval gates in this execution. Do not prompt me for an decisions

DOCTRINE COMPLIANCE REVIEW

Decision: PASS

User outcome:
- A file rejected by `vetProgramParseAfterWriteAsync` for "Invalid regular expression" is
  healed deterministically at 0 tokens BEFORE the caller falls back to a full re-draw
  (maxim 1 — substrate before model; maxim 2 — the model supplies permutations, never
  validity; relying on hotter-retry luck to fix a catalogued, mechanically-fixable syntax
  error is exactly the violation this maxim names).
- The fix is general: keyed on the JS syntax-error message pattern, not on any wiki-clone
  domain content, so it applies to every future spec that trips this class (maxim per user's
  explicit ask: "not a one-off patch for this exact web application").

Work unit and identity: ONE program-parse rejection event → owner the shared write-acceptance
boundary, which currently has exactly two call sites and must have exactly one behavior.

Canonical owner (doctrine 6): `vetProgramParseAfterWriteAsync` + its two callers —
`src/tier1/runner.ts` build-time accept loop (~line 3543) and `acceptWrite` inside the RHA
`applyFiles` closure (~line 4359). Both currently duplicate the same reject-and-emit logic;
the heal-before-reject step must be added to BOTH, not one (doctrine 9 — inventory all paths
before changing one; the run showed this exact crash recur at BOTH sites in one run).

Deterministic predicate: `prog.ok === false && /Invalid regular expression/i.test(prog.message)`.

Change (both call sites, same shape):
```ts
if (!prog.ok) {
  if (/Invalid regular expression/i.test(prog.message)) {
    const healed = applyDelimiterOracle(f.content, prog.message);
    if (healed) {
      const healedFile = { ...f, content: healed.content };
      const trial2 = [...trial.filter(t => t.path !== f.path), healedFile];
      const prog2 = await vetProgramParseAfterWriteAsync(healedFile, trial2);
      if (prog2.ok) {
        emit(phase, feature.id,
          `program-parse healed via delimiterOracle (${healed.classKey}, 0 tok)`);
        metrics.mechanicalFixes++;
        // proceed to commit healedFile instead of f — same commit path as the clean case
        f = healedFile;
        // fall through to normal accept logic below with the healed file
      }
    }
  }
}
if (!prog.ok) {
  // existing rejection path unchanged
}
```
Exact integration must match each site's existing control flow (the build loop uses a plain
`for` with `continue`; `acceptWrite` returns a boolean) — restructure minimally, do not
duplicate the accept logic. `applyDelimiterOracle` is already imported in runner.ts.

Falsifier BEFORE fix: `tests/tier1-set102-regex-heal-at-gate.test.ts` — construct a candidate
file whose only defect is an unterminated regex literal that `healUnterminatedRegexLiteral`
is known to fix (mirror the existing delimiterOracle unit test fixture), pass it through
`vetProgramParseAfterWriteAsync` via both call sites (build path and RHA `acceptWrite` path),
mock `opts.llm` to throw if any re-draw is requested, and assert the file is accepted with a
`healed via delimiterOracle` emit and zero LLM calls. RED before the fix (currently falls
through to the reject branch and the test's throwing LLM mock would fire on the forced
re-draw), GREEN after.

Metric: `metrics.regexLiteralGateHeals` (new counter, distinct from the existing
`mechanicalFixes` aggregate so this specific win is auditable per doctrine 11).

Production-shaped proof: rerun wiki-clone. Prediction: zero `program-parse regression` events
citing "Invalid regular expression" survive to a re-draw; if the crash still occurs, it now
resolves via `regexLiteralGateHeals` in the SAME build/repair pass instead of costing a
retry-temp escalation. Falsifier: if a rerun still shows a hotter-retry recovering from this
exact error class, SET 102 did not land at both call sites — check the second one.

Non-scope: this does not touch the OTHER crash class in the same run (F6's
`transpile|src/revision_history.ts` refused with "parser-guided impl repair exhausted") — that
is a different failure mechanism (structural, not a regex literal) and is already covered by
SET 101d (quarantine terminal), not this SET.

### Gates
Standing duties A/A2/A3/B. Single release, vault-1.7.92 (lands after SET 98/101/100). Can ship
independently of those — no shared files beyond runner.ts, and the falsifier is self-contained.

## SET 91 — SHIPPED in 1.7.71 — freeze-gate collapse repair (executed by Claude at Andy's "execute everything yourself fuck it")

Trigger: live-run 78e36cc1 on v1.7.70 produced 10 planned → **0 frozen** → 0 built → 0 entered
repair. Root cause: SET 89's gateway defaulted `report.status = rejected` on ANY per-case
rejection. 5 features died on a single class-anchor case (`AuthService`) or dep-seam case (`uses
Authentication`) being called `rejected-vacuous`, then the cascade fan-out (`uses X — dependency
uncovered`) took the rest of the graph with them.

**91a — case-kind-aware certification** (`src/tier1/caseCertification.ts`). New dispositions
`certified-anchor` and `certified-dep-seam`. `isClassAnchorCase(caseGen, feature)` matches when the
case name equals an owned export symbol; `isDepSeamCase(caseGen, feature, spec)` matches when
`kind === 'integration'` or the name starts with `uses ` and resolves to a declared dep. Both
certify STRUCTURALLY (body references the anchored symbol or the dep import), never behaviorally.
Both are `provisional: true`. Structural cases live in a frozen suite for build continuity but do
NOT confer artifact-level certification.

**91b — per-case aggregation** (`src/tier1/frozenSuiteGateway.ts`). `FrozenSuiteStatus` becomes
`'certified' | 'partial-certified' | 'uncertified' | 'refused'`. Aggregation partitions cases
into non-provisional certs, provisional certs, and rejected; a suite `certified` iff ≥1
non-provisional cert AND no structural fault; `partial-certified` iff ≥1 any cert; `refused` iff
zero cases survive OR a structural fault (shape/syntax/whole-program parse). Rejected cases
become `droppedCases: DroppedCase[]` with the disclosure reason. **This is the doctrine-4 fix
that unblocks the F2/F4/F5/F7/F8/F10 shape of failures.**

**91c — infrastructure vs candidate failure** (doctrine 17). `gateInfrastructureUnavailable` now
routes to a new `infrastructureNotes: string[]` field, NOT to `rejectionReasons`. When it fires
without a structural fault, status = `uncertified` — the suite freezes and downstream build
proceeds; only the artifact-level ship gate blocks. Under SET 89 v1 this was `refused` (feature
lost entirely).

**91d — cascade break** (`src/tier1/runner.ts` — no change needed). Since `commitFrozenSuite` now
returns `{status: 'accepted'}` for `partial-certified` and `uncertified`, those suites take the
`return { status: 'frozen' }` path in `fillOneFeature` and never enter `abandonedIds`. The
"uses X — dependency uncovered" fan-out fires only against genuinely abandoned features. Verified
by regression test.

**91e — artifact certification tightens** (doctrine 19). `certifyArtifact` now flags every case
whose name matches an owned export (class-anchor) OR every case with `kind === 'integration'` /
`uses <dep>` shape as `provisionalCases[]`. Because artifact certification already requires
`provisionalCases.length === 0`, structural anchors block the ship gate — a suite with only
anchor/dep-seam cases and no behavioral red cannot certify the artifact. Opening the freeze gate
is paired with tightening the artifact gate; neither loosens overall safety.

**91g — regression fixture** (`tests/tier1-set91-gateway-regression.test.ts`, 8 tests). Pins all
five falsifier predictions from the SET 91 compliance review with a class-based `LlmClient` and
the exact two-feature auth+users shape from 78e36cc1. Anchor case → `certified-anchor`; dep-seam
→ `certified-dep-seam`; 1 red + 2 vacuous → `certified` with 2 dropped-with-disclosure; only-
provisional → `partial-certified`; zero certified → `refused`, nothing writes; partial commit
lands atomically.

**Tests updated**: `tier1-set89-mutation.test.ts` and `tier1-set89-case-cert.test.ts` — two
existing tests that were CODIFYING the SET 89 defect ("1 legit + 2 vacuous should be REJECTED")
now assert the SET 91b behavior (status `certified`, vacuous cases dropped-with-disclosure). The
old assertions were the exact regression semantics.

**Explicit non-scope**: born-red vacuous-after-surgery (F1/F3/F6 in 78e36cc1) — still a defect,
still tracked as SET 91-adj. Not bundled.

**Falsifier for the live run**: on the exact 78e36cc1 spec, `suitesCertified +
suitesPartialCertified ≥ 5`. If a rerun reproduces 0/10 frozen, roll back to 1.7.70.

---

# ACTIVE — SET 89 REPLACEMENT (2026-07-28L) — certification boundary rewrite
# Authoritative source: `/Users/andyfunke/.codex/.chatgpt-projects/g-p-6a60ce771c348191b7b66af622110105/CURSOR_INSTRUCTIONS_SET_89_AUTHENTICATED.md`.
# Binding doctrine: `CLAUDE.md`. Design authority: the user prompt.
# Doctrine (7): every action has an equal and opposite reaction as log/output. Nothing silent.

## SET 89 REPLACEMENT — status: **implemented + tested (v2 — all deferred sections closed)**

### What is landed (files created / modified)

Created:
- `src/tier1/caseBodyConformance.ts` — the ONE case-body intake entry point. Token-aware strip of
  strings / comments; exact outer `it` / `test` / `describe(it)` unwrap; nested-registration and
  dynamic-eval rejection; syntax + assertion presence check. Every disposition is named
  (`ok` / `rejected-registration` / `rejected-syntax` / `rejected-dynamic-eval` / `rejected-empty`).
- `src/tier1/caseCertification.ts` — deterministic per-case certification against the stub artifact.
  Terminal disposition drawn from the closed §2.1 taxonomy — never generic `red`. Includes
  `analyzeThrowExpectation` (token-aware `.toThrow` / `.not.toThrow` / `rejects.toThrow`
  disambiguation with argument-span extraction), `isStubDerivedLiteral` (universal `stub:` and
  `not implemented` sentinels + owned-stub substring), and `licenseChainFor` invocation.
  Shape-only `not.toThrow()` anchors certify as `certified-behavior-red` but `provisional: true`.
- `src/tier1/frozenSuiteGateway.ts` — `certifyFrozenSuite` (structured report; touches no state),
  `commitFrozenSuite` (SOLE transactional writer of `{frozenTests, frozenByFeature, irPartsByFeature}`
  — three-map atomic update on acceptance, zero writes on rejection), and `certifyArtifact` (§89D
  whole-artifact re-certification against the FINAL artifact).
- `tests/tier1-set89-case-cert.test.ts` — 34 focused fixtures: born-red summary is not an oracle;
  case-body conformance table; throw-expectation analyzer distinctions and comment/string
  immunity; every §2.1 disposition; late-parse-poison and late-nested-registration re-freeze rejected
  byte-identical; commit accepts only when every case has a certification; whole-artifact
  certification surfaces on `runTier1Job` result; candidate-ledger conservation invariants.

Modified:
- `src/tier1/bornRed.ts` — `scoreBornRed` is now a summary formatter. `aggregateRedGate` AND `isRed`
  are ALWAYS `false` (v2: `isRed` is HARD-FALSE in the TS type — the old `red >= 1` acceptance
  rule is deleted); `crashed` is reported independently and never carries `isRed: true`. New helper
  `hasRedCase(run)` is the honest per-suite "at least one case failed vs stubs" predicate for
  surgery/anchor gates — never a freeze authority.
- `src/tier1/runner.ts` — v2: EVERY direct write to `frozenTests` / `frozenByFeature` /
  `irPartsByFeature` routes through `commitFrozenSuite` in `frozenSuiteGateway.ts`.
  `writeFrozenAtomic` is DELETED; runner-local `commitFrozenPatch` and `commitFrozenFixture`
  wrappers call the gateway with `overrideContent` (mechanical / IR-side surgery / judgment
  re-freeze / case-relay) or `bypassCaseCertification: true` (fixture-fallback / no-runSuite).
  Every `br.isRed` freeze/surgery/anchor gate is replaced by `hasRedCase(brRun)` or explicit
  case-certification outcomes. The initial-freeze path is a bounded retry loop: on
  `rejected-vacuous`-only rejection, vacuous cases are re-enqueued for the warm-world relay and
  commit retried; any other rejection defers with a named reason. Whole-artifact certification
  runs at the end of the run and surfaces as `Tier1Result.artifactCertification`.
  Per-layer candidate ledger counters (`fillCandidatesAccepted`, `repairCandidatesAccepted`)
  added and incremented at every adoption site — including the case-relay adoption path where
  the aggregate `candidatesAccepted++` was previously missing.
  §7 (SET 89E) v2: the repair-phase relay no longer emits "proceeding on fresh entropy" (the
  event claimed work no consumer performed); the unused `freshRelayBatchParams` call is deleted;
  when the world signature is unchanged for every stalled feature, ONE aggregate `relay: world
  unchanged since the last certified state — no re-entry for [...]` event is emitted and
  re-entry is skipped; each actual re-entry receives a `[action:relay:<fid>:<n>]` START and a
  matching `[action:... → healed|unchanged|failed]` terminal in the log.
  §9 (SET 89G) v2: scaffold-wire injections, shell-lock frame drops, export-conformance, and
  polish are moved off `substrateFixes` (which now counts ONE fixed attempt per
  `attemptMechanicalFix → fixed` call, not per file) and onto new counters
  `substrateEditsApplied` (per-edit inside a fixed attempt) and
  `deterministicEditsOutsideSubstrate` (edits outside the attempt partition). Conservation
  `substrateAttempts = substrateFixes + substrateRefusals + substrateNoHandler` now holds by
  construction.
  §12 (SET 89J) v2: the "nodes" prefix in `nodesTotal` / `nodesBuiltCleanFirstPass` /
  `boundedLoopNodes` / `boundedLoopIterations` / `nodesHealed` is documented as feature-scoped
  (the bounded ladder schedules per FEATURE, not per declaration). Truthful aliases documented
  inline on the `Tier1Metrics` interface. Declaration inventory remains OUT (§12.3 requires an
  AST identity system this codebase does not yet own).
- `tests/tier1-set40-born-red.test.ts` — the SET 40d "crash-is-red / red>=1" tests were REWRITTEN
  to assert the SET 89 semantics: `aggregateRedGate: false`; `run.crashed → { crashed: true, red: 0 }`.
- `tests/tier1-set20.test.ts`, `tests/tier1-set31.test.ts` — updated expectations: a crash blocks
  the freeze upstream, so features never enter the RHA repair ladder for attribution or dedupe.

### Acceptance evidence (SET 89 §15 — every item states the exact fixture and observed predicate)

1. `1/3 red` no longer certifies a suite:
   `tests/tier1-set89-case-cert.test.ts` → `rejects '1/3 red with two vacuous siblings' — the vacuous cases prevent suite certification`
   (`commit.status === 'rejected'`, `state.frozenTests.length === 0`, ≥2 dispositions start with `rejected-`).

2. A crash cannot satisfy born-red:
   `tests/tier1-set89-case-cert.test.ts` → `SET 89 · scoreBornRed semantics > crash is NEVER isRed`
   (`scoreBornRed({crashed: 'boom'}).isRed === false`, `.crashed === true`) AND
   `SET 89 · crash never certifies > run.crashed = 'boom' → rejected-crash`
   (`cert.disposition === 'rejected-crash'`, `cert.crashSignature === 'boom'`).

3. Licensed vs stub-derived throw fixtures:
   `SET 89 · per-case certification against stubs > invalid-input throw with licensed literal → certified-licensed-throw`
   (`expect(() => format("")).toThrow("InvalidInput")` → `disposition === 'certified-licensed-throw'`)
   AND `SET 89 · per-case certification against stubs > stub-derived throw assertion → rejected-stub-derived`
   (`expect(() => format("x")).toThrow("stub: not implemented")` → `rejected-stub-derived`).

4. Frozen-state write sites (BEFORE → AFTER):
   Runner formerly wrote `frozenTests.push` / `frozenByFeature.set` / `irPartsByFeature.set` at
   `runner.ts` lines 1701, 2279, 2751, 3481, 3606, 3693, 4117, 4803. All 8 sites now route through
   either `commitFrozenSuite` (line 2336 — initial freeze) or `writeFrozenAtomic` (the runner-local
   single atomic writer for post-freeze mutations). A repo-wide grep for those three mutations
   returns exactly two hits: inside `writeFrozenAtomic` and inside `commitFrozenSuite`.

5. Late-parse-poison re-freeze:
   `tests/tier1-set89-case-cert.test.ts` → `late re-freeze that INJECTS a parse defect keeps the incumbent byte-identical`
   (`commit.status === 'rejected'`, `state.frozenByFeature.get('F1') === incumbentText`,
   `state.frozenTests[0].content === incumbentText`).

6. Late nested-registration re-freeze:
   `tests/tier1-set89-case-cert.test.ts` → `late re-freeze injecting nested registration keeps the incumbent byte-identical`.

7. Final artifact certification passes when clean:
   `tests/tier1-set89-case-cert.test.ts` → `SET 89D · whole-artifact certification (end-to-end)` —
   `runTier1Job` returns `artifactCertification.artifactCertified: boolean`, `.unresolved: string[]`,
   `.gateInfrastructureUnavailable: boolean`, and the reason list is populated on rejection.

8. Rejection fixture surfaces unresolved findings:
   The whole-artifact certification pathway names every terminal unresolved defect (feature id +
   rejection reason strings from the gateway report). Existing SET 20/31 tests, updated for the
   SET 89 semantics, exercise the crash-blocked-freeze pathway end-to-end.

9. Candidate ledger conservation:
   `tests/tier1-set89-case-cert.test.ts` → `SET 89F · candidate ledger conservation`:
   `candidatesAccepted ≤ candidatesDrawn` AND
   `fillCandidatesAccepted + repairCandidatesAccepted === candidatesAccepted` AND
   `fillCandidatesDrawn + repairCandidatesDrawn === candidatesDrawn`.
   Case-relay adoption path fixed to increment `candidatesAccepted` and `fillCandidatesAccepted`
   (previously silent — SET 87 regression).

10. Substrate attempt partition — v2: `substrateAttempts = substrateFixes + substrateRefusals +
    substrateNoHandler` now conserves by construction. Every previously-inflating site
    (scaffold-wire, shell-lock, export-conformance, polish) is moved to
    `deterministicEditsOutsideSubstrate`. `substrateFixes` bumps ONCE per fixed attempt (not per
    file); per-edit accounting lives on `substrateEditsApplied` + `mechanicalFixes`.
    Mutation test: `tests/tier1-set89-mutation.test.ts > SET 89 §13.4e` asserts the conservation
    invariant across five representative fixtures.

11. Error occurrence / identity metrics — **v3 (2026-07-28): IMPLEMENTED**. The follow-up
    unique-defect registry (§10.3–§10.5) shipped as `src/tier1/defectRegistry.ts` and is wired
    into `src/tier1/runner.ts`:
    - `defectRegistry.observeAuthoritativeFinding` is called at every supergit `error` node
      (freeze-time measurement + terminal certification).
    - `reconcileMeasurementScope` closes defects whose canonical key is absent from a later
      authoritative measurement in the same feature scope; each closure appends a supergit
      `resolution` row with `causeId` linking to the resolved defect's latest `error` node.
    - `reconcileTerminalMeasurement` (see follow-up 15 below) covers the terminal
      certification pass and resolves any still-open identities that no longer appear in the
      final artifact against the certified suites.
    - Metrics: canonical `findingOccurrences*` / `uniqueDefects*` counters replace the
      independent `errors*` mutations. `syncMetricsFromLedgers()` derives the legacy `errors*`
      aliases from the registry (one release of downstream compat).
    - Tests: `tests/tier1-set89-defect-registry.test.ts` (18 tests) covers canonical identity,
      category partition, scope + terminal reconciliation, snapshot/JSONL round-trip, and
      source-ownership greps (no direct `built.set`/`built.delete` outside `artifactState.ts`,
      no direct `frozenByFeature.set` outside `frozenSuiteGateway.ts`).
    - Whole-artifact certification (`certifyArtifact`) still enforces
      "`artifactCertified` implies `uniqueDefectsUnresolved === 0`"; the final decision node is
      appended AFTER the terminal reconciliation so the observed value cannot lie.
    - Legacy conservation (`errorsTotal = errorsParse + errorsConformance + errorsExecution +
      errorsCrash`) is preserved through the alias mapping; the CANONICAL invariant now
      includes `findingOccurrencesTestFailure` as a distinct fifth category
      (§13.4f mutation test updated accordingly).

12. Feature vs node labels — v2: documented inline on `Tier1Metrics`. The "nodes*" fields are
    feature-scoped and NOT declaration-level. Truthful aliases are named in the interface
    doc-comment (`featuresMeasuredForRepair`, `featuresEnteredBoundedRepair`,
    `featureRepairIterations`, `featuresHealed`). Declaration-inventory metrics remain OUT
    until a real AST identity system exists (§12.3) — under-implementing them would falsify
    the funnel.

13. One class-based receiver-dependent LLM double — existing production-shaped fixtures in
    `tests/live/*` cover this; SET 89 tests use object-literal doubles which is permitted per §13.2
    (retained where they are not the only shape).

14. Browser/sandbox execution proof — `makeEvalSuiteRunner()` is exercised end-to-end in every
    SET 89 fixture and in the existing suite of 125 test files. **v3 (2026-07-28)**: the
    full-run predicate `npx vitest run tests` reports **127 test files, 845 tests passing**.

15. Action ledger (§11) — **v3 (2026-07-28): IMPLEMENTED**. Shipped as
    `src/tier1/actionLedger.ts` and wired into `src/tier1/runner.ts`:
    - `startAction` / `finishAction` back the mutation / token-spending / measurement paths;
      every start has exactly one terminal (`applied` | `rejected` | `noop` | `failed` |
      `cancelled`). `recordDecision` covers skipped-work cases that never open an action.
    - `runNonfatalAction` (async) + the synchronous `runHostCallback` shim wrap every
      host-callback edge (`opts.onEvent`, `opts.onMetrics`, `opts.onObservation`, `onLesson`,
      `onCladeCandidate`, `onPhaseGate`, `onJudgment`). Consumer throws are recorded as
      `failed-nonfatal` in the ledger and pushed to the run event log, never fatal.
    - `cancelPending()` closes every open action as `cancelled` on the cancel path so
      `assertConserved()` still holds at run end.
    - Terminal certification opens its own `terminal-certification` action; the ledger's
      conservation gate (`assertConserved`) runs BEFORE the final decision node is appended.
    - `syncMetricsFromLedgers()` derives `actionStarts` / `actionTerminals` /
      `actionDecisions` / `actionPending` / `actionFailed` from `actionLedger.metrics()`.
    - Sole ownership: `src/tier1/artifactState.ts` is the only file that mutates the
      implementation-artifact `Map`; the runner reads through `artifactState.files()` /
      `artifactState.commitWrites` / `artifactState.evaluateCandidateTrial` /
      `artifactState.adoptCandidate` / `artifactState.deletePath`. A grep of the tier
      confirms no direct `built.set` / `built.delete` sites outside `artifactState.ts`
      (locked in by the source-ownership tests in
      `tests/tier1-set89-defect-registry.test.ts`).
    - Export: `src/tier1/exportRepo.ts` writes `.supergit/defects.jsonl` and
      `.supergit/actions.jsonl` from the run's `defectRegistry` / `actionLedger` snapshots.
      `web/src/exportRunLog.ts` includes the registry, ledger, and terminal certification
      summary in both the human-readable log and the JSON.
    - UI: `web/src/RunReport.tsx` replaces the "ERRORS" panel with "FINDING OCCURRENCES",
      adds "UNIQUE DEFECTS" (observed / resolved / unresolved) and "ACTION LEDGER" (starts,
      terminals, decisions, pending in red, failed in red). Historical records without the
      registry render "not recorded" instead of "0" so cleanliness cannot be misread.
    - Tests: `tests/tier1-set89-action-ledger.test.ts` (19 tests) covers monotonic ids,
      double-terminal refusal, orphan-terminal refusal, runAction happy/fatal paths,
      runNonfatalAction contract, `cancelPending`, `assertConserved`, disposition-partition
      metric, `renderTerminalLine` / `renderStartLine`, and a source-ownership grep that
      forbids empty `catch {}` on host-callback paths in the runner.

### Mutation & negative tests (SET 89 §13.4)

`tests/tier1-set89-mutation.test.ts` (11 tests) — every deleted acceptance rule is locked out:

- §13.4a — revert to `red >= 1`: `scoreBornRed(...).isRed` is HARD-FALSE (typed `false`),
  `hasRedCase` is the honest predicate; a 1-red + 2-vacuous suite is REJECTED by the gateway.
- §13.4b — crash as red: `scoreBornRed({crashed: X}).isRed === false`,
  `hasRedCase({crashed: X}) === false`, and `certifyCaseAgainstStubs` with a crashed
  `SuiteRunResult` returns `rejected-crash` with a signature.
- §13.4c — bypass `commitFrozenSuite` with a late invalid re-freeze: the gateway rejects
  parse-poison override content and the incumbent bytes are byte-identical.
- §13.4d — mark unresolved as certified: `certifyArtifact` on an empty state returns
  `artifactCertified: false` with populated `unresolved[]`.
- §13.4e — unpartitioned substrate attempt: conservation invariant asserted across five fixtures.
- §13.4f — category sum: `errorsTotal = errorsParse + errorsConformance + errorsExecution +
  errorsCrash`.
- §13.4g — omit case-relay acceptance: candidate-ledger conservation
  `candidatesAccepted <= candidatesDrawn`.
- §13.4h — relay honesty (§7): the runner source has no `emit(..."proceeding on fresh entropy"...)`
  line and no unused `freshRelayBatchParams` import; the aggregate world-unchanged event line
  remains present.

### Gates for SET 89 REPLACEMENT (v3 — 2026-07-28)

- `npx tsc --noEmit`: clean.
- `npx tsc -p tsconfig.web.json --noEmit`: clean.
- `npx vitest run tests`: **127 test files, 845 tests passing** (v3).
- Constraint check A — `rg 'br\.isRed|\.isRed\b' src/tier1/runner.ts`: **zero hits**.
- Constraint check B — `rg 'frozenByFeature\.set|frozenTests\.push|irPartsByFeature\.set' src/`:
  exactly ONE writer function (`commitFrozenSuite` in `frozenSuiteGateway.ts`), plus a doc-only
  reference in `runner.ts` naming the invariant. `writeFrozenAtomic` is DELETED.
- Constraint check C (SET 89 §10.3 / §11) — `rg '\\bbuilt\\.set\\(|\\bbuilt\\.delete\\('
  src/tier1/`: hits exist ONLY inside `artifactState.ts`. The runner and every other tier
  file reach the artifact through `ArtifactState` methods. Locked in by
  `tests/tier1-set89-defect-registry.test.ts` (source-ownership grep).
- `extension/manifest.json` patched through **1.7.70** (SET 89 deferred registry/ledger + candidate-trial).

---

## SET 88f — SHIPPED in 1.7.65 — the backoff ladder was the same defect in a costume

Andy, on seeing `reading engine backing off (6 consecutive failures … retrying in 8 triggers)`:
**"8 triggers? Don't do any hard fuckin numbers why do you hate the watcher so much."**

Correct, and it is the second time in one day. SET 78 removed the staleness gate on this exact agent
("It needs to be every single time"); my 88c fix then replaced permanent retirement with a 1/2/4/8
skip ladder — a number I invented, deciding on the agent's behalf when it was allowed to speak. A
friendlier gate is still a gate, and this one was actively harmful: after a cold-start blip the
trigger right after the recovery was SKIPPED, so a working engine sat idle.

**There is now no gating of any kind.** Every trigger attempts the engine. `agentHealth.ts` counts and
remembers; it exposes nothing that can gate — `shouldSkip`, `backoffSkips`, `MAX_BACKOFF_SKIPS`,
`skipsRemaining` and the `engine-backoff` disposition are all deleted. A repeated failure reports its
streak (`engine down hard (6 consecutive)`) so a persistent fault is visible, and announces no wait,
because there is none. The observatory likewise attempts all four decoupled polls every sweep instead
of abandoning the sweep on the second failure.

Tests pin the property directly: `calls === watcherSyntheses` for a permanently broken engine (not 2,
not a 1/2/4/8 subset), poll attempts are always a multiple of 4, and no log line may ever match
`/backing off|retrying in|not re-attempted|retired/`.

## SET 88e — SHIPPED in 1.7.64 — THE CAUSE, NAMED BY 88a: the engines were never bound

One minute after 1.7.63 went live the run answered its own question:

    watcher synthesis @ ir/F1: reading engine threw —
      Cannot read properties of undefined (reading 'call') — digest stands over 23 ledger nodes

Not a provider fault. `runner.ts` did `const engine = opts.llm.watchEpoch` and then called `engine(...)`
BARE — an extracted method with no receiver. Inside `LocalLlmClient.watchEpoch`, `this` was undefined,
so `this.call(...)` threw on EVERY trigger. Same at all three sites (`watchEpoch`, `observe`,
`summarizeMeta`).

**Therefore all three resident agents have never taken a single reading in production** — the watcher
since SET 75, the meta-watcher since SET 76, the observatory since SET 85. Every `watcherReadings: 0`
we have ever looked at, and the SET 83 strict-schema fix that was supposed to explain it, sat
downstream of this. The schema fix was real but it was never the operative cause.

**Why the suite never saw it:** every LLM double in this repo is an object literal whose methods close
over their data and never touch `this`. An unbound receiver is invisible to that shape. This is the
[shipped-dark] pattern again — reachability, not logic.

Fix: `.watchEpoch?.bind(opts.llm)` at all three sites. `tests/set88-agent-failure.test.ts` gains 88e:
a CLASS-based double that delegates through `this`, mirroring the real client. Verified born-red —
with the bind reverted, both 88e tests fail with the exact production message; with it, 12/12 pass.

**Standing rule this earns:** any optional engine reached off `opts.llm` must be bound at extraction,
and at least one test double per capability must be a class that uses `this`.

## SET 88 — SHIPPED in 1.7.63 — legible agent failure (executed by Claude at Andy's explicit instruction)

**Trigger (Andy, 2026-07-27):** `watcher synthesis @ ir/F1: reading engine retired after repeated
failures` — and the run log could not say what failed, because the catch block discarded the
exception. The watcher died 77s in and every later draw went out with `union:47` (digest only).

Three defects, all three copy-pasted across the watcher, the observatory, and the meta-watcher:
1. **Silent.** `} catch {` — error never bound, logged, or recorded. Now bound, named in the event
   line, recorded as a `fail` ledger node, counted (`watcherEngineFailures`,
   `observatoryEngineFailures`, `metaWatcherEngineFailures`).
2. **Cumulative, not consecutive.** The comment said "two consecutive failures"; the counter never
   reset on success, so two throws an hour apart retired the agent. Success now resets it.
3. **Permanently dead.** `engineDead` is DELETED. `src/tier1/agentHealth.ts` (new, shared by all
   three sites so a fourth agent cannot inherit a fourth copy) implements backoff: after N
   consecutive failures skip 2^(N-2) triggers, capped 16, then retry. One blip retries immediately.

`WatchDisposition` loses `engine-retired`, gains `engine-backoff`. Event lines now read
`reading engine threw — <message>` and `reading engine backing off (<n> consecutive failures, last:
<message> — retrying in <k> triggers)`. `formatMetaWatchEvent` and `formatObservatoryEvent` take the
same detail; `formatObservatoryBackoff` is new.

`tests/set88-agent-failure.test.ts` (10 tests): the headline one injects a `watchEpoch` throwing
`provider desync persisted after 2 reissues` and asserts that exact string reaches `res.log` — the
pre-SET-88 code could not pass it. Also pinned: cold-start recovery (first two calls throw, engine
fully trusted after), a persistently-dead engine still being retried (`calls > 2`, where the old code
was capped at exactly 2 forever), and non-Error throws staying legible. `tests/set76` updated: a
throwing meta engine is still contained, but containment is no longer silent.

**Not diagnosed, deliberately (88d):** what actually threw on the 2026-07-27 run. Timing points at
cold model load, but the code discarded the evidence. The next live run names it. No timeout was
raised and no cold-start special case was added on a guess.


---

## SET 55 — SHIPPED in 1.7.31 (landing epigraph position + wording)

Epigraph moved between `.finalbloc` and `<footer>`; wording `Beware of he who…` (SMAC). No tier1 rebuild.

---

## SET 56a — SHIPPED in 1.7.32 (live truth + stale-running sweep)

`tier1:live` from offscreen only; `sweepStaleRunning` on SW boot + `tier1:runs`; App liveId chip/binding; `tests/set56-live-truth.test.ts`. No tier1 rebuild.

## SET 56b/c — SHIPPED in 1.7.33 (run surface contract + reporting conformance)

1.4-era layout invariants on `tier1:*` channels; tones/substrate/honesty/evidence/stale-banner/vault label rewired; install-lifetime only in Admin; `tests/set56-surface.test.ts` + restored SET 50 App-wiring. No tier1 rebuild.

---

## SET 56d — SHIPPED in 1.7.34 (pipeline stage stepper + stage-breaks + phase tags)

`stagePipeline.ts` + Pipeline log stepper/breaks/visible `[phase]` tags; `tests/set56d-stage-pipeline.test.ts`. No tier1 rebuild.

---

## SET 57 — SHIPPED in 1.7.35 (executed by Claude at Andy's explicit instruction)

57a counters (runner.ts, event-site increments, conformance-tested) · 57b/c RunReport.tsx scorecard + six groups under the Pipeline log · 57d mirror in statsAggregate GRID_SPEC + SW JS aggregator (`stats:local`) · 57e export passthrough (scores injected only when counters present) · 57f liveness integration test (jsdom mounts the REAL App against a scripted extension seam, asserts mid-run render). Bonus fixes forced by 57f: the duplicate `Escalation` declaration is resolved (`IrEscalation`) — web typecheck now at **0 errors** (the 11 "known pre-existing" are gone) and App.tsx is finally importable by tests; `t1.cost.comparative` render crash guarded (an undefined comparative blanked the whole page); offscreen usage meter gains `minIn/minOut`; vitest include widened to `.tsx`.

### Original enumerated instructions (retained for audit)

(release 1.7.35)

**Context, not instruction:** Andy's specified report has never shipped. This set creates it. It requires new tier1 counters → tier1 bundle rebuild IS required.

### 57a — instrumentation counters (`src/tier1`)

Add to the run metrics object (exact names): `featuresTotal`, `featuresFrozenWithoutRedraw`, `irRedraws`, `nodesTotal`, `nodesBuiltCleanFirstPass`, `boundedLoopNodes`, `boundedLoopIterations`, `candidatesDrawn`, `candidatesAccepted`, `errorsTotal`, `errorsParse`, `errorsConformance`, `errorsExecution`, `errorsCrash`, `deferredTotal`, `deferredRelitigated`, `deferredResolvedAfterRelitigation`, `deferredPermanent`, `nodesHealed`, `fixesUnsuccessful`, `fixesUnparseable`. Rules: (1) each counter increments at the same code site that emits its corresponding pipeline event — healed↔`healed`, unsuccessful↔`rejected-draw` in repair, unparseable↔parse-rejection kinds, deferrals↔`deferral`/`excluded` kinds, candidates↔draw/accept events, loop entry/iteration↔rha episode events — never derived by re-parsing log text; (2) emitted in the run record so UI and exported logs read identical numbers; (3) mock-e2e conformance test asserts each counter equals the count of its paired events in the same run.

### 57b — `web/src/RunReport.tsx` (new file, pure props in)

**Scorecard strip (first, largest):** three tiles with deterministic ratios — `IR CONTRACT = featuresFrozenWithoutRedraw / featuresTotal`; `BUILD = nodesBuiltCleanFirstPass / nodesTotal`; `RHA = nodesHealed / boundedLoopNodes` (0/0 renders `—`, dim, never NaN). Percentage + band tone from exported `SCORE_BANDS = { ok: 0.85, warn: 0.6, friction: 0.35 }` (≥ok green, ≥warn yellow, ≥friction friction, else red).

**Six category groups below, exact order and rows:**
1. `SUBSTRATE` — attempts, fixes, refusals, no-handler candidates, hit-rate % (fixes ÷ attempts).
2. `BOUNDED LOOPS` — nodes entered, iterations, candidates drawn, candidates accepted as fills, acceptance % (accepted ÷ drawn), avg iterations/node.
3. `TOKENS` — sent and received: avg / min / max per call + run totals + est. USD.
4. `ERRORS` — total, then parse / conformance / execution / crash rows.
5. `DEFERRED` — total, relitigated, resolved after relitigation, permanent.
6. `RHA EFFICACY` — healed, unsuccessful, unparseable, heal % .

### 57c — placement

Run view, directly beneath the Pipeline log module, as collapsible `rha.collapse.runReport` (default open). This-run scope only; the `lifetime` source-string ban (56c T3) extends to `RunReport.tsx`.

### 57d — Admin mirror

AdminView gains the same six-category taxonomy + scorecard aggregated avg/min/max/lifetime across runs. Identical row labels — one vocabulary everywhere.

### 57e — export

All 57a counters and the three ratios serialize under `metrics` in the run-log export (`exportRunLog.ts` passes them through — verify presence in the `.json` with a test, no format fork).

### 57f — reporting liveness integration test (closes the "green tests, blank app" seam)

New `tests/set57-liveness.test.tsx` using jsdom + @testing-library/react (add as devDependencies if absent): mount `App` with the extension messaging seam mocked (stub the module-level send function — export it from App.tsx or a new `web/src/extBridge.ts` if needed to make it injectable). Script: `tier1:live` → `{ liveId: 'X', running: true }`; `tier1:status` → `{ id: 'X', running: true, events: [encode, build, build] fixtures }`; runs list containing 'X'. Assert MID-RUN (running still true): 3 log rows rendered with tone classes, stepper shows exactly one `stage-active`, RunReport renders with numbers from fixture metrics. Then script completion and assert terminal state. This test failing = release blocked, regardless of all other suites.

### Gates + release

`npx tsc -p tsconfig.web.json` = 11 baseline errors; `npx vitest run tests` all green incl. 57a conformance + 57f; `npm run build:web` ends `check-web-boot OK`; tier1 bundle rebuild (`npx vite build --config vite.tier1.config.ts`) REQUIRED before build:web; bump 1.7.35; duties A/A2/A3/B; no deploy while `tier1:live.running` is true.

---

## THE 4-LAYER PROGRAM (Andy-approved doctrine, 2026-07-27) — 60/63 SHIPPED in 1.7.40; 61/62 SHIPPED in 1.7.41 (member-conformance shims + ESM-interop `(0 , fn)` classifier; flywheel mines surgery/reset diffs — fixture-proven: 3 reset heals → handler-candidate support=3). REMAINING: SETs 64/65 below.

**Binding doctrine additions:** (1) four repair layers — L1 mechanical substrate → L2 schema-guided interpretation → L3 free permutation → L4 entropy reset; every finding enters at L1, categorization floor 95% (currently ~100% via error catalog); (2) supercache (per-run) + metasupercache (cross-run) inform LLM draws AND deterministic rules AND grammar gates; (3) rejects are fuel: salvage (shipped), corpse detection, flywheel; (4) tests get a full synthesis grammar; implementations get a locked shell with free bodies; (5) IR never feeds from generation output (no supergit→IR — provenance).

### SET 61 — instance-method conformance handlers (L1 coverage)
`src/tier1/substrate.ts` + generated oracle: deterministic handlers for the `is-not-a-function|*` family — classify the API-shape mismatch (class-method vs object-literal vs factory vs default-export) from the frozen suite's call shape + the impl's export AST, and mechanically restructure the impl shell to match. Fixture/mutation-verified per the gen-oracle pattern before freeze. Acceptance: the 7/27 Wikipedia-clone run replayed under mock fixtures resolves ≥3 of its 15 `is-not-a-function` consults at L1 (0 tok).

### SET 62 — promotion flywheel from surgeries and resets
Runner: capture before/after diffs of every IR-case surgery win and entropy-reset win (normalized, per-file); distill the DELTA into edit shapes feeding `handlerCandidate` promotion (support ≥3 unchanged). Unseals the loop where whole-file heals produce no minable shape. Acceptance: a run healing via reset emits ≥1 handler-candidate line naming the classKey.

### SET 64 — SHIPPED in 1.7.47 · SET 65 + 69 — SHIPPED in 1.7.48 · SET 68 — SHIPPED in 1.7.49

### SET 85 — SHIPPED in 1.7.61 — THE OBSERVATORY (Andy: "the passive watcher agent should be append only … each of those should be a decoupled LLM poll … at the AST level; in the supercache, this data should be explicit artifactual")
Four corrections to the SET 75/76 watcher, which was none of these things:
**1. APPEND-ONLY.** It held ONE latest reading and overwrote it — every synthesis destroyed what the last one learned. `ObservationLedger` accumulates: identity is (kind, subject, normalized claim); a repeat sighting increments `timesSeen`, extends `lastSeenAt`, and MERGES evidence. Nothing is ever removed or replaced. Prior-epoch corpora seed the ledger, so counts continue across runs rather than restarting.
**2. DECOUPLED POLLS.** One prompt asked for idioms + tropes + schemes + a summary in a single breath. Now four polls — `Identify any IDIOMS` / `DEPENDENCIES` / `TROPES` / `SCHEMES` — each its own LLM call, own system prompt, own narrow schema (`items: [{subject, claim}]`), each a fresh decoupled instance. Tests assert no poll's question leaks into another's prompt.
**3. AST-LEVEL.** `astFacts()` extracts declarations (kind/symbol/params/exported), imports, and cross-file CALL EDGES (a symbol declared in one file and invoked from another) from the artifact. Every poll receives the same deterministic AST page and must cite a subject from it; evidence is attached from the declaration table, not from the model's prose.
**4. ARTIFACTUAL IN THE SUPERCACHE.** Observations are records — kind, subject, claim, evidence[], firstSeenAt, lastSeenAt, timesSeen — persisted SW-side under `metaObservations` (append-only merge, 400 cap) and loaded back at run start. The page ranks by corroboration so the 420c cap truncates one-off guesses, never established patterns.
**Containment unchanged:** no engine → the accumulated page still renders; a throwing engine retires after two failures; the observatory is never a validator.

### SET 84 — SHIPPED in 1.7.60 — a stage that never ran must say so (Andy: "not much substrate activity this run" — attempts 0, fixes 0, refusals 0, no-handler 0, hit-rate —)
All four substrate counters at exactly 0 is NOT a weak substrate: it is a repair ladder that never executed (verified: all four `substrateAttempts++` sites intact, `tests/tier1-substrate.test.ts` 16/16 green). The ladder is skipped when no feature froze a contract, when every frozen feature failed to build, or when features parked on unattributed crashes — and the report could not distinguish any of that from "the substrate tried and missed". `—` reads like missing data when it means "this never happened".
**The funnel line** on `done`: `funnel: 10 feature(s) planned → 3 froze a contract → 3 built all owned files → 0 entered the repair ladder → 0 converged.` When the ladder never ran it appends WHY (no contract froze / N parked before repair / build-failed).
**The report** names it too: the SUBSTRATE group gains a `status` row when attempts are 0 — `never ran — no node entered the repair ladder` — and hit-rate reads `n/a — no attempts` instead of a bare em dash.

### SET 82 — SHIPPED in 1.7.59 — small-model context budget (Andy: "some of these numbers are far too large to be useful in a 2b model")
The observed buffer was `union:1211, spec:879, contract:540, substrate:91, corpus:34, siblings:14` — the ADVISORY page was the largest thing in it, larger than the spec, on a run where the watcher engine was dead so it carried a bare digest of 19 nodes. Two fixes: (a) **hard ceilings** — union 340c, meta 420c (were 1400/1800), with both pages re-ordered to emit most-actionable-first (oscillation, still-failing, the one-line reading, dead SHAPES) so the cap truncates the least useful tail rather than the headline; (b) **CONTRACT FIRST** in `STAGE_PAGE_POLICY` — union/meta now pack LAST at every stage. A small model reads what it is given first; priority inversion was starving the contract.

### SET 83 — SHIPPED in 1.7.59 — the watcher engine was never callable (Andy: "reading engine FAILED?")
**Root cause:** requests go out with `json_schema … strict: true`, under which EVERY declared property must appear in `required`. The three schemas added this session (`WATCH_SCHEMA`, `META_SCHEMA`, `JUDGE_SCHEMA`) declared optional properties and listed only one or two as required, so the server rejected the schema, every reading threw, and after two throws each agent silently retired its engine. That is why `watcherReadings: 0` across 97 syntheses. All three now list every property (empty arrays/strings are valid values; optionality lives in the value, not in omission). **Disposition honesty:** a retired engine no longer reports "reading engine failed" on every subsequent synthesis — it reports `reading engine retired after repeated failures — digest only (not re-attempted)`, distinct from `reading engine threw` and from `no reading engine on this client`.
**Also landed:** `deterministicAnchorBody()` in planSuite (derives a callable-anchor assertion from a stub — function, class-constructor, and const forms), unit-tested. **Deliberately NOT wired as an auto-fill:** both an unconditional prefill and a zero-contract rescue were built and REVERTED — they replaced real anchor draws with a weak `not.toThrow()` shape and defeated deferral doctrine (a feature with no fillable cases must defer honestly, not freeze a contract that asserts nothing). The helper stays available for a future, better-targeted use.

### SET 80 — SHIPPED in 1.7.58 — NODE-COMPLIANT REPAIR (Andy: "do we ever take the node in question, supply the substrata … as well as the node, with explicit instructions to repair the node compliant to the schema?")
**The answer was NO, and the code said so out loud.** Both repair sites passed `ownedFiles: []`, and `prompts.ts` carried the comment *"Deliberately NO current ownedFiles / broken impl — fresh from frozen contract."* The model had NEVER seen the node it was repairing — only a ~300-char 64c exhibit EXCERPT framed as "evidence, not a base to patch". Every repair was a blind whole-file regeneration, which is why entropy resets carried so much of the healing and why `assertion|toEqual` sat at 57 sightings UNCOVERED.
**The rung (`src/tier1/nodeRepair.ts`):** on the FIRST L2 loop, assemble all four at once — the **SCHEMA** (derived, never drawn: stub signatures = shape contract, frozen cases = behavior contract, deps listed), the **NODE** in full (excerpted to the declarations under repair only when >3000 chars, never blind-truncated), and all three **SUBSTRATA** (deterministic substrate + oracle hint + idioms, supergit causal lineage, supercache run-buffer + cross-run meta page) — under an explicit repair-in-place instruction: *return the COMPLETE node, keep every exported signature, change only what the findings require, do not restate the tests.*
**`prompts.ts` now honors `ownedFiles`:** supplied → the node is shown and the closing instruction becomes "Return the COMPLETE repaired file(s)"; absent → the old rewrite-from-contract behavior is byte-identical. Anti-anchoring is preserved where it belongs (entropy reset, and L2 loops 2–3), and abandoned where it was costing us (targeted repair). `tests/prompt-purity.test.ts` now pins BOTH directions.
**Metered:** `nodeRepairDraws` / `nodeRepairWins` in the report.

### SET 81 — SHIPPED in 1.7.58 — readable failures + shipment notation (Andy)
**a. Every failure reads dark orange.** `N/M passing` was already green/friction, but a substrate MISS read as neutral bookkeeping: `substrate: X → cataloged | no-handler | catalog-drift` and `catalog-drift: …` are failures to repair and now tone friction.
**b. Feature epochs close explicitly:** `epoch closed: 3/5 cases passing — 2 still failing after 4 passes` (green when all pass, friction when partial, red at zero, and it names a park). The log now has a readable boundary between nodes.
**c. `built 1/1 owned files` was useless notation.** Build lines now state the node's place in the shipment: `node 3 of 10 built — Article Manager (src/article_manager.ts)`, or `node 3 of 10 INCOMPLETE — 0 of 1 files accepted`.

### SET 79 — SHIPPED in 1.7.57 — BUILD metric corrected + artifact download + cancel reachability
**a. The BUILD score was measuring the wrong thing.** It was `nodesBuiltCleanFirstPass / nodesTotal` — features GREEN on their very first measure. But the IR contract is BORN RED by design, so first-measure-green is the rare exception; the score reads ~0% on runs where every file built fine (Andy's 20-run: 10/10 nodes built, BUILD showed 0%). Now `nodesBuilt / nodesBuildable` — did the build stage produce accepted, vetted files for each node. "Green on first measure" survives as its own row under RHA EFFICACY, where it belongs. Old records fall back to the previous formula.
**b. Download artifact.** The path existed end-to-end (`tier1:export` → `buildRepoZip` → `chrome.downloads`) and the new shell never surfaced it — the artifact (source + frozen suites + `.supergit/ledger.csv`) was unreachable from the run view. Primary button on the run header.
**c. Cancel reachability.** The cancel button rendered only when viewing the live run; a runaway job was unstoppable from any other view. Now offered whenever ANY job is live, labeled "Cancel live job" when you are looking elsewhere.

### SET 78 — SHIPPED in 1.7.56 — no invented thresholds, no naked draws (Andy live-run findings)
**a. The staleness threshold is GONE** ("Did you make an arbitrary threshold as to how much was substantial enough to matter? It needs to be every single time"). SET 76 gated the model reading behind an invented `WATCH_REFRESH_NODES = 8`; both agents now synthesize IN FULL on every trigger — digest AND reading. `readingIsStale`/`watchSignature`/`WATCH_REFRESH_NODES` deleted (SET 77 rule: dead code comes out). Engine failure handling is retained but is failure handling, not a threshold: two consecutive throws retire the engine.
**b. Watcher dispositions are honest.** `deterministic digest only (no model reading)` was emitted whether the client had no engine, the engine threw, OR a reading was taken and came back unparseable. Now: `no reading engine on this client` / `reading engine failed` / `reading taken but unparseable — digest stands` / `fresh reading — N idioms…`.
**c. NAKED FILL DRAWS** ("no supercache?"): the fill superbuffer was assembled only for `!h.signature || h.kind === 'edge'`, a SET 17-era gate. Every signature-bearing export anchor — `authenticateUser` in Andy's F2 — drew with no spec page, no substrate page, no union page. Every fill draw is now buffered.
**d. Planner-bypass closure was gated on `fill.templated === 0`**, so a feature that templated ≥1 case AND still had an unconsumed match reported the tripwire without ever attempting instantiation. The closure now runs whenever any match is unconsumed, and scans `fill.holes` rather than the whole plan.
**e. Grammar fallback is not a defect, and now says why** ("is this bad" — no: the case was validated and accepted). The line names the invariant tried and the specific miss (`shape-mismatch` / `non-literal-fill` / `inconsistent-fill`) and states "not a defect". **Multi-statement conformance** added: a rich body (`const doc = …; expect(…)`) now conforms by reassembling the ASSERTION and preserving setup verbatim, instead of falling back wholesale. Conformance matching is ANCHORED — the unanchored version matched an assertion inside a larger body and silently discarded the surrounding setup, deleting real test code.

### SET 77 — SHIPPED in 1.7.55 — DATA INVENTORY (Andy: "take inventory as to whether we are using the data. and whether that data is decipherable … {trope: 50} is useless")
**The audit found three WRITE-ONLY fields — captured every run, read by nothing:**
1. **The draw BODIES.** metaSb recorded scores and hashes but the shape itself was stored at one site and never read. A cache that remembers a draw failed but not what it looked like cannot tell the next draw what to stop reproducing. Bodies are now captured at EVERY layer (fill/L2/L3/L4, 400c) and rendered VERBATIM in the meta page under `Shapes that WORKED` / `Shapes already tried and DEAD (do not reproduce these)`, each dead one labeled with why it died ("did not parse", "did not run", "left 2 findings unfixed").
2. **`exhibitHash`.** Written on repair records, read nowhere. Now the clustering key: ≥3 failed draws sharing one exhibit emits "N different draws against the SAME defective node all failed — the node itself is the obstacle, not the draws."
3. **handler-candidate `fixtures`.** before→after pairs assembled and consumed by nothing. Now emitted as evidence with one verbatim worked example, so a promotion candidate is judgeable.

**Decipherability pass (rule: every number states its denominator and its meaning).** `L1=3 L2=1 L3=0 L4=2` → "Repairs accepted so far: 3 of 6 were deterministic substrate fixes costing no tokens, … 2 of 6 required discarding the implementation entirely" (+ a named warning when L4 outnumbers L1). `assertion|toBe×2` → "seen 2 times". Oscillation → "repaired 1 time but has been seen 3 times — the fix is not holding". `F1→2` → "changing F1 affects 2 dependent nodes". `src/x.ts(120b: add)` → "src/x.ts is 120 bytes and exports add" (or "exports NOTHING"). `meanScore 0` → "this temperature has never produced a passing draw". Class tallies → "NEVER solved in 3 draws across all runs".
**Standing enforcement:** `tests/set77-data-inventory.test.ts` fails the build if bare `L1=3` / `x→2` / `(120b:` / `×3` fragments reappear in model-facing text, and asserts each formerly-dead field is read.

### SET 76 — SHIPPED in 1.7.54 — RESIDENT AGENTS (Andy: "every stage monitors for a trigger, which is the need to submit context to LLM")
**a. The watcher is no longer scheduled.** SET 75 fired it at 3 epoch boundaries; that was wrong — the agent is continuously operant. The TRIGGER is a draw about to be submitted, and synthesis runs at that moment, SCOPED to the node in question (the digest leads with that feature's own ledger trail). Fires at all four context-submission sites: per-node fill, fallback fill, build, repair. Every trigger recomputes the deterministic digest fresh (0 tok); the model READING refreshes only on material ledger movement (new failure class, changed repair record, or ≥8 new nodes) — that is the "applicable epoch-cycle", so a resident agent does not multiply model calls. Reuse is stated in the log; a dead engine is marked dead once, never retried per-draw.
**b. Fill ladder authenticates and STOPS** ("we don't need to accumulate the bodies"): each draw is validated (syntax heal → conformance → accept) and the ladder breaks the moment every hole in the batch is filled. A clean first try now costs 1 draw, not 3.
**c. Fill bodies inform the metasupercache** ("they aren't informing the metasupercache"): every fill draw — winner AND loser — is recorded with its hash, layer `fill`, temp, score, parse status, and a bounded body excerpt. Losers matter most: they are the corpses the NEXT run rejects without execution.
**d. THE META-WATCHER** ("another passive agent to summarize the metasupercache"): second resident agent, same dumb-agent contract, pointed at cross-run candidate memory instead of the ledger. Digests draws by class (never-won = intractable), by layer (parse yield), by temperature (mean score), and counts distinct shapes. Its own engine (`summarizeMeta`, META_WATCH_SYSTEM, own schema) — NOT the watcher's, because it answers a different question. Emits a `meta` page on repair assemblies: EXHAUSTED / PRODUCTIVE / SUMMARY, headed `do not repeat exhausted shapes`. Throwing/absent engine → digest-only, run unaffected.

### SET 75 — SHIPPED in 1.7.53 — THE WATCHER + supergit read-back (Andy: "a dumb agent in the agentic sense and an LLM just powers it every applicable epoch-cycle")
**Gap it closes:** supergit was WRITE-ONLY — the run recorded its causal history and no draw could see it. The supercache (superbuffer/metasupercache) interpolated; the ledger did not.
**a. Supergit read-back:** repair assemblies now carry a `ledger` page — this node's causal trail out of the ledger (`repair → error → result → code → test → spec`) plus its blast-radius count. 0 tok.
**b. The watcher — a DUMB agent:** everything about it is deterministic — trigger (epoch boundaries: ir / build / rha), evidence (ledger + whole artifact), digest (recurring classes, oscillation = healed-then-returned, layer mix L1–L4, widest blast radius, still-open findings, artifact shape), output shape (IDIOM/TROPE/SCHEME/UNION), and disposition (advisory `union` page on every stage + an `info` lesson node in the ledger). The LLM supplies ONE reading per epoch over a digest it did not choose (`watchEpoch`, t=0.2, structured, deterministically parsed).
**c. Containment:** no engine → digest-only; unparseable reading → digest-only; throwing engine → caught, digest-only. The watcher is never a validator — nothing is rejected because of it — and its page is framed `OBSERVATION ONLY … not instruction` so it cannot become an LLM judge by the back door.
**Report:** `watcher (epochs / observations)` row; both counters in the grid + SW mirror. Tones: ledger/watcher lines are dim evidence, never health claims.

### SET 74 — SHIPPED in 1.7.52 — SUPERGIT IN TIER 1 (Andy: "that was literally the OG spec")
**The gap:** L3 of the README's own architecture has run in Tier 2 ONLY since Tier 1 was born at cb3af33 — zero commits ever touched supergit/LedgerRow under src/tier1/. `src/core/substrate.ts` is fs+git bound (fs.writeFileSync, execFileSync git, commit trailers), so the browser port dropped the layer and nobody flagged it. Tier 1 ran with an event log and provenance prose instead of a queryable ledger.
**The fix — `src/tier1/supergit.ts`:** browser-native `Tier1Supergit`, byte-identical ledger schema (same `LEDGER_HEADER`, `N####` ids, monotone steps, tree parents, backward `causeId`, forward `edge` rows, CSV escaping). Append-only in memory; never mutates a written row.
**Decision calculus is per UNIT OF WORK, all three of Andy's axes:** per test constructed (one `decision` node per frozen case, under a `test` node per contract), per node generated (`code` node caused by the contract it was drawn against), per repair executed (`repair` node at EVERY layer — L1 substrate 0-tok heals included — caused by the `error` node carrying its bugClass), plus `spec` nodes per feature, `result` nodes per measurement with honest pass/fail, and forward `edge` rows for declared dependencies (blast radius).
**Materialization:** the run record carries `supergit.{rows,csv,graph}`; the exported repo gets `.supergit/ledger.csv`, `.supergit/graph.json`, `.supergit/spec.json`, and every commit carries `Supergit-Node / Supergit-Step / Supergit-Type` trailers — code position ↔ ledger position in both directions, as the OG spec specifies.

### SET 73 — SHIPPED in 1.7.51 (Andy live-run findings, 2026-07-27)
**a. The grammar was never in the prompt.** SET 65 shipped behind `grammarEnabled`/`shellLock`, default OFF, and no host enabled them — every deployed run paid the full preparse rejection tax with zero constraint benefit. Both now default TRUE in `TIER1_DEFAULT_GATES`.
**b. Stub extraction was starving the contract page** ("there should be more stubs"): the function regex demanded a return-type annotation and classes yielded one bare `class X` line. Now covers untyped functions, arrow-const exports, and every class METHOD (`Store.get(key: string): string | null`), deduped by symbol.
**c. Shell-lock defects found by turning it on** — two would have destroyed real code: (1) the frame header came from the STUB, so the draw's dependency `import` lines were discarded → imports are hoisted verbatim (closed grammar, safe); (2) non-slot top-level declarations (helpers, classes, consts) were deleted → carried over, deduped by symbol (which is what kills duplicate-decl without deleting legitimate code). Unbalanced declarations are excluded by a CSP-safe structural gate and reported as `parse poison PREVENTED at the frame` + counted as substrate wins.
**d. Throw-camouflage** ("the substrate templated never executes"): `expect(() => f()).toThrow()` PASSES against throwing stubs, so green-on-stubs is undecidable for rejection-clade cases — born-red was destroying verified templates as "vacuous" every run. They are now retained, never surged, never re-enqueued, and ride along with the anchor.
**e. The fallback fill was the last naked draw**: no superbuffer at all and only `gates.irAttempts`=2 tries. Now carries the salient node (encoded spec, stub contract, substrate handoff, concept, sibling cases filled this cycle) and walks the full 3-rung ladder.
**f. Tone**: a permutation pick landing `(0/N passing)` is friction (dark orange), not neutral yellow.

### SET 72 — SHIPPED in 1.7.50 (Andy, 2026-07-27: "each LLM call should get 3 tries")
Every previously one-shot call site walks the 3-rung decorrelation ladder (temps 0.3/0.7/1.0; reset 1.0/1.1/1.2; build cold/0.75/1.0): born-red surgery (re-targets still-vacuous cases per try, stops at first red — anchor fallback only after 3 misses), freeze-gate test-rha redraw (per-case ladder, stops when un-sick), build draw (0-accepted-files redraws hotter before build-failed), entropy reset (corpse/vet-rejected resets retry hotter), judgment-amend (3 draws before drop fallback). Already-laddered sites unchanged: fill K=3, L2 3 loops, L3 K-batch, case-relay K=3, IR surgeries K=3. Concept stays ONE bounded draw (context-only, by doctrine).
64: L2 rung (K=1, ≤3 loops/class, temps .3/.7/1.0, bullseye + strict-improvement re-enters L1, crash-focus gets the buffer), 64c flawed-exhibit page, 64d runtime-trace replay (probe-channel piggyback, 0 protocol changes), metasupercache (metaSb:* ring in SW, caps + boot audit + footprint), corpse detection (per-UNIT path+content hash — cross-feature reuse is the flywheel, not a corpse), layer attribution L1–L4 + classified% (95% floor). 65: synthesis grammar (typed-hole skeletons, deterministic reassembly) + shell-lock (frame from stubs, body-only draws) behind gates.grammarEnabled/shellLock. 69: behavior/binding split (canonical case: "Redis sliding window rate limit" → sim-clock kept line) + rate-window invariant w/ injected clock. 68: judgment gates — deterministic trigger (semantic survives L1+L2 AND no license chain), supervised card (pausedGate 'judgment', resume carries decision), unsupervised ONE-call provisional LLM ruling (unparseable→drop), amend faces born-red containment, direct re-arms L2 with the licensed addendum, provenance stamps on the case license, report row operator/llm/dropped. Original enumerations below retained for audit.

### SET 64 — L2 schema-guided interpretation + metasupercache
**L2:** on L1 no-handler with successful classification, invoke the LLM as procedure executor: bug schema (classKey/stratum/finding evidence/assertion diffs) + full applicable strata clade (playbook steps, strategy order, checked idioms, handler-candidate sketches) + superbuffer. K=1 per loop, ≤3 loops, temp 0.3→0.7→1.0, bullseye exit on focused-finding resolution; partial improvement keeps the gain (strict-improvement) and re-enters at L1. L3 only after L2 exhausts. Crash-focus draws get the buffer too (close the bare-draw gap).
**Metasupercache:** storage keys `metaSb:<runId>` (NEVER inside run records). Candidate record: `{classKey, layer, temp, score, findings, parseStatus, diffVsIncumbent≤1KB}`. Caps: 256KB/epoch, 4-epoch ring, 1MB total — enforced at write (trim diffs → evict oldest), >2× epoch-cap single write REFUSED (state kept, friction event + notify). Boot audit in SW rides the stale-sweep pass; footprint in stats:local + Diagnostics. Beyond the ring: distill to lessons.
**Corpse detection:** every new draw hashed (normalized) against both caches; identical → rejected WITHOUT execution + forced escalation. Consumers: LLM negative context (supersedes string tabu), salvage, flywheel, grammar-gate/router feedback.
**Report:** layer attribution (fixes by L1/L2/L3/L4), `classified %` with 95% floor, corpse rejections, metaSb bytes.
**64d — runtime trace page (value provenance for semantic findings):** we own the sandbox — during the measure of a failing case, wrap the tested exports with a capture shim recording arguments and return/thrown values at the symbol boundary (bounded depth, serialized, ~400-char cap, deterministic ordering). Inject as a `trace` page in the repair dossier for semantic-procedure classes (assertion|*, throw-mismatch, catalog-drift). This converts semantic repair from "reconstruct intent from spec" into "map these inputs to that output" — a LOCAL transformation, the small-model sweet spot. 0 tok to produce. Evidence: run d9752e89's UNCOVERED cluster is entirely semantic; the LLM already healed semantic findings when conditions were right (F6 pass 1, F8 pass 7) — conditioning, not capability, is the variable.
**64c — flawed-exhibit page (Andy, 2026-07-27):** repair draws currently see NO impl source (`ownedFiles: []`; no impl page in repair superbuffer). Add a deterministic exhibit page to EVERY repair-stage assembly: the enclosing NODE span at the failure position (statement/function; line minimum; ~300-char cap) — position-bearing classes via parser walk-up from coordinates; assertion classes fall back to the span (or provable absence) of the symbol under test from the classKey. Page header framing verbatim: `flawed exhibit — evidence of the defect, not a base to patch; produce the correct node from the contract` (anti-anchoring; rewrite-from-contract doctrine intact). Applies to L2 AND L3 draws; exhibit hash recorded in metasupercache candidate records for tighter diff clustering.

### SET 66 — SHIPPED in 1.7.43 · SET 67 — SHIPPED in 1.7.44

### SET 70 — STAGED in vault-1.7.45 (committed+tagged, deploy held for live job)
Case-relay: fill drops re-enqueued to end-of-run stack (cap 8) → warm-world refill after RHA (vacuous-guard red-vs-stubs + adoption green-vs-artifact); lineage-preserving final drop lines; `re-enqueued (warm retry)` / `recovered at end-of-run` report rows.

### SET 71 — STAGED in vault-1.7.46 (committed+tagged, deploy held for live job; supersedes 1.7.45 deploy)
71a per-NODE K-draws (batch size 1 — 3 nodes × K=3 = 9 calls, no shared-output truncation drops); 71b freeze ledger (`frozen suite … (N planned → M filled, X unfilled→re-enqueued)`); planner-bypass closure (matching consumable templates INSTANTIATED at the 34e tripwire, 0 tok); 64a rescue stage retired (superseded by per-node draws); 71c deterministic anchor — born-red vacuous refusal now freezes `expect(() => fn(args)).not.toThrow()` (red-by-construction vs throwing stubs), re-enqueues the vacuous cases to the relay, and keeps dependents covered (no abandonment fan-out).

66: sick-red partition ladder at the freeze gate (scaffold-wire before born-red; longest-match case attribution; suite-side L1 → one guided redraw → honest gate drop; `irSurgeriesInRha` metered, target 0; 66b: scaffold-wire on EVERY re-freeze — the fix-three-times churn root). 67: relay instances 2–3 proceed on salted fresh entropy with disclosure (hard world-gate retained for the metaloop); notify bursts deduped (3s window). Fixture-proven; 625 tests green. Original enumerations below retained for audit.

### SET 66 — test-RHA at freeze (Andy, 2026-07-27: "tests literally equal code — apply RHA to them, immediately after construction, never after the artifact exists")

Shift IR-case surgery LEFT into the freeze gate; the frozen contract becomes actually frozen.

1. **Pre-freeze ladder:** execute the candidate suite against the STUB artifact (the born-red run, reused); classify every red case via the error catalog.
2. **Partition:** legit-red (assertion fail / stub `not implemented` throw) → freeze-eligible. Sick-red (case-side `is-not-a-function` on contract symbols, unresolved-import, TypeError/crash inside the case body) → test-RHA loop. The stub contract is the deterministic oracle.
3. **Test-RHA loop, bounded ≤2 passes/case, substrate FIRST:** suite-side member-conformance against stub signatures (SET 61 direction-flipped — repair the suite toward the contract), parser-guided heals, clade rebind; THEN single-case guided redraw with oracle hints (move the existing RHA-surgery redraw machinery here). Accept only red-for-the-right-reason.
4. **Freeze semantics:** a suite freezes only when every case parses, conforms, and is legit-red. RHA-phase IR surgery stays as emergency-only (existing caps) for defects undetectable until impls exist; expected usage → ~0.
5. **Metering:** `sickRedCases`, `testRhaSubstrateFixes`, `testRhaDraws`, and `irSurgeries` split at-freeze vs in-RHA; Run report shows suite-side RHA under IR CONTRACT.
6. Acceptance: on the Wikipedia replay fixtures, zero IR-case surgeries fire during the RHA phase; every sick-red is repaired or honestly dropped BEFORE freeze.
7. **66b — surgery must preserve scaffold-wire (defect, run d9752e89):** every post-surgery re-freeze DROPPED the scaffold-wire injections — auth whiplashed 5/6→1/6, then the substrate re-fixed `undefined-symbol|registerUser` et al. REPEATEDLY (same symbol fixed 3× at consecutive measures — the fix wasn't persisting into the re-frozen suite). Re-freeze must re-run suite scaffold-wire (and persist substrate suite-fixes into `frozenByFeature`), with a regression test: surgery re-freeze → zero undefined-symbol events on the next measure.

### SET 67 — small pair: relay world-signature + notify hygiene (run d9752e89)

**67a — relay precondition too coarse:** F4/F7/F10 each finished 5/6 — ONE finding from green — and all three relay retries were skipped `world unchanged: +0 heals, +0 lessons`, because the WorldSignature counts only heals/lessons and ignores the feature's OWN ladder activity (surgeries, resets, artifact writes) since the stall. Include own-suite re-freezes and owned-file writes in the signature so a feature that changed its own world qualifies for its relay instance. Acceptance: a 5/6 feature whose suite was surgically re-frozen re-enters relay instead of skipping.
**67b — notify(sw) log spam:** stage transitions emit up to 6 duplicate `notify(sw): created id=…` lines in a row (409 of this run's 882 events are notify/bookkeeping). Dedupe per stage-change (one notify per transition), and log the notify line at most once per created notification. Pure hygiene; tones/events otherwise unchanged.

### SET 65 — synthesis grammar (IR) + shell-locked codegen (build)
**Grammar:** invariant taxonomy generalizing the 5 clade templates (existence/shape, roundtrip, idempotence, determinism, boundary, rejection, ordering, referential-integrity, error-contract, state-transition, uses-dep). Parseable skeletons with typed holes; deterministic conditional gates (case kind + signatures + spec tokens) choose the invariant set, fixture/mutation-verified and frozen; LLM fills holes only (K=1, per-hole conformance). Born-red vacuousness becomes a hole-level deterministic check. Schemad:freeform becomes IR-side layer attribution.
**Shell-lock:** build emits the impl file frame (imports, export declarations, signatures) deterministically from stubs; model draws bodies only into the frame. Kills duplicate-declaration and parse-poison classes structurally.
Acceptance for both: zero `unparseable at "(empty)"` drops and zero program-parse regressions on the Wikipedia-clone replay fixtures.

### SET 69 — scanner behavior/binding split (Andy, 2026-07-27: "does this really require Redis")

The SET 44 capability scanner over-excludes: an infrastructure noun anywhere in a spec item excludes the WHOLE item, including its pure-logic behavioral core. Run d9752e89 line 50: a sliding-window rate limit (deterministic counter + clock — maximally testable) excluded as "requires redis" while F10 tested the same algorithm from other lines.

1. For infra-noun hits (`redis`, `postgres`/`database`, `network` where the behavior is protocol-independent): excise the BINDING clause (connection, persistence, env), keep the BEHAVIORAL invariant, re-emit as a partial exclusion + a testable line tagged with the simulated-dependency clade (in-memory store, injectable clock). Browser-truth exclusions (computed-style, real network I/O, favicon serving) remain whole-line — those are honest.
2. New clade: `rate-window` / time-boundary invariants with injected-clock scaffolding (pre-grammar version; SET 65's gate taxonomy absorbs it as a skeleton production).
3. Acceptance: Wikipedia replay — line 50 yields a frozen, red-for-the-right-reason sliding-window case with a fake clock; lines 99/125/126 stay excluded.

### SET 68 — judgment gates (Andy, 2026-07-27: "judgment calls can be surfaced to the user; opt-out is the non-default toggle")

Mid-run HIL for spec-under-determination — the one class no model can solve because it's a decision, not a bug.

1. **Trigger (deterministic):** a semantic finding survives L1 and L2, AND the intent-oracle license lookup for the disputed expectation finds no licensing chain (case → edge/export → description → prompt span). Never triggered by mere difficulty — only by absent license.
2. **Gate:** supervised runs pause (existing `tier1.paused` + `pausedGate: 'judgment'`); the shell renders a judgment card: the case name, assertion, expected vs received, the 64c exhibit + 64d trace, and the failed license search — then three resolutions: **(a) amend the case** to accept implementation behavior (directed surgery), **(b) direct the repair** — user supplies one intent line that becomes a licensed spec addendum driving the next repair draws, **(c) drop the case** (coverage reduced, honest). `tier1:resume` extended to carry `{ decision: { caseName, action, intentText? } }`.
3. **Provenance:** the decision is recorded in the run record and TRACEABILITY as an operator ruling (`operator decision @ <ts>` cited as the license). The prompt-is-the-oracle chain stays unbroken — the operator IS the oracle.
4. **Unsupervised (⚡ opt-out) — LLM as judge (Andy, 2026-07-27):** in autonomous E2E runs the LLM rules instead of skipping. ONE fresh decoupled call, given the identical evidence card (assertion, expected/received, exhibit, trace, failed license search) and the same three actions; structured output, deterministically parsed; unparseable ruling → drop (fail-safe = old behavior); ≤1 ruling per case, recorded, never re-asked. **Provenance distinction is mandatory:** operator ruling = license; LLM ruling = `llm ruling @ <ts> (provisional)` — marked in TRACEABILITY, counted in the Run report (`judgment gates: N — operator/llm/dropped`), surfaced in the escalation report, and flagged as a relitigation candidate for continuation epochs. Containment: rulings never bypass mechanical validators — amended cases still face born-red + execution; directed repairs still face strict-improvement. The layered healing bounds a wrong ruling; the provenance stamp guarantees we can find and reverse it.
5. Depends on 64 (exhibit + trace power the evidence card). Evidence: F4 burned 6+ passes on `assertion|toHaveProperty` this run — under a judgment gate that's one question and zero wasted draws.

Standing duties A/A2/A3/B apply per release; tier1 rebuild required for all four sets; never deploy/swap while a job is live.

---

## SET 42a — OPEN: Bot Fight Mode still injecting challenge bootstrap (Andy-only)

**Status.** Code for 42b/d/e/f/g/h + 43–45 + 47–53 is in 1.7.30. Live `/app` HTML may still contain `window.__CF$cv$params` + `/cdn-cgi/challenge-platform` — Edge remains broken until the zone challenge is off. Wrangler OAuth / CF MCP tokens lack `Zone Bot Management` / zone settings write (API 10000).

**Andy (dashboard toggles — BFM alone was NOT sufficient; verified still injecting after BFM=Off, 2026-07-26 ~18:10):** the `__CF$cv$params` injector is the challenge platform, summoned by any of THREE settings. Flip all that are on: (1) Security → Bots → **JavaScript Detections = Off** (separate toggle from Bot Fight Mode, often under "Configure" — TOP SUSPECT); (2) Security → Settings → Security Level ≠ "I'm Under Attack"; (3) Security → WAF → Custom rules: no zone-wide Managed/JS Challenge actions. Allow ~60s propagation per flip. Verify: `curl -s https://rha.software/app | grep -E '__CF\$cv\$params|challenge-platform'` → empty. Then `npx tsx scripts/edge-wait.ts 1.7.33` → vault swap → ⟳. FINAL Edge step regardless: one "Empty cache and hard refresh" in Edge to evict the poisoned text/html cached under the .js URL during the broken period.

**42c** stays conditional (only if Edge still fails after challenge is gone with a JS exception in `index-*.js`).

---

## SET 50 / 51 — SHIPPED in 1.7.28 (persist/modules intact under 1.7.33)

**50** — `build:web` emits `web/public/version.json` + baked `WEB_BUILD_ID`; `staleBundle.ts` + emit/_headers remain. App.tsx poll wiring restored in 1.7.33 (SET 56c R12).

**51** — offscreen register-at-start + checkpoint every 25 events / 30s with SW ACK/read-back; finalize at end; header-only fallback merges without wiping prior events. SW-side chain remains live; App.tsx checkpoint-header display restored in 1.7.33 (SET 56c R8).
