# W33 — the tripwire

Third wave of **Milestone #38** (*the control surface*). One Request — **#26** — and it
is unlike every other wave in this milestone: nothing about it is visible on the board.
It is a **regression tripwire** on the tag scanner, and its acceptance is a red-first
test, not an Operator's read.

W32 shipped as **v0.9.0** + **v0.9.1** and is closed by the installed binary; #11 and #18
await the Operator's board read.

**Precedence resolved:** `M38-PLAN.md` says #26 must not be prioritised against **#25**.
#25 is **Done** (closed completed 2026-07-26), so nothing blocks this wave.

## What the shape is, precisely

`scan_digest` (`src/tags.rs`) fires per **sealed turn** and advances the cursor over that
turn's `input_seq`. Two `continue`s hold a turn back, and one of them is silent:

```rust
if turn.get("partial")… { continue; }                      // still open — fires when it seals
let Some(input_seq) = turn.get("input_seq")… else { continue };   // ← this one
```

That second jump is correct for an **`input: null` preamble turn** — non-partial and
seq-less *by design*, where Boundary/Context entries live, and holding on it wedges the
cursor at every session boot. It is a **silent tag loss** for the other shape it also
catches: a turn that is non-partial, carries **no `input_seq`**, and is **input-bearing**.
That turn could hold a tag, and today it is skipped with nothing said.

`REQ-TAG-CURSOR-JUMP-ANNOUNCE` is that discrimination plus a notice.

## The rulings this wave inherits — do not re-derive them

1. **Announce-and-advance is TERMINAL, not interim** (doyle's ruling 2026-07-26, liam's
   census as the discriminator). A barrier would need a *will-seal vs never-seals*
   discrimination, which is a negative over the future; the heuristic that approximates
   it is diagnostic-only upstream. So the cursor **still advances** — it just stops being
   quiet about it.
2. **Delivery is `spt send --active-only`** (Operator ruling 2026-07-28). Durable, so it
   is never dropped the way live-only shell sensory would be, and it rides the target's
   own poll rather than waking an idle owner. Verified present on this node's CLI:
   *"Deliver only through the target's own poll (the no-interrupt hook channel); never
   wakes an idle target."*
3. **This is a tripwire against a digest-producer regression, NOT a live leak.** The shape
   is unreachable under the published turn contract (input-bearing turns carry a seq, open
   turns carry `partial`, preamble turns carry `input: null`). Which decides the fixture
   rule below.

## The fixture rule (binding)

**Red-first against a rig-constructed digest snapshot.** The shape cannot be produced on a
live node, and **no specimen is to be manufactured on a live endpoint** — the test builds
the JSON by hand, exactly as `tags.rs`'s existing digest tests already do (`fn digest(...)`
in its test module). A test that needed a live producer to emit an impossible turn would be
a test we could never run, and manufacturing one on a real endpoint corrupts a real
owner's cursor.

## Tasks

- **T1 — impl. The discrimination.** In `scan_digest`, split today's seq-less `continue`
  in two on the turn's `input`: `input: null` (or absent) stays a **silent** jump; an
  input-bearing turn with no `input_seq` becomes a **recorded** jump. `DigestScan` gains
  the jumped turns — enough to name what was skipped in the notice (the turn's position
  and, where present, its entry seq span), and nothing more.
- **T2 — impl. The notice.** `TagPump::tick` emits one owner-facing line per jumped turn,
  in the same `Vec<String>` the cursor-slide notice and the confirm-expiry notices already
  ride. It names what was skipped and says the tag may need re-issuing — the same contract
  as `[tag] digest window slid past my cursor`, which is the precedent to copy verbatim in
  tone and shape.
- **T3 — impl. The delivery flag.** `main.rs` currently sends every `tick` message with a
  bare `spt.send`. This wave needs `--active-only` for the jump notice. Decide once,
  narrowly: either `Spt::send` grows an explicit variant (`send_active_only`) used for
  this notice, or the pump returns a typed message rather than a bare `String`. **Open
  design question, resolve at T3 and record the reason** — the constraint is that no
  *existing* notice silently changes delivery class as a side effect. Tag replies are
  answers to something the owner just did and must not become poll-only by accident.
- **T4 — unit (red-first).** Rig-constructed snapshots, all four shapes in one place:
  an input-bearing seq-less turn **announces** and the cursor advances past the turns
  around it; an `input: null` preamble turn is jumped **silently** (the wedge case — assert
  no notice AND no cursor stall); a partial turn is still held with no notice; and a normal
  sealed turn fires as it always did. Plus: the notice names the skipped turn.
- **T5 — doc.** `CONTEXT.md` states the property beside the existing quotation/cursor
  rules, and `docs/KNOWN-HAZARDS.md` gains the entry — the milestone plan says the hazard
  entry lands with this wave, not before.

## Gate

`cargo test` green, `./traceable-reqs.exe check` exit 0, `REQ-TAG-CURSOR-JUMP-ANNOUNCE`
activated at `["doc", "impl", "unit"]` with its evidence tagged in the same commit.

**Acceptance is the red-first test, not a board read** — this is the one wave in Milestone
#38 whose subject never renders. What it must NOT need is a live specimen: if the only way
to see it pass is to break a real endpoint's digest, the test is wrong.

## Not in this wave

- **Any barrier / hold-until-sealed behavior.** Ruled terminal against; re-opening it needs
  a will-seal discrimination upstream, not a local heuristic.
- **The `--active-only` question generalised.** Only the jump notice changes class here.
