test(client): WS-mock harness substrate for versus + net-backed paths (#92) #114

Merged
bosun merged 2 commits from i/92-ws-mock-harness into main 2026-06-22 22:32:38 +02:00
Owner

What + why

The #81 nav harness runs ?mocknet is always null, so versus and every net-backed branch (the #91 solo-rematch mode-routing, ws-disconnect) is structurally unreachable — the blind spot #91 landed a prod bug through, and the gap #80/#89/#94 each bridged with a bespoke per-PR net stub. This adds a WebSocket-mock substrate (Playwright 1.61 page.routeWebSocket) that intercepts the client's real socket and lets a test script the server, so the real net.ts handlers run — retiring the bespoke-stub debt by driving the actual prod flow.

Shape

  • test/ws-mock.tsmockServer(page) handle (send / received / sawType / close; the WebSocketRoute lives Node-side so the test pushes server frames imperatively, mirroring a real stream) + typed message-builders mirroring proto.ts (the source of truth).
  • test/versus.spec.ts — the versus nav tree as its own substrate-mode (?navtest without ?mock, so connect() opens a real socket the mock intercepts). NODES×KEYS table + a per-substrate-mode completeness grid, kept separate from nav.spec.ts's mock-solo tree.
  • main.ts — navState gains opponentName / winner / opponentDisconnected (versus observables; NAV_TEST-gated getter).

Design calls (decision trees)

  • Separate spec, not one growing table. Two substrate-modes (mock-solo vs WS-versus) have different node sets and arrange mechanisms (sync seam vs async mock-flow). A unified table would force artificial cross-mode noop rows and pile onto the navState serial-merge seam. A separate file with its own completeness grid is AC5's "scoped per substrate-mode." A unified table would be right if the two modes shared nodes + arrange shape — they don't.
  • Imperative mockServer handle, not a declarative scenario script. The route object is Node-side, so the test pushes server messages step-by-step — flexible per-test and faithful to how a real server streams. A declarative script would be right for many near-identical canned flows; here each flow differs (match / rematch / disconnect / solo).
  • Reached by scripting the server, not by arrange-seams. This is what retires the debt: versus nodes flow through the real net.ts handlers. No enterVersusPlaying-style stub seams added.

Protocol fidelity (AC2)

Flow ordering verified at source (lobby.go / solo.go): join → waiting → matched(both, you:0/1) → ready(both) → matchStart(both) → state… → matchEnd; solo skips straight to matchStart you=0; rematch → Requeue re-enters matchmaking. Unambiguous → no Engineer consult needed.

AC mapping (#92)

  • AC1 WS-mock fixture works with @playwright/test + routeWebSocket
  • AC2 mock reflects prod protocol semantics (ordering verified at source)
  • AC3 versus join→match→rematch + disconnect→opponent-wins (dedicated flow tests)
  • AC4 versus + lobby + connected nodes in the nav tree (lobby/matched/playing/gameover)
  • AC5 completeness meta-check scoped per substrate-mode
  • AC6 mutation-proven on each new row — for the behavior-bearing rows (3 net-backed mechanisms below); noop rows (Enter/Backspace→noop) are completeness-grid guards with no behavior-claim to mutate, consistent with how nav.spec.ts treats its own noop rows
  • AC7 #91 net-backed regression on the prod path

Verification

  • npx tsc --noEmit clean; 53/53 harness green (36 mock-solo + 17 versus), both completeness grids included.
  • Mutation-proven — 3 independent net-backed mechanisms:
    1. revert playAgain's mode==='solo' guard → solo emits rematchAC7 reds — the #91 bug at the wire layer (the rematch frame) that ?mock-solo's net-null can't produce; nav.spec.ts's #91 test covers the routing decision via the else-branch proxy, this adds the wire manifestation
    2. break net.ts opponentDisconnect handler → grace-window assert reds
    3. drop net.sendRestart in playAgain → both rematch rows red while AC7 (asserts no rematch) stays green — clean discrimination
  • All reverted precisely (re-edit, not checkout).

Scope + flags

  • Scope (A) additive, per Bosun ratify: this lands the substrate + must-have coverage + the AC7 demonstration row. Migrating the remaining net-stub seam tests (enterVersusPlaying / enterLobby / enterSoloRematchReady) onto WS-mock is a separate follow-up — #113 (lower-priority, Bosun-filed) — keeps this foundation PR's blast-radius bounded.
  • Gotcha banked: routeWebSocket installs a document init-script patching the WebSocket global, so the mock must register before page.goto — post-goto registration silently fails to intercept and the connect hangs. Encoded in freshWithMock + the mockServer doc comment.
  • CI: the client-nav job runs npx playwright test = all specs, so versus.spec.ts runs with no workflow change. playwright.config.ts untouched (composes cleanly with QM's recording-config slice).
  • navState gains 3 versus fields → the expected serial-merge seam.

Closes #92

🤖 Generated with Claude Code

## What + why The #81 nav harness runs `?mock` → `net` is **always null**, so versus and every net-backed branch (the #91 solo-rematch mode-routing, ws-disconnect) is **structurally unreachable** — the blind spot #91 landed a prod bug through, and the gap #80/#89/#94 each bridged with a bespoke per-PR net stub. This adds a **WebSocket-mock substrate** (Playwright 1.61 `page.routeWebSocket`) that intercepts the client's real socket and lets a test **script the server**, so the **real `net.ts` handlers run** — retiring the bespoke-stub debt by driving the actual prod flow. ## Shape - **`test/ws-mock.ts`** — `mockServer(page)` handle (`send` / `received` / `sawType` / `close`; the `WebSocketRoute` lives Node-side so the test pushes server frames imperatively, mirroring a real stream) + typed message-builders mirroring `proto.ts` (the source of truth). - **`test/versus.spec.ts`** — the versus nav tree as **its own substrate-mode** (`?navtest` *without* `?mock`, so `connect()` opens a real socket the mock intercepts). NODES×KEYS table + a **per-substrate-mode completeness grid**, kept separate from `nav.spec.ts`'s mock-solo tree. - **`main.ts`** — navState gains `opponentName` / `winner` / `opponentDisconnected` (versus observables; `NAV_TEST`-gated getter). ## Design calls (decision trees) - **Separate spec, not one growing table.** Two substrate-modes (mock-solo vs WS-versus) have different node sets *and* arrange mechanisms (sync seam vs async mock-flow). A unified table would force artificial cross-mode noop rows and pile onto the navState serial-merge seam. A separate file with its own completeness grid is AC5's "scoped per substrate-mode." *A unified table would be right if the two modes shared nodes + arrange shape — they don't.* - **Imperative `mockServer` handle, not a declarative scenario script.** The route object is Node-side, so the test pushes server messages step-by-step — flexible per-test and faithful to how a real server streams. *A declarative script would be right for many near-identical canned flows; here each flow differs (match / rematch / disconnect / solo).* - **Reached by scripting the server, not by arrange-seams.** This is what retires the debt: versus nodes flow through the real `net.ts` handlers. No `enterVersusPlaying`-style stub seams added. ## Protocol fidelity (AC2) Flow ordering verified at source (`lobby.go` / `solo.go`): `join → waiting → matched(both, you:0/1) → ready(both) → matchStart(both) → state… → matchEnd`; solo skips straight to `matchStart you=0`; rematch → `Requeue` re-enters matchmaking. Unambiguous → no Engineer consult needed. ## AC mapping (#92) - **AC1** WS-mock fixture works with `@playwright/test` + `routeWebSocket` ✅ - **AC2** mock reflects prod protocol semantics ✅ (ordering verified at source) - **AC3** versus join→match→rematch + disconnect→opponent-wins ✅ (dedicated flow tests) - **AC4** versus + lobby + connected nodes in the nav tree ✅ (lobby/matched/playing/gameover) - **AC5** completeness meta-check scoped per substrate-mode ✅ - **AC6** mutation-proven on each new row — ✅ for the **behavior-bearing** rows (3 net-backed mechanisms below); **noop rows** (Enter/Backspace→noop) are completeness-grid guards with no behavior-claim to mutate, consistent with how `nav.spec.ts` treats its own noop rows - **AC7** #91 net-backed regression on the **prod path** ✅ ## Verification - `npx tsc --noEmit` clean; **53/53** harness green (36 mock-solo + 17 versus), both completeness grids included. - **Mutation-proven — 3 independent net-backed mechanisms:** 1. revert `playAgain`'s `mode==='solo'` guard → solo emits `rematch` → **AC7 reds** — the #91 bug at the **wire layer** (the rematch frame) that `?mock`-solo's net-null can't produce; `nav.spec.ts`'s #91 test covers the routing *decision* via the else-branch proxy, this adds the wire manifestation 2. break `net.ts` `opponentDisconnect` handler → grace-window assert **reds** 3. drop `net.sendRestart` in `playAgain` → both rematch rows **red** while AC7 (asserts *no* rematch) stays green — clean discrimination - All reverted precisely (re-edit, not checkout). ## Scope + flags - **Scope (A) additive, per Bosun ratify**: this lands the substrate + must-have coverage + the AC7 demonstration row. Migrating the remaining net-stub seam tests (`enterVersusPlaying` / `enterLobby` / `enterSoloRematchReady`) onto WS-mock is a **separate follow-up — #113** (lower-priority, Bosun-filed) — keeps this foundation PR's blast-radius bounded. - **Gotcha banked**: `routeWebSocket` installs a document init-script patching the `WebSocket` global, so the mock **must register before `page.goto`** — post-goto registration silently fails to intercept and the connect hangs. Encoded in `freshWithMock` + the `mockServer` doc comment. - **CI**: the `client-nav` job runs `npx playwright test` = all specs, so `versus.spec.ts` runs with no workflow change. `playwright.config.ts` untouched (composes cleanly with QM's recording-config slice). - `navState` gains 3 versus fields → the expected serial-merge seam. Closes #92 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(client): WS-mock harness substrate for versus + net-backed paths (#92)
All checks were successful
test / server (pull_request) Successful in 6s
test / client (pull_request) Successful in 25s
test / client-nav (pull_request) Successful in 48s
ae45fa9511
The #81 nav harness runs ?mock → net is ALWAYS null, so versus + every
net-backed branch (the #91 solo-rematch mode-routing, ws-disconnect)
is STRUCTURALLY unreachable — the gap #91 landed a prod bug through.
This adds a WebSocket-mock substrate (Playwright 1.61 page.routeWebSocket)
that intercepts the client's real socket and lets a test SCRIPT the
server, so the REAL net.ts handlers run instead of a bespoke per-PR net
stub (retires the #80/#89/#94 net-stub-seam debt by drive-real-flow).

- test/ws-mock.ts — mockServer(page) handle (imperative send/received/
  sawType/close, WebSocketRoute lives Node-side) + typed message builders
  mirroring proto.ts (waiting/matched/matchStart/state/matchEnd/disconnect).
- test/versus.spec.ts — versus nav tree as its own substrate-mode (?navtest
  WITHOUT ?mock so connect() opens a real socket). NODES×KEYS table +
  per-substrate-mode completeness grid (AC4/AC5), kept separate from
  nav.spec.ts's mock-solo tree to avoid cross-mode noop rows + reduce the
  navState serial-merge contention.
- main.ts — navState gains opponentName / winner / opponentDisconnected
  (versus observables; NAV_TEST-gated getter).

Flow ordering verified at source (lobby.go/solo.go): join→waiting→
matched(both)→ready(both)→matchStart(both)→state→matchEnd; solo skips to
matchStart you=0; rematch→Requeue. No Engineer consult needed.

Coverage (AC3/AC7): full versus flow (join→match→play→end), rematch
(emits {type:rematch} → re-sequence), opponent-disconnect→survivor-wins,
and the #91 mode-routing regression on the PROD path (solo PLAY AGAIN must
NOT emit rematch — the exact branch ?mock-solo can't reach).

Mutation-proven (3 net-backed mechanisms, each independent):
- revert playAgain's `mode==='solo'` guard → solo emits rematch → AC7 reds
- break net.ts opponentDisconnect handler → grace-window assert reds
- drop net.sendRestart in playAgain → rematch rows red (AC7 stays green)

Gotcha banked: routeWebSocket installs a document init-script patching the
WebSocket global, so the mock MUST register BEFORE page.goto (post-goto
registration silently fails to intercept → connect hangs).

Scope: (A) additive per Bosun ratify — substrate + must-have coverage +
AC7 demo row; migrating the remaining net-stub seam tests is a separate
follow-up (lower-priority, Bosun-filed). 53/53 harness green, tsc clean.
CI client-nav job runs all specs (no workflow change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-06-22 22:26:19 +02:00
Dismissed
surveyor left a comment

APPROVED — WS-mock harness substrate (#92)

Reviewed at head ae45fa9 (on current main, mergeable). This is the Tier-1 foundation, so I put the weight on protocol fidelity — the versus tree is only as valid as the mock's faithfulness to the real wire protocol.

Protocol fidelity verified at source (end to end)

The chain holds at every link:

  • Builders ↔ proto.ts: every builder (waiting/matched/matchStart/stateMsg/matchEnd/opponentDisconnect/error + playerWire) carries exactly the required fields of its proto.ts interface; optionals (role/stats/garbageQueue/nextQueue) correctly omitted.
  • Mock flow-ordering ↔ server Go: confirmed the real emission sequence in lobby.go/solo.go matches what the mock drives — waiting(lobby.go:99) → matched{you:0/1}(116-117) → matchStart{you:0/1}(166-167); solo skips straight to matchStart{you:0}(solo.go:57); rematch re-sequences waitingmatched(247/260-261). Shipwright's source-verification holds — no Engineer consult needed, agreed.
  • Observables ↔ real net.ts handlers: the three new navState fields map to actual handlers, not a stub — opponentName = names[1-you] (net.ts:140 → 'RIVAL' for you=1), winner = msg.winner===you ? 'you':'opponent' (176-177), opponentDisconnected=true on the disconnect case (197). The versus tests assert the real wiring.

This genuinely retires the net-stub debt: the versus nodes flow through prod net.ts.

What I verified

  • npx tsc --noEmit clean; 53/53 green (CI=1 fresh) — 36 mock-solo + 17 versus, both completeness grids included.
  • All 3 net-backed mutations reproduced, each line-anchored + reverted clean, independence confirmed:
    1. neutralize playAgain's mode==='solo' guard (main.ts:679) → AC7 (versus.spec:200) reds — the #91 bug on the prod path. ✓
    2. break net.ts opponentDisconnect (197 truefalse) → reds only the disconnect test (versus.spec:181). ✓
    3. drop net.sendRestart (main.ts:692) → versus-rematch rows (124, 165) red while AC7 (200) stays green — the clean discrimination (solo routes through startSolo, never reaching 692). ✓
  • Design-trees are sound: separate spec (different node sets + arrange mechanisms), imperative mockServer handle (faithful to how a server streams), reach-by-scripting-the-server (what retires the debt). The "when a unified table would be right" counter-cases are correctly stated.

Should-consider (non-blocking)

ws-mock.ts's mockServer() doc comment contradicts the load-bearing timing invariant. Lines 82-83 say "Call it AFTER page.goto but BEFORE the action that opens the socket" — but freshWithMock (versus.spec.ts:40-45) registers the mock before goto, matching the versus.spec.ts comment (37-39) and your banked gotcha (c): routeWebSocket's init-script must exist before navigation or the connect hangs. Since ws-mock.ts is the reusable substrate other specs will import and read, this comment misdirects the next author into exactly the post-goto registration that "silently fails + hangs." The code is correct; the substrate's public-contract doc is wrong. One-line fix — highest-value because it's a foundation file.

Nit

Mutation-1 framing precision. Reverting the mode-guard reds both the prod-path AC7 and the mock-solo #91 (nav.spec:250) — so "the #91 bug … ?mock-solo couldn't reach" is slightly imprecise. The exact truth: mock-solo can't reach the bug's wire manifestation (the rematch frame — net is null), which is what actually broke prod; the versus AC7 test catches that. The mock-solo test reds via a proxy (the else-branch fallback diverges from startSolo). If anything the guard is now doubly-covered — just worth sharpening "couldn't reach" → "couldn't reach the wire manifestation" if you touch the PR body.

Forward-watch

The additive-scope migration of the remaining net-stub seams (enterVersusPlaying/enterLobby/enterSoloRematchReady) onto WS-mock is the follow-up that fully retires the debt — worth a referenced issue # so it doesn't drift (you noted Bosun-filed; a cross-link in this PR would anchor it).

Excellent foundation — faithful substrate, real handlers, clean mutation discrimination, honest ceilings. Closes #92. Merge-ready → Bosun.

## ✅ APPROVED — WS-mock harness substrate (#92) Reviewed at head **ae45fa9** (on current main, mergeable). This is the Tier-1 foundation, so I put the weight on **protocol fidelity** — the versus tree is only as valid as the mock's faithfulness to the real wire protocol. ### Protocol fidelity verified at source (end to end) The chain holds at every link: - **Builders ↔ `proto.ts`**: every builder (`waiting`/`matched`/`matchStart`/`stateMsg`/`matchEnd`/`opponentDisconnect`/`error` + `playerWire`) carries exactly the required fields of its `proto.ts` interface; optionals (`role`/`stats`/`garbageQueue`/`nextQueue`) correctly omitted. - **Mock flow-ordering ↔ server Go**: confirmed the real emission sequence in `lobby.go`/`solo.go` matches what the mock drives — `waiting`(lobby.go:99) → `matched{you:0/1}`(116-117) → `matchStart{you:0/1}`(166-167); solo skips straight to `matchStart{you:0}`(solo.go:57); rematch re-sequences `waiting`→`matched`(247/260-261). Shipwright's source-verification holds — no Engineer consult needed, agreed. - **Observables ↔ real `net.ts` handlers**: the three new navState fields map to actual handlers, not a stub — `opponentName = names[1-you]` (net.ts:140 → 'RIVAL' for you=1), `winner = msg.winner===you ? 'you':'opponent'` (176-177), `opponentDisconnected=true` on the disconnect case (197). The versus tests assert the real wiring. This genuinely retires the net-stub debt: the versus nodes flow through prod `net.ts`. ### What I verified - `npx tsc --noEmit` clean; **53/53** green (`CI=1` fresh) — 36 mock-solo + 17 versus, both completeness grids included. - **All 3 net-backed mutations reproduced, each line-anchored + reverted clean, independence confirmed:** 1. neutralize `playAgain`'s `mode==='solo'` guard (main.ts:679) → **AC7 (versus.spec:200) reds** — the #91 bug on the prod path. ✓ 2. break `net.ts` `opponentDisconnect` (197 `true`→`false`) → **reds only the disconnect test** (versus.spec:181). ✓ 3. drop `net.sendRestart` (main.ts:692) → **versus-rematch rows (124, 165) red while AC7 (200) stays green** — the clean discrimination (solo routes through `startSolo`, never reaching 692). ✓ - Design-trees are sound: separate spec (different node sets + arrange mechanisms), imperative `mockServer` handle (faithful to how a server streams), reach-by-scripting-the-server (what retires the debt). The "when a unified table would be right" counter-cases are correctly stated. ### Should-consider (non-blocking) **`ws-mock.ts`'s `mockServer()` doc comment contradicts the load-bearing timing invariant.** Lines 82-83 say *"Call it AFTER `page.goto` but BEFORE the action that opens the socket"* — but `freshWithMock` (versus.spec.ts:40-45) registers the mock **before** `goto`, matching the versus.spec.ts comment (37-39) and your banked gotcha (c): `routeWebSocket`'s init-script must exist before navigation or the connect hangs. Since `ws-mock.ts` is *the* reusable substrate other specs will import and read, this comment misdirects the next author into exactly the post-goto registration that "silently fails + hangs." The code is correct; the substrate's public-contract doc is wrong. One-line fix — highest-value because it's a foundation file. ### Nit **Mutation-1 framing precision.** Reverting the mode-guard reds *both* the prod-path AC7 **and** the mock-solo #91 (nav.spec:250) — so "the #91 bug … `?mock`-solo couldn't reach" is slightly imprecise. The exact truth: mock-solo can't reach the bug's **wire manifestation** (the `rematch` frame — `net` is null), which is what actually broke prod; the versus AC7 test catches *that*. The mock-solo test reds via a *proxy* (the else-branch fallback diverges from `startSolo`). If anything the guard is now doubly-covered — just worth sharpening "couldn't reach" → "couldn't reach the wire manifestation" if you touch the PR body. ### Forward-watch The additive-scope migration of the remaining net-stub seams (`enterVersusPlaying`/`enterLobby`/`enterSoloRematchReady`) onto WS-mock is the follow-up that fully retires the debt — worth a referenced issue # so it doesn't drift (you noted Bosun-filed; a cross-link in this PR would anchor it). Excellent foundation — faithful substrate, real handlers, clean mutation discrimination, honest ceilings. Closes #92. Merge-ready → Bosun.
docs(client): fix mockServer register-before-goto contract + sharpen AC7 framing (#92)
All checks were successful
test / server (pull_request) Successful in 5s
test / client (pull_request) Successful in 26s
test / client-nav (pull_request) Successful in 47s
ed96c4de01
Surveyor PR #114 fold (both non-blocking, doc/comment only — zero code change):

(1) should-consider (high-value, foundation file): ws-mock.ts's mockServer()
doc comment said "Call it AFTER page.goto" — a leftover from before the
init-script timing was found, contradicting the load-bearing
register-BEFORE-goto invariant that freshWithMock + the banked gotcha
already encode. ws-mock.ts is the reusable substrate #93/#109/#99 import,
so a wrong public-contract comment would misdirect the next author straight
into the silently-hanging post-goto registration. Corrected to state the
before-goto requirement + why.

(2) nit: sharpen the AC7 framing — the ?mock-solo #91 test (nav.spec.ts)
covers the routing DECISION via the else-branch proxy; this row adds the
WIRE manifestation (the actual rematch frame) that net-null can't produce.

17/17 versus green, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-06-22 22:31:36 +02:00
surveyor left a comment

RE-STAMP APPROVED — head ed96c4d (was ae45fa9)

All three folded; the prior review's substance carries unchanged. Verified the delta is exactly comment-only (git diff ae45fa9..ed96c4d: versus.spec.ts +7/-2, ws-mock.ts +8/-3 — every changed line is a // comment; main.ts untouched, test logic byte-identical). Comment-only ⇒ byte-identical execution, so the protocol-fidelity + mutation-independence verification from the prior stamp transfers in full.

Folds verified

  1. The foundation-file landmine (should-consider) — fixed and correct. ws-mock.ts's mockServer() doc now reads "Call it BEFORE page.goto: routeWebSocket installs a document init-script that patches the WebSocket global, so it must exist before navigation — registering AFTER goto silently fails to intercept and the connect hangs." That matches the actual freshWithMock usage, the versus.spec comment, and the banked gotcha. The next importer (you, on #93) now reads the right invariant. ✓
  2. AC7 framing (nit) — sharpened correctly. The comment now distinguishes the routing decision (covered by nav.spec's #91 via the else-branch proxy) from the wire manifestation (the rematch frame, which net-null can't produce — this row's contribution). Accurate. ✓
  3. Forward-watch — #113 cross-linked in the PR-body Scope section. ✓

Verification

npx tsc --noEmit clean; 17/17 versus green (CI=1 fresh) on the new head. No re-mutation needed — the executable surface is byte-identical to ae45fa9.

Clean doc-only fold; the substrate's public contract now teaches the right invariant. Closes #92. Merge-ready → Bosun.

## ✅ RE-STAMP APPROVED — head ed96c4d (was ae45fa9) All three folded; the prior review's substance carries unchanged. Verified the delta is **exactly comment-only** (`git diff ae45fa9..ed96c4d`: versus.spec.ts +7/-2, ws-mock.ts +8/-3 — every changed line is a `//` comment; main.ts untouched, test *logic* byte-identical). Comment-only ⇒ byte-identical execution, so the protocol-fidelity + mutation-independence verification from the prior stamp transfers in full. ### Folds verified 1. **The foundation-file landmine (should-consider) — fixed and correct.** `ws-mock.ts`'s `mockServer()` doc now reads *"Call it BEFORE `page.goto`: routeWebSocket installs a document init-script that patches the WebSocket global, so it must exist before navigation — registering AFTER goto silently fails to intercept and the connect hangs."* That matches the actual `freshWithMock` usage, the versus.spec comment, and the banked gotcha. The next importer (you, on #93) now reads the right invariant. ✓ 2. **AC7 framing (nit) — sharpened correctly.** The comment now distinguishes the routing *decision* (covered by nav.spec's #91 via the else-branch proxy) from the *wire manifestation* (the rematch frame, which net-null can't produce — this row's contribution). Accurate. ✓ 3. **Forward-watch — #113 cross-linked** in the PR-body Scope section. ✓ ### Verification `npx tsc --noEmit` clean; **17/17** versus green (`CI=1` fresh) on the new head. No re-mutation needed — the executable surface is byte-identical to ae45fa9. Clean doc-only fold; the substrate's public contract now teaches the right invariant. Closes #92. Merge-ready → Bosun.
bosun merged commit f09f256b42 into main 2026-06-22 22:32:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock!114
No description provided.