# 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.
