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

- Heals ID: `TFB-THUMB-HEALS-v0_2_2-20260615T075000Z`
- Predecessor: v0.2.1 (`HEALS_v0_2_to_v0_2_1.md`)
- This file: v0.2.2 (2 brain heals + 1 new gate + 1 sensor refinement; 6/6 fast gates PASS; n=200 queued)
- Driver: DAVID+ v0.2.1 note-A (RuleBrain identity_break refusal) + CODE_MECHANIC v0.2.1 note-A' (frozenset default_factory)
- Decision authority: CEO directed "greenlight to heal"; no autonomous scope expansion
- Wider-claim authority: still `inactive`
- New bounded-claim addition: **brain-layer refusal on reversible-tier DOM swap-attack** (proven by `gate_identity_break.py`)

---

## 1. The heals — each names source, file:line, verification

### Heal 1 — `Observation.identity_breaks` default_factory (CODE_MECHANIC v0.2.1 note-A')

[brain.py:23, 41–47](brain.py)

```python
from dataclasses import dataclass, field
...
identity_breaks: frozenset = field(default_factory=frozenset)
```

Cosmetic but correct: symmetric with the `list = field(default_factory=list)` pattern elsewhere. The original literal default (`frozenset()`) didn't have a runtime-mutation footgun (frozenset is immutable), but the `default_factory` form is what a CODE_MECHANIC review of a future field would expect to see, so making the asymmetry go away closes a future-noise channel.

### Heal 2 — `RuleBrain.decide` refuses on identity_break (DAVID+ v0.2.1 note-A)

[brain.py:85–117](brain.py)

Two failure shapes are caught, named explicitly in the new comment block:

- **Shape (a)**: an actionable the brain plans to touch is itself flagged as `identity_break` — the new id surfaced via DOM swap.
- **Shape (b)**: an actionable the brain EXPECTED to find is absent from the world AND at least one identity_break has been observed — the substrate saw a swap; the brain's expected target may be the swap victim.

In either shape, the deterministic Brain returns `Intent("done", summary="refusing on identity_break: ...")` rather than dispatching. The summary text explicitly names *which* affordance + *which* shape + *how many* swaps were detected, so an operator reading the trace sees the exact reason.

This is the **D-PERCEPTION-IS-A-SECURITY-SIGNAL** doctrine candidate ratified by ship: the substrate SEES the swap-attack signal end to end; the deterministic consumer now USES it.

### Heal 3 — sensor coords-key tightening (mid-build heal of heal 2)

[sensors.js:84–90](sensors.js)

Initial coords-key used `(round(x/8), round(y/8), round(w/16), round(h/16))` — included element dimensions in the slot identity. A swap that preserves position but changes label width (e.g., `"Save draft"` → `"Cancel"`, 77 px → 58 px) lands at different `round(w/16)` buckets and was NOT detected.

Heal: key on position only — `(round(x/8), round(y/8))`. Catches the typical button-swap shape: same top-left, different (id, signature). Verified by `gate_identity_break.py` PASS — the previous coords-key had this exact swap shape and silently let it pass.

### Heal 4 — `gate_identity_break.py` — proves the brain-layer defense

[gate_identity_break.py](gate_identity_break.py) (NEW FILE)

A reversible-tier swap-attack page: input email triggers a script that replaces a `Save draft` button (REVERSIBLE) with a `Cancel` button (REVERSIBLE) at the same on-screen slot. The Ceiling's keyword classifier would NOT block either click — both are reversible. The defense must come from the brain layer + the substrate's identity_break signal.

The gate runs the full Agent loop. Result:

```
agent.run() returned: True
page status: ''
refused on identity_break: True
  [0] acted intent=type target='Email' summary=''
  [1] done intent=done target='' summary="refusing on identity_break: expected affordance 'Save draft' is absent while 1 DOM swap(s) detected"
IDENTITY_BREAK GATE: PASS -- RuleBrain refused to act on an affordance that surfaced via DOM swap; reversible-tier swap-attack closed at the brain layer (sibling of the Ceiling's consequential-tier swap-label catch)
```

The page status is empty (the `Cancelled (attack)` swap-handler never fired). The trace shows the brain refused on the second step with the exact reason. The agent returned True because `Intent("done", summary=...)` IS the clean halt outcome — refusal with summary is the right shape for a brain that successfully resisted an attack.

---

## 2. Re-verification — 6/6 fast gates PASS + analytics intact + n=200 queued

| Gate | Result | Time |
|---|---|---|
| Phase 0 — `demo.py` | PASS — settled 1182 ms, 5 rows | 2.2 s |
| Phase 1 — `demo_thumb.py` | PASS — recovered dropped key | 1.7 s |
| Phase 2 — `harness.py 200` | **PASS — TFBthumb 0/200, baseline 130/200** | ~10 min |
| Phase 3 — `gate_ceiling.py` | PASS + swap-label PASS | 1.4 s |
| Phase 4 — `gate_agent.py` | PASS — 799 ms TFB vs 1652 ms baseline (2.1×) | 3.8 s |
| v0.2 effect-gate — `gate_sentinel.py` | PASS — Pay→POST single-effect-per-window still covered | 1.3 s |
| v0.2.2 identity_break — `gate_identity_break.py` | **PASS** — reversible-tier swap-attack closed at brain layer | 1.4 s |

Analytics receipt at `/tmp/claude-501/tfbthumb_sandbox/canonical_analytics_v0_2_2.json`:
- Token ratio: 7.7× (unchanged)
- Byte ratio: 55.5× (unchanged)
- Correctness n=60: 100% TFB vs 36.7% baseline (TFB unchanged; baseline within noise of prior 38.3%)
- Motion 4/4 (unchanged)
- Safety: 0 ungated; ledger verifies; tamper detected
- Signature 9/9 same

## 3. Bounded-claim set — one explicit addition

The v0.2 bounded-claim set in `REVIEWER_PACKET.md §1` is preserved. v0.2.2 adds one specific claim now provable:

> **The deterministic Brain refuses to dispatch on an affordance that surfaced via DOM swap.** Reversible-tier swap-attacks (where the Ceiling's keyword classifier alone would pass the click) are closed at the brain layer via `obs.identity_breaks` consultation. Proven by `gate_identity_break.py`.

This composes with the v0.2.1 swap-label catch (consequential-tier; proven by `gate_ceiling.py:swap-label`) to give complete tier coverage:

| Tier | Defense | Gate |
|---|---|---|
| CONSEQUENTIAL swap | Ceiling re-reads target_name at authorize time | `gate_ceiling.py:swap-label` |
| REVERSIBLE swap | RuleBrain refuses on identity_break | `gate_identity_break.py` |

Public claim wider than this still requires fresh CEO decision per `REVIEWER_PACKET.md §12`.

## 4. Warden Kill-Test (v0.2.2 heals layer)

- **Claim under review:** the 2 v0.2.1 notes are healed structurally; the new gate proves the brain-layer defense; no regression in any prior gate or analytics metric.
- **Null hypothesis:** the heals are cosmetic, OR the new gate tests something other than what it claims (e.g., the agent gives up for a different reason), OR a prior gate regressed.
- **Discriminating test:** parse-gate every file; run 6 fast gates; verify analytics; trace the new gate to confirm the refusal fires on the identity_break signal specifically (not on some other halt condition).
- **Outcome:** null killed. The new gate's trace shows `intent.summary` starting with `"refusing on identity_break:"` — the exact halt signature. No prior gate regressed (5/5 prior gates re-PASS). Analytics within noise. **Phase 2 n=200: TFBthumb 0/200 flakes, baseline 130/200 flakes — bounded-claim set holds at the literal blueprint contract on v0.2.2 SHAs.**
- **Present-tense downgrade:** *"v0.2.2 inspected internally on the same M5 Max sandbox by the same Claude Opus 4.7 session; awaiting independent reproduction when convenient."*

## 5. What this heals receipt authorizes

- The v0.2 bounded-claim set continues to hold at v0.2.2 SHAs.
- One explicit new claim: **brain-layer refusal on reversible-tier DOM swap-attack** (proven by `gate_identity_break.py`).
- The v0.2.1 + v0.2.2 heal chain composes — no falsification anywhere.

## 6. What this heals receipt does NOT authorize

- **No wider public claim than §3 above.** All `REVIEWER_PACKET.md §10` boundaries hold.
- **No re-validation by Gemini implied.** Gemini's `verified` is paired with the v0.2 SHAs.
- **No production-deployment statement.** Out-of-process Authority + (now) production-shape evaluation of identity_break false-positive rate on real-world SPA pages are required.
- **No claim that identity_break is bulletproof.** False-positive shape: a legitimate full DOM re-render (e.g., page transitions) could surface as a swap-attack to the deterministic brain. The brain refuses on the safe side; an LLM-Brain may be smarter. This is a feature, not a bug.

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

Files that changed in v0.2.2 (see `sha_manifest.txt` post-heal column):

- `brain.py` (heals 1 + 2)
- `sensors.js` (heal 3)
- `gate_identity_break.py` (NEW, heal 4)
- This file: `HEALS_v0_2_1_to_v0_2_2.md` (NEW)

Files NOT changed at v0.2.2: all of `retina.py`, `thumb.py`, `ceiling.py`, `sentinel.py`, `agent.py`, `demo.py`, `demo_thumb.py`, `harness.py`, `gate_ceiling.py`, `gate_agent.py`, `gate_sentinel.py`, `analytics.py`, plus all v0.2 + v0.2.1 receipts.

## 8. Closing

The 2 v0.2.1 notes were the smallest still-named gaps in the system after the 7 v0.2→v0.2.1 heals. Healing them closed:

- **note-A' (cosmetic):** the asymmetric `frozenset()` literal default.
- **note-A (substantive):** the deterministic Brain's blindness to `identity_break` despite the substrate carrying the signal end to end.

Plus one mid-build discovery: the original coords-key included width, which silently let label-width-changing swaps slip past. The position-only key catches the typical attack shape.

Plus one new gate proving the brain-layer defense end-to-end. The substrate now has a complete swap-attack defense at both tiers — consequential (Ceiling) and reversible (Brain).

Per `REVIEWER_PACKET.md §12`, any wider promotion still requires a fresh decision packet.
