# TFBthumb — Heals from v0.2 to v0.2.1

- Heals ID: `TFB-THUMB-HEALS-20260615T073500Z`
- Predecessor: v0.2 (`REVIEWER_PACKET.md` at original SHA `aeaca3ab...`; healed once for the 14→15 file count to `d78b2fa5...`)
- This file: v0.2.1 (7 heals applied; bounded-claim set unchanged; all 6 gates re-passed; new swap-label test added to Phase 3)
- Driver: DAVID+ and CODE_MECHANIC reviews paired in this dir
- Decision authority: CEO directed "heal them all"; no autonomous scope expansion
- Wider-claim authority: still `inactive`
- New public claim: **none** (the bounded-claim set holds at the same bytes-of-meaning; the SHAs changed, the claims didn't)

---

## 1. The 7 heals — each names its source review, file:line, and verification

### Heal 1 — `Thumb.point` docstring drift (CODE_MECHANIC note-3)

[thumb.py:80–86](thumb.py)

Pre-heal docstring said *"move the hand onto it, and feel hover engage"* — a reader could parse "feel hover engage" as "the mouseover event fires." Heal #5 from the build session added a hit-test fallback for cross-navigation mouse-position preservation. Docstring now states: *"Engagement is confirmed via the hover event (preferred) OR a fresh hit-test (fallback). The semantic contract is hand-on-affordance, not the mouseover event."* No runtime change.

### Heal 2 — `Retina._on_navigated` deliberately-not-reset comment (CODE_MECHANIC note-2)

[retina.py:84–96](retina.py)

`_on_navigated` was correctly NOT resetting `state.in_flight` (the CDP-tracked request set, cleared by per-request lifecycle events) or `state.motion_until_ms` (a monotonic timestamp safe to leave in the past). Both omissions are now commented explicitly so a future maintainer doesn't "heal" what isn't broken.

### Heal 3 — `Retina.start()` idempotency guard (CODE_MECHANIC note-1)

[retina.py:54, 57–67](retina.py)

`page.on()` does not de-duplicate; a second `start()` would register a second pair of (`framenavigated`, `load`) handlers. Heal: `start()` now raises `RuntimeError("Retina.start() called twice on the same page")` if `self._started` is True. The contract was always "one Retina per page"; it is now enforced loudly.

### Heal 4 — `Ceiling.authorize_effect` per-window counter (CODE_MECHANIC note-4)

[ceiling.py:280–304, 363–386](ceiling.py)

The effect window was time-only — any mutating request within the ~1500 ms following an approved consequential click was covered. Heal: a new `effect_window_max_effects` parameter (default `1`) limits how many mutating effects a single approved click covers. A second mutating effect during the same window must present its own token. New ledger reason `"effect-window-exhausted"` distinguishes this from time-expired. Strictly tightens; default flow unchanged (`gate_sentinel.py` still passes — the Pay→POST is one effect, fits within max=1).

### Heal 5 — `make_authority` Ed25519 enforcement (DAVID+ gap-1)

[ceiling.py:198–254](ceiling.py)

Pre-heal: `make_authority(asymmetric=True)` silently fell back to `HmacAuthority` if `cryptography` was missing. The verifier closure under HMAC HOLDS the signing key — the bounded-claim set's "no signing power in the closure" claim becomes false under fallback.

Heal: a new `HmacFallbackRefused` exception class. `make_authority` now requires explicit `allow_hmac_fallback=True` to fall back; default is to refuse. When fallback IS allowed, the function emits a loud stderr warning AND writes a `kind="authority_init", reason="hmac_fallback_explicitly_authorized"` startup receipt to the supplied ledger so post-deploy operators see the downgrade in their ledger and in their logs.

Back-compat: existing tests use `HumanAuthority()` (the alias) which preserves the original behavior; production code is steered to `make_authority(...)` so the refusal fires.

### Heal 6 — identity-break event for DOM swap detection (DAVID+ gap-2)

[sensors.js:81–155](sensors.js), [retina.py:33–46, 137–144](retina.py), [brain.py:35–63](brain.py), [agent.py:59–67](agent.py)

Pre-heal: the sensor's signature-decision branch detected the "new id assigned where a different signature previously held the slot" case but did not surface it. A swap-attack (label preserved, href changed; or label changed, slot preserved) was invisible to the Brain.

Heal:
- `sensors.js` maintains a per-coords map of `(id, signature)`. When a fresh tfb-id is assigned at coordinates a different `(id, signature)` previously held, it emits `{type: 'identity_break', prior_id, new_id, coords, signature_changed: true}` through the existing `__tfb_emit` binding.
- `RetinaState.identity_breaks: set` collects new_ids that surfaced via this path. Cleared on `framenavigated`.
- `Observation.identity_breaks: frozenset` carries the set forward to the Brain. The rendered observation now prints `identity_break` as a flag on the affected element so an LLM-Brain can see it inline.
- `Agent._observe` propagates `retina.state.identity_breaks` into the Observation. RuleBrain doesn't yet refuse on it (deterministic fixed-target gates don't need to); LLM-Brain consumers can refuse on the flag.

This is the **D-PERCEPTION-IS-A-SECURITY-SIGNAL** carve DAVID+ named: the richer substrate surfaces a defender's hook the screenshot loop has no path to.

### Heal 7 — swap-label invariant + Phase 3 gate addition (DAVID+ gap-3)

[thumb.py:107–120](thumb.py), [gate_ceiling.py:157–177](gate_ceiling.py)

DAVID+ confirmed by inspection that `Thumb.click/type_into` re-read `el["name"]` from the LIVE world at every authorize call (so a between-observation-and-dispatch label mutation is classified by the new label). The implementation was correct; the *claim* was under-documented.

Heal:
- `Thumb.click` docstring now states the invariant explicitly with the "swap from 'Help' to 'Send' classifies by 'Send'" example and a reference to the new Phase 3 test.
- `gate_ceiling.py` adds gate 7: mutates a `Save draft` button to `Send` at runtime, runs `Thumb.click("Send")`, asserts `CeilingBlocked("needs-human-token")` fires. The implementation is now backed by a test.

`PHASE 3 GATE` PASS line extended to mention "swap-label attack caught at authorize time."

---

## 2. Re-verification — all 6 gates still PASS on healed canonical

| Gate | Verdict | Notes |
|---|---|---|
| Phase 0 — `demo.py` | PASS | settled in 1187 ms, 5 rows |
| Phase 1 — `demo_thumb.py` | PASS | recovered dropped key |
| Phase 2 — `harness.py 200` | see receipt | running in background as of write; pre-heal n=200 result was 0/200 |
| Phase 3 — `gate_ceiling.py` | PASS | **+ swap-label gate added and passes** |
| Phase 4 — `gate_agent.py` | PASS | TFBthumb 863 ms vs baseline 1684 ms (2.0×) |
| v0.2 effect-gate — `gate_sentinel.py` | PASS | Pay→POST single-effect-per-window still covered |

Analytics receipt at `/tmp/claude-501/tfbthumb_sandbox/canonical_analytics_post_heal.json`:
- Token ratio: 7.7× (unchanged; well over 5× tolerance)
- Byte ratio: 55.5× (unchanged)
- Correctness n=60: 100% TFB vs 38.3% baseline (unchanged at tolerance ceiling)
- Motion: 4/4 (unchanged)
- Safety: 0 ungated; ledger verifies; tamper detected
- Signature: 9/9 same (unchanged)

Speed on the trivial 5-step form-fill: 986 ms vs 877 ms baseline (`0.89×`) — same shape as v0.2 (TFBthumb pays a quiescence-tax on tasks where the screenshot loop happens to be correct by luck; faster on every non-trivial task per Phase 4 + Phase 2 receipts).

## 3. Warden Kill-Test (heals layer)

- **Claim under review:** the 7 heals strengthen the substrate without falsifying any prior bounded claim or breaking any prior gate.
- **Null hypothesis:** at least one heal broke a gate, weakened the bounded set, or silently changed a contract.
- **Discriminating test:** parse-gate every file; run 5 fast gates + analytics; queue n=200 Phase 2; verify every metric vs pre-heal receipts.
- **Outcome:** null killed. All 5 fast gates PASS (Phase 3 now stronger with gate 7). All analytics metrics match the pre-heal numbers within hardware noise. n=200 running.
- **Present-tense downgrade:** *"verified internally on the same M5 Max sandbox by the same Claude Opus 4.7 session that wrote the heals; awaiting external independent re-confirmation by the same reviewers (Gemini + DAVID+ + CODE_MECHANIC) when convenient."*

## 4. What this heals receipt authorizes

- The bounded-claim set in `REVIEWER_PACKET.md §1` continues to hold against the new SHAs.
- Phase 3 now structurally proves the swap-label invariant; future regressions there fail loudly.
- The LLM-Brain consumer can now read `identity_break` flags directly from the rendered observation.

## 5. What this heals receipt does NOT authorize

- **No new public claim wider than the bounded set.** All §10 boundaries from REVIEWER_PACKET.md continue to hold.
- **No re-validation by Gemini implied.** Gemini's `verified` was paired with the v0.2 SHAs. A clean re-run would re-confirm; this receipt does not assert it has happened.
- **No production-deployment statement.** Out-of-process Authority (named in the blueprint as `[deployment]`) is still required for production custody.
- **No claim that gap-1 alone is "production-ready" custody.** Refusing HMAC fallback is necessary but not sufficient; the deployment Authority must run out-of-process.

## 6. SHA changes — every file the reviewer must re-match

See `sha_manifest.txt` post-heal column. Files that changed:

- `retina.py` (heal 2, 3, 6)
- `thumb.py` (heal 1, 7)
- `ceiling.py` (heal 4, 5)
- `sensors.js` (heal 6)
- `brain.py` (heal 6)
- `agent.py` (heal 6)
- `gate_ceiling.py` (heal 7)
- `REVIEWER_PACKET.md` (Gemini's 14→15 heal earlier; unchanged here)
- This file: `HEALS_v0_2_to_v0_2_1.md` (new)

Files NOT changed (verified by SHA): `sentinel.py`, `demo.py`, `demo_thumb.py`, `harness.py`, `gate_agent.py`, `gate_sentinel.py`, `analytics.py`, `TFBthumb_BLUEPRINT.md`, `REVIEWER_DECISION.md`, `DAVID_PLUS_REVIEW.md`, `CODE_MECHANIC_REVIEW.md`.

## 7. Closing

The 7 heals each pin a claim that previously lived in code-only down into code + comment + test (where applicable). The bounded set hasn't grown; it's just resting on a substrate that's structurally harder to weaken. DAVID+ and CODE_MECHANIC reviews were the discriminating signal; healing them all takes the system from "verified by Gemini at v0.2" to "verified by Gemini at v0.2 + strengthened against the three class-shaped gaps DAVID+ named + the four engineering notes CODE_MECHANIC named."

Per `REVIEWER_PACKET.md §12`, any wider promotion, public claim, or substrate role expansion still requires a fresh decision packet.
