# CORRECTION NOTICE — ADDENDUM: FAIL-CLOSED **Date:** 2026-07-30 **Adds to:** `CORRECTION-NOTICE-2026-07-30.md` (unchanged, still served at its URL) **New artifact:** `sealed-run-sweep-v2-2026-07-30.json` **Supersedes by addition:** `sealed-run-sweep-2026-07-30.json` (sha256 `a67fde379b10d270edeb6e9c4e7598b0d013ab0c7085083e5630f61475ba58dc`, byte-identical, not edited) --- ## Why an addendum exists The first correction reported two gaps in the measurement layer: 1. **Magnitude suffixes.** `USD 37M` was read as `37`, so a declared floor was never compared arithmetically. 2. **Uncompiled comparators.** `≥ ≤ < >` and prose forms such as *"longer than"* were never compiled, so the line fell through to `human_review`. Two findings in the same layer, both rendering **clean**, is not two bugs. It is one **permissive default**: a line the compiler could not fully parse produced a verdict that a reader interprets as safe. The default is now inverted. ## The rule > **Fail closed.** Any declared boundary that does not compile into a complete predicate > renders **`undetermined`**. Never `clean`. Never a `human_review` row that a reader can > mistake for clean. Mechanically: - `boundary-predicate.ts` returns `compile_status: "uncompiled"`, `render_verdict: "undetermined"`, `fail_closed: true` and a machine-readable `fail_closed_code` (`no_quantity`, `no_comparator`, `contradictory_comparators`, `unrecognized_currency`, `unrecognized_unit`, `empty_threshold`, `empty_prohibition`). There is exactly **one** constructor for that path, so no later edit can invent a fourth way to render clean. - `operator-boundary-detector.ts` honours it as **Class C**: an uncompiled line can only resolve to `undetermined`. Accusatory-only still holds — an existing heuristic accusation is preserved, never cleared. We refuse to let silence read as safety; we do not refuse evidence. - A qualitative red line (no quantity, e.g. *"never concede exclusivity"*) is therefore **not scorable** unless the lock declares explicit forbidden terms (`machine_predicate: {kind: "prohibition", forbiddenTerms: [...]}`). Lock quality is now load-bearing, by design. ## How the rule was tested `fail-closed-garbage.test.ts` feeds the layer deliberate garbage and requires every case to return `undetermined`: unknown slash-units (`0.20 USD/frobnitz`), unknown per-units, foreign currencies (`EUR 37M`, `MXN 700M`), mixed currencies in one line, contradictory symbolic comparators (`≥ ≤`), opposite symbols (`>= 37M and <= 12M`), contradictory prose (*"at least 60 days but no more than 30 days"*), symbol-versus-prose conflict, a currency with no number, a comparator with no quantity, a dangling comparator, nonsense with a stray digit, symbols only, empty string, whitespace only, and a bare scope statement (*"annex scoped to 90 days"* — a scope, not a boundary). **17 garbage cases × 2 layers (compiler and detector) — all `undetermined`.** If one comes back clean, the class is still open. Full suite: **153/153** green in `supabase/functions/_shared`. ## Verdict states are now first-class `retired` is a **verdict state**, not a blank field. `null` renders as empty, and empty reads as "not measured yet" or, worse, as clean. | State | Meaning | |---|---| | `measured` | Measured end to end by the currently live scoring layer. | | `retired` | Previously published, now **withdrawn**. The old number is preserved in `retired_value` for the audit trail and is **never re-issued**. | | `undetermined` | Not resolvable. Fail-closed. Never rendered as clean. | | `unverifiable` | Structurally impossible to verify — permanent, not pending work. | Standing rule: **a score may only be emitted by a pipeline that measured the run end to end with the layer that is live at emission time.** Anything else is `retired` or `unverifiable`. ## Sweep v2 — every sealed run, re-classified | Status | Runs | |---|---| | Verdict affected (a line was silently unscored pre-fix) | **2** — `3050d24d`, `18bcdce0` | | Verdict undetermined (fail-closed: no line compiles) | 3 — `da3734cc`, `72c4c03f`, `c79fc15b` | | **Permanently unverifiable** (pre-dates preimage persistence) | 2 — `b03e7fe9`, `f24c34bb` | The v1 label `not_sweepable_no_preimage` read as pending work. It is not: those two seals carry no recoverable lock text, so they can never be re-measured. The label is now `permanently_unverifiable`. | DQ verdict | Runs | |---|---| | `retired` | 2 — `3050d24d` (was 100), `18bcdce0` (was 82) | | `undetermined` | 3 — no score was ever published, and none can be emitted now | | `unverifiable` | 2 | | `measured` | **0** | ## Re-score of `18bcdce0` — the bundle Craig already holds Stated plainly, before it is asked for: - The line *"Do not accept an annex longer than 120 days"* (`NG3`) was **never evaluated** pre-fix: `longer than` was not in the comparator set. It now compiles to a `max` predicate with threshold `120`. - The remaining four lines do **not** compile (no quantity, or a bare scope) and therefore render `undetermined` under the fail-closed rule — not clean. - **No turn-level transcript is persisted for this run**, so it cannot be re-measured end to end under the corrected layer. Its published `DQ 82/100` is therefore **retired, not recalculated**. A recalculated number would be a second false precision. - **The seal is untouched.** Preimage, ed25519 signature and Bitcoin anchor (blocks 959991/959992) are unchanged. This addendum adds a record; it edits nothing. Forward runs persist per-turn attestations (`turn-verdict-log.ts`), which removes the "no transcript" limitation for everything sealed from here on. ## What has not changed - v1 artifacts are byte-identical at their original URLs. No anchored file was edited. - Every hash, signature and Bitcoin anchor published before this notice still verifies. - Correction by addition, never by mutation.