Commit Graph

1276 Commits

Author SHA1 Message Date
Edward Langley c5e77591d3 tooling: a merge driver for .beads/issues.jsonl
issues.jsonl is a passive export whose rows are ordered by insertion, so two
sides that file issues collide in the same region even when the issues are
disjoint. Git line-merges it and produces conflict markers INSIDE the JSONL,
which breaks bd's auto-export silently -- observed today, where a stash-pop
left `<<<<<<< Updated upstream` in the file and auto-export was skipping with
"invalid character '<'" until the file was regenerated.

The driver does no merge logic of its own, because bd already has the
semantics: `bd import` skips any row whose updated_at is not strictly newer
than the local one, creates ids it has not seen, and keeps the local row on a
tie. So importing BOTH sides cannot move the database backwards, and the file
is then regenerated from it. The database is the truth; the file is derived.

Deliberately not reimplemented here: field-level reconciliation. `bd conflicts
show/resolve` already does that against the Dolt working set, which is the real
sync path -- a second copy against the text export would have to track bd's
schema by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 14:22:08 -07:00
Edward Langley e4adf86c1a style: cargo fmt the assertion 2b086b2 added
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
2b086b2 landed an assert_eq! that rustfmt wraps, and `cargo fmt --all --check`
is the first step of the host CI job -- so every step after it was SKIPPED,
including the test run. PR #101 inherited the red through its merge commit
while its own file is formatted correctly.

Run the formatter before committing, not after CI says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 14:02:05 -07:00
Ed Langley 56ee496d30 Merge pull request #100 from fiddlerwoaroof/cursor/adversarial-review-2026-09-21-5eff
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Adversarial code review 2026-09-21
2026-09-21 13:51:26 -07:00
Edward Langley 5f47fecf05 chore(merge): gh/main — PR #100 adversarial review findings
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
# Conflicts:
#	.beads/issues.jsonl
2026-09-21 13:50:16 -07:00
Edward Langley 2b086b205a test: parse_duplicate_categories could not fail for the merge it names
Found by an external Grok Bot adversarial review (improvise-jkxr), verified by
injection rather than taken on report.

The test parses two `## Category: Type` sections -- `- A` then `- B` -- and
asserted only `!item_names.is_empty()`. push_category concatenates on a
duplicate section name, and a merge keeping EITHER half alone still leaves the
list non-empty, so only a total wipe could ever have failed it.

Fault injected: `existing.items.extend(items)` -> `existing.items = items`, so
a second section replaces the first. The old assertion PASSED; the new one
fails with left ["B"], right ["A", "B"].

Pins order as well as membership -- insertion order is load-bearing (gotcha 18)
-- matching the standard its neighbour already holds groups to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 13:49:58 -07:00
Grok Bot (Ed Langley) 2705c16cab beads: file PR #100 adversarial review findings
24 new beads from cloud adversarial review PR #100 (P1–P3). No application
code changes. Map: F01–F05 P1, consolidated P2/P3 transport and ws-server,
F21 noted as dup of iobc.
2026-09-21 13:49:44 -07:00
Edward Langley d358da46a1 chore(merge): gh/main — Grok Bot adversarial findings (jkxr, df2w)
# Conflicts:
#	.beads/issues.jsonl
2026-09-21 13:47:48 -07:00
Edward Langley 62d3af8f10 chore: various things 2026-09-21 13:46:31 -07:00
Cursor Agent 220944e26c Add final subagent findings (33 total)
New P1 findings:
- F04: Auto-persist failure in worker not communicated to client
- F05: dispatch() swallows interactive() errors silently

New P2 findings:
- F15: law_toggle_group_involution test doesn't apply effects
- F16: law_sequence_associativity test proves tautology
- F17: ServerFrame serialization failure silently dropped
- F18: Unparseable ClientFrame silently ignored
- F19: ClientFrame for closed document produces no response
- F20: Debug-string tests verify effect names, not correctness

Total: 5 P1, 18 P2, 10 P3 = 33 findings

Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com>
2026-09-21 20:45:56 +00:00
Edward Langley f9d4e3bb3e chore(merge): gh/main 2026-09-21 13:44:56 -07:00
Grok Bot (Ed Langley) fdcb9063dc beads: adversarial review findings (jkxr, df2w)
Vacuous parse_duplicate_categories checker; open empty formula panel
invisible via emptiness-as-closed RenderModel encoding.
2026-09-21 13:44:16 -07:00
Cursor Agent b3c941172a Expand review with subagent findings
Adds 15 additional findings from parallel crate exploration:
- P1: CSV NaN/Infinity silent corruption (F01)
- P1: Optimistic fold no rollback on rejection (F02)
- P1: Worker startup silent dead state (F03)
- P2: Socket CLOSING state throw (F06)
- P2: Render failure infinite retry (F07)
- P2: Date detection sampling limit (F08)
- P2: Mixed-type column misclassification (F09)
- P2: Vacuous test assertions (F10, F11)
- P2: Non-exhaustive wildcards (F12)
- P2: Silent wasm serialization failures (F13, F14)

Total findings: 25 (3 P1, 12 P2, 10 P3)

Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com>
2026-09-21 20:44:00 +00:00
Edward Langley 132728d269 docs: the sync-access-handle premise is dedicated-worker-only
Two agents' findings, filed and recorded.

Gotcha 28 read as though "a browser worker" were one thing. Measured in a
module SharedWorker on Chromium and Firefox, createSyncAccessHandle is ABSENT
-- getDirectory and getFileHandle both succeed and the method is not on the
object -- so a SharedWorker cannot host OpfsFs and the synchronous Storage
trait rests on a narrower premise than the paragraph stated. OpfsFs::open's
own guard does not enforce it either: it dyn_into's WorkerGlobalScope, which
a SharedWorkerGlobalScope satisfies.

Also adds web/shell.js and web/serial.js to the File Inventory. shell.js holds
gridPlan/frameCostOf/documentTitle, so some of the browser's render decisions
are no longer in the file gotchas 42 and 46 name.

Beads filed: g3no i0v9 uej8 s8h7 lut7 wk18 0ygd hs0u pcje 7xyx 7gip 12ha

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 13:41:33 -07:00
Edward Langley 074569e150 chore: add skills in less claude-specific locations 2026-09-21 13:40:54 -07:00
Cursor Agent 5225a3e0f8 Add adversarial code review 2026-09-21
Documents patterns that could hide bugs or silently swallow errors:
- Parser wildcards that ignore unknown grammar rules (F01)
- Formula parser defaults unknown operators (F02)
- Worker seed fetch swallows errors (F03)
- WebSocket drops messages during state transitions (F04)
- Effect error handling asymmetries (F05-F06)
- Test coverage gaps for new projection types (F08)

Review follows repository doctrine from .claude/skills/code-review/SKILL.md

Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com>
2026-09-21 20:40:46 +00:00
Edward Langley 856a5f84eb chore: update issues.jsonl 2026-09-21 13:40:08 -07:00
Grok Bot (Ed Langley) bb69a157e0 beads: file web smoke bugs from external Grok Bot hunt
OPFS handshake flake, silent seed/doc failures, narrow-grid clip,
Esc leaving formula panel open, and Create-cell silent decline.
2026-09-21 13:33:27 -07:00
Edward Langley 1f2886873b kb: a derivation protects only what it derives
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Three facts from the UX audit, which found the product mostly coherent and
the incoherence concentrated in two places.

The help pages are DERIVED from the keymap and a row naming a nonexistent key
is dropped — good architecture, working. The ex-command rows are Literal and
escaped it, and that is exactly where the worst content bug lives: every
documented spelling of :formula fails, on the quick-start page, for the
headline feature, because the parser splits on whitespace and nothing says to
quote. Verified. When a derivation lands, enumerate what it does not cover,
because the covered part now looks trustworthy and the uncovered part looks
identical.

The browser paints the host's hints and help over a smaller capability set:
four advertised keys decline out loud, ten more are silent, and help page 2's
whole Search section is taught to a browser user who cannot use it. A shared
vocabulary still owes a per-surface capability filter.

And the status bar contradicts the wizard overlay on four of eight steps —
on Done it says Esc cancels where Esc goes back. Two writers of one fact,
improvise-edp's disease in the UI layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 13:30:40 -07:00
Edward Langley 532c7f2405 docs: the gotcha-44 gap list named two closed tickets as open
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
improvise-h0iv and improvise-1wl0 are both fixed — group_runs coalesces by
header identity via ptr::eq, and AxisFrame::first carries no serde(default),
its doc comment saying why. The repo map listed both as 'still open beside
it'. Verified against the code before correcting.

That is the second time this exact paragraph has done it, and
doctrine(a_stale_list_of_what_is_missing_is_worse_than_no_list) was written
about its previous instance — by me, two days ago, after which I committed
the next one. A list of what is missing decays in the one direction that
reads as a to-do.

Also files the bug-hunt findings. Six hunters over six territories produced
five P1s, every one demonstrated rather than reasoned:

  dy3j  a Label category is not persisted, so a document with 12 categories
        plus a label can NEVER BE REOPENED
  o9uq   in records mode edits someone else's record and can rename it
        away — silent data corruption from a documented key
  pv3g  fit-measure panics on large finite values; a hard crash in the TUI
  bd9o  the browser reaches records mode through the tile bar, and everything
        it can do there is silent
  (plus the P2 set: prune_empty never persisted, the browser drawing neither
   page bar nor totals, two prune_axis_items group bugs, an inline-edit
   commit addressing the current cache while the screen is frozen)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 13:26:29 -07:00
Edward Langley 70025e2e33 chore: close PR #98's three tickets
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
improvise-lltt (P1), improvise-e5pf and improvise-ac2z all landed in #98 and
were left open — I closed #96's tickets on merge and not #98's. Each verified
against merged main rather than the PR body: App::view_mut() is gone and
view_mut_of is present; resolve::changed_mode is gone and settled_mode is in
improvise-protocol; grid_in::rederived_for is present.

Second occurrence of this gap. The first was 26 tickets at once, and the cause
is the same: .beads is orchestrator-only, so an agent cannot close what it
fixed, and closing is a separate step I have to remember after every merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 13:15:09 -07:00
Edward Langley aeda06a182 kb: a worktree agent merges the orchestrator's uncommitted local main
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
I committed context/code/pr-review.md straight to local main without a
build-doc-site manifest entry. Agents in worktrees share the repository's local
refs, so 'git merge main' takes my local main INCLUDING commits never pushed —
and PR #98's branch went red on the doc-site gate, which is improvise-res9
working exactly as designed.

The agent diagnosed it as 'main has been red since #96'. Both halves were
wrong: CI on #96's merge was green, and the doc was not in main when #96's
agent merged at ea206bf. It entered between, so only the later branch saw it.
Recording that because the wrong attribution is the interesting part — the
cost lands on whoever merges next, who then debugs somebody else's change.

My local main is an unreviewed publishing channel into every running agent. A
commit there is subject to the same gates as a PR without getting them.

Also filed as a doctrine: a manifest gate over a repository-wide invariant
should run where the violation is AUTHORED, not only where the tree is
assembled. It sits in the build job, so the author never sees the failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 11:18:37 -07:00
Ed Langley ae4ba44d60 Merge pull request #98 from fiddlerwoaroof/wave/operandless-commands
Wave L: operandless commands name their operand (lltt, e5pf, ac2z)
2026-09-21 11:17:35 -07:00
Edward Langley 787e354b9b doc-site: name context/code/pr-review.md as an omission
The `build` job fails on main and on every branch merged from it:

  doc-site: context/code/pr-review.md is neither published nor a named omission

NOT a merge loss -- scripts/build-doc-site.mjs in this branch is
byte-identical to main's, and neither lists the file. PR #96 added
context/code/pr-review.md without a manifest line, which is exactly the
gate improvise-res9 added: a doc added without a line publishes nothing
and says nothing, so the generator refuses.

Omitted rather than published, and the line is cloud-agents.md's rather
than tooling.md's. tooling.md explains checkers a reader of the code will
meet; pr-review.md is about THIS REPOSITORY'S PROCESS -- where a Codex
review appears, how to tell it from our own agents' comments, why absence
is not approval. Nobody visiting a website is about to read a PR thread.

Both homes updated, because an omission is restated beside the manifest
AND in doc-site.md section 2, and the prose there counted them ("Neither
is referenced...") so it went stale at three.

This strays into scripts/, which is Wave N's ground. Flagged in the
hand-back; it is a one-line data entry either way if the orchestrator
would rather publish it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 11:12:00 -07:00
Edward Langley 3d0850c132 Merge main (d7bfcbb, PR #96) into wave/operandless-commands
Two textual conflicts, and one the merge resolved cleanly and wrongly.

effect.rs: #96 extracted the LayoutGeometry list into
layout_geometry_cases() shared by two tests, one of which compares it
against the wire vocabulary; my side had added ViewTarget arguments and
carried the base's assert_eq!(cases.len(), 13). Kept #96's structure with
my arguments and dropped the count -- the list-versus-vocabulary check is
strictly stronger and a count beside it would be a second answer to the
same question. Verified the count came from the merge base, not from my
side, before dropping it.

test-evidence/command.md: both sides appended entries at the same point.
Kept both.

app.rs auto-merged CLEANLY and did not compile: improvise-4dpj's new
a_batch_that_leaves_records_settles_out_of_records_normal constructs
effect::SetAxis, which now carries a view operand. Fixed with
ViewTarget::Active -- the host's own key path, which is what that test is.

Checked rather than assumed, per the brief:

- Test names diffed BOTH ways against the union of the two parents, not
  the count. Union 1510, merged 1509, exactly one name absent and it is
  the intended one: a_hosts_none_cats_leak_into_another_views_formula_columns,
  the ac2z characterization test my own branch replaced with
  a_formulas_columns_survive_a_host_that_aggregates_differently. Nothing
  else lost, nothing unexpected gained.
- #96's the_enumeration_names_every_variant_the_derive_declares is still
  discriminating against the reshaped ModelCommand variants: dropping
  TransposeAxes from every_model_command() fails it by name.
- The test-support feature stays off in shipped builds: cargo tree
  --edges normal --target wasm32-unknown-unknown finds no serde_json.

And the finding the brief asked for, which is the opposite of a
disagreement: #96's switching_views_across_records_ness_settles_against_
the_view_switched_to is the HOST-side twin of the client fix in this
branch. Same batch, same crossing in both directions, and independently
the same phrase in its message. Neither is redundant -- nothing in the
projection stream carries the host's ViewState, so a host-side guard
cannot see a client's mode or the reverse. Cross-referenced in
context/test-evidence/clients.md so neither is deleted as a duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 11:08:17 -07:00
Edward Langley 26456c698e kb: editing a shell script corrupts the instances already running it
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
bash reads a script incrementally rather than loading it whole, so editing a
file while an instance executes it shifts the byte offsets underneath that
instance. Two background pr-reviews watchers, started ten minutes earlier and
sleeping in their poll loops, both died with 'syntax error near unexpected
token do' the moment I appended a paragraph to the usage text. bash -n
confirmed the file itself was fine.

The failure is in the RUNNING instance, not the file, so it is invisible to
every check you would think to run afterwards. And worse than a crash here:
each watcher exited having printed only the EARLIER review, so the wait it
existed to perform silently did not happen. I re-ran both cleanly before
merging rather than counting them.

Closes improvise-hsoe, improvise-4dpj and improvise-26dm from PR #96.
Backlog: 32 open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 11:04:36 -07:00
Edward Langley d7bfcbbd93 Merge gh/main (PR #96) into orchestrator main 2026-09-21 11:04:03 -07:00
Ed Langley f1d98534f4 Merge pull request #96 from fiddlerwoaroof/wave/gate-and-coverage
Three checks that could not fail: duplicate evidence headings, the records-mode settle, the LayoutGeometry case list
2026-09-21 11:03:45 -07:00
Edward Langley c73df4b5a1 kb: a pipe discards the exit code you meant to read
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Fourth occurrence in one campaign, and this one was in a tool I wrote TO BE
the reliable answer. scripts/pr-reviews --wait correctly exited 1 on a
timeout, and my invocation ended in '| tail -20; echo', so the harness
reported 0 — a review window that had EXPIRED read as a review that had
ARRIVED, which is the dangerous direction.

The earlier three: pr-watch reading tr's status through a pipe; a probe
reporting EXIT=0 from a checker that had exited 1; and PIPESTATUS in zsh,
where the array is lowercase pipestatus — the two shells differ, which is its
own trap.

The script's usage now says not to pipe it.

Closes improvise-711n (,.2f yielded zero decimals — the spelling our own docs
show) and improvise-x1ns (the preflight undercount ChatGPT Codex found on a
PR I had already merged unread).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:56:36 -07:00
Edward Langley 60bb3c56f1 Merge gh/main (PR #97) into orchestrator main 2026-09-21 10:56:33 -07:00
Ed Langley f5df19553e Merge pull request #97 from fiddlerwoaroof/wave/format-and-fit
Wave M: number-format precision (711n), preflight under-count (x1ns), e8no declined
2026-09-21 10:55:57 -07:00
Edward Langley 0466da173d kb: an unreachability claim is only as wide as the routes enumerated
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Three from Wave L's Codex round. The first is the campaign's cleanest
statement of a recurring failure.

PR #98 predicted its own composition-time mode drift and declared it
unreachable because the toggle route (R) is a stranding decline in the
browser. True of that route. A VIEW SWITCH reached the same drift, because
records-ness is auto-detected from the axes so a switch changes it with
nothing in the batch saying 'records'. The enumeration of routes is the part
nothing checks, so an unreachability argument is evidence about the routes you
thought of and nothing else.

The remedy is the thing worth copying: close the CLASS by observation rather
than patch the route. resolve::changed_mode is deleted; ClientState::fold
settles against the grid it holds, so no enumeration of composition sites is
load-bearing. Read-time derivation was rejected for having the same window and
for moving the enumeration to readers, where a missed reader is silent. Same
shape the repo already prefers: cursor_epoch over matching on CycleAxis,
document_generation over matching on LoadModel.

And a test-construction trap found by the author in its own bidirectional
test: a precondition computed from the same tuple element as the assertion
cannot be relaxed one iteration at a time, so the test fails for the wrong
reason — which reads exactly like the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:49:35 -07:00
Edward Langley 81fe546594 Settle the mode by observing the grid, not at each composition site
ChatGPT Codex's P1 on #98: switch_view_at composes
[SwitchView(target), ChangeMode(Normal)], and changed_mode settled that
against the grid the client STILL HOLDS -- the outgoing view's. Records ->
pivot emitted RecordsNormal and pivot -> records emitted Normal, which is
the cross-view drift this PR exists to close, by a second door.

The hand-back predicted this as follow-up 3 and called it unreachable
because R is a stranding decline in the browser. That is right about the
TOGGLE route and wrong about this one: records-ness is auto-detected from
the axes (gotcha 1), so a view switch changes it with nothing in the batch
saying "records". One door was enumerated and there were two.

So the remedy is general rather than a patch to switch_view_at.
changed_mode is DELETED; every site emits the plain AppMode::Normal; and
ClientState::fold settles after every fold against the grid it HOLDS.
Route-independent, so no enumeration of composition sites is load-bearing
any more -- the same shape the repo already prefers in cursor_epoch over
matching on CycleAxis, document_generation over matching on LoadModel, and
the dirty flag over a table of command variants.

improvise_protocol::settled_mode is the one rule and all three ends call
it: ClientState::fold, ClientSession::fold (the server's mirror, so the two
cannot diverge in a field nobody currently reads today) and
App::settle_normal_mode. App::normal_mode is gone -- it was that
function's host-side duplicate, and my change orphaned it.

Bidirectional coverage drives a real Host: a session connected to a
records-mode host, the pair folded locally and sent as a
ClientFrame::Commands, every reply delivered in arrival order. The
composed ChangeMode carries the STALE answer deliberately, because
composing the friendly value would let the records -> pivot direction pass
under the fault. Both directions fail with settle_mode no-opped, each
proved separately since the loop stops at the first.

Evidence, the injections and why the first fix's unreachability argument
was too narrow: context/test-evidence/clients.md, context/code/protocol.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:44:17 -07:00
Edward Langley e7c63e9530 kb+docs: strengthen the check, do not settle for correcting the prose
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Three facts from Wave N's Codex round.

The first is the one that generalises. improvise-26dm's round 1 found its OWN
defect — the right-hand side of a completeness diff was a second hand-written
list, not the derived enumeration its prose claimed — and shipped an honest
correction of the prose. Codex asked for the fix instead and was right: the
disclosure was accurate and still the weaker artefact, because the fix was
four lines of plumbing. Correcting prose is the fallback, not the deliverable.

Two traps, both of which caught someone:

cfg(test) and pub(crate) are different walls, and only one is a visibility
keyword. The follow-up said 'pub(crate) -> pub' — filed by the agent that
wrote it and repeated by me when I authorised 'one token' — because both of us
read the item line and not the attribute above it.

And cargo tree shows DEV edges by default, so asking whether a new feature
leaked serde_json into the wasm bundle reports it PRESENT. --edges normal
reports 0. I verified both readings myself. Same class as the difftastic grep
trap: a command that looks like it answers the question, answering a
neighbouring one, in the direction that reads as a finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:44:05 -07:00
Edward Langley 23c81977eb kb: an invariant generalised from a fix generalises the fix
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Three facts from Wave M's Codex round, and the middle one is about the kind of
mistake that is hard to see from inside.

Having repaired the categorical under-count, the agent proposed a
never-under-counts invariant WHILE THE TREE ALREADY HELD A COUNTER-EXAMPLE —
the missing Date arm, which Codex then found. An invariant written immediately
after a fix tends to state what the fix established rather than what the code
guarantees, because the evidence in front of you is the defect you just
closed. The question that would have caught it is about the OTHER function:
'what else does the thing I approximate do that I have not mirrored?'

The agent retracted it unprompted, before it was ever recorded.

Its structural fix is the right shape: encoded_width now matches Forced with
NO WILDCARD, so a fourth variant is a compile error rather than a silent
categorical fallback — which is exactly how Date was missed.

And the reason both bugs were invisible to a green suite: an advisory bound
over another function is a duplicated derivation, and nothing compared the two
at all. A width asserted against a constant passes at every stage while the
approximation drifts. The only honest test is to run the thing it approximates
and compare against what it actually produced.

Noted while filing: scripts/kb refused the first draft with dangling_citation,
because the doctrine named an invariant that was retracted and never recorded.
The same class as a dead ticket id, caught by the tool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:41:14 -07:00
Edward Langley 67905d9440 scripts: pr-reviews, because twenty PRs merged with their reviews unread
Prints a PR's reviews and says whether each targets the CURRENT head or an
earlier one, because that changes what you do: a finding against an old commit
may be fixed or still live, and both have happened this week.

--wait blocks until a review lands against the head. Observed latency is ten to
fifteen minutes after a push, so a merge four minutes later races the review
that was about to arrive. When it times out that is a decision to merge
unreviewed, not a clean bill.

The detection rule is placement, not authorship: Codex posts under the owner's
account, the same one our agents use, and ENDS with its attribution where ours
OPEN with theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:40:38 -07:00
Edward Langley 49c6af1978 Merge main (95657dc) into wave/operandless-commands 2026-09-21 10:38:50 -07:00
Edward Langley dedf9641cd Merge gh/main (95657dc) into wave/operandless-commands 2026-09-21 10:38:35 -07:00
Edward Langley 498582deb1 Bound every_model_command() with the derive (improvise-26dm, Codex P1)
The right-hand side of the LayoutGeometry completeness diff was a second
hand-written list. Both this branch's own review and ChatGPT Codex found it, and
the previous commit corrected the PROSE; Codex asked for the FIX. This is it.

the_enumeration_names_every_variant_the_derive_declares diffs
every_model_command() against variants::declared::<ModelCommand>() -- serde's
derive-generated variant list, improvise-5ubs's mechanism. The chain is now
derive -> every_model_command() -> model_effects -> case list, every link
checked, which is what the ticket asked for.

Injection is Codex's own, run end to end: a ModelCommand::ProbeGeometry whose
effect declares LayoutGeometry, with the model_effects and variant_name arms the
compiler forces, omitted from BOTH lists. Before: the whole workspace green.
After: staged, each stage naming the probe --
  every_model_command() never constructs ["ProbeGeometry"]
then, with the enumeration repaired and only the case list short,
  the_layout_geometry_cases_are_every_wire_effect_of_that_kind
  right: [... "ProbeGeometry" ...]
The staging is the point: the two tests answer different questions.

SCOPE OVERRUN, declared. `variants` was `#[cfg(test)] pub(crate)`, not merely
`pub(crate)`, and `declared()` needs serde_json, which improvise-protocol
carries only as a dev-dependency -- so the authorised one-token change could not
work. What it took instead, all plumbing, no behaviour change:
  improvise-protocol/Cargo.toml  serde_json optional + a `test-support` feature
  improvise-protocol/src/lib.rs  #[cfg(any(test, feature = "test-support"))] pub
  improvise-command/Cargo.toml   dev-dep enables that feature
The feature is OFF in every shipped build: `cargo tree --edges normal
--target wasm32-unknown-unknown` shows no serde_json, and all eight wasm-gated
crates still check clean. Drop this commit if the crossing is unwanted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:37:46 -07:00
Edward Langley c2bc939608 improvise-x1ns (2nd): the preflight counts a date measure's parts
P1 from ChatGPT Codex on #97, live at f128087 — in the change that fixed the
first instance of this same defect.

`preprocess` infers Numeric -> Date -> Categorical, and `encoded_width` had no
date arm at all. A date string does not parse as f64, so a measure column holding
dates fell through to the categorical count. `build_date` emits
epoch_day/year/month/day/dow, and seven parts once any cell carries a time, so a
column with ONE distinct timestamp promised 1 and materialised 5. Against a
budget of 64 that is an under-count of four on a single column, in the direction
that spends the allocation the preflight exists to prevent.

The fix is structural rather than another remembered branch, because two
instances one branch apart is the tell. `inferred_kind` reproduces `preprocess`'s
own match arm for arm and returns a `Forced`; `encoded_width` matches that with
NO wildcard, so a fourth variant is a compile error here instead of silently
taking the categorical arm — which is exactly how Date was missed. The .missing
flag moves out of the arms, since every branch owes it on the same terms.

The fixture carries a time case because of the RIVAL FIX, not the bug: "a date
column is 5" and "5, or 7 when any cell has a time" both repair the plain-date
failure and differ only on a column containing a time. Injected, `date_parts`
hard-coded to 5, the plain-date case still PASSES and the time case fails at
"promised 8 ... materialised 10". Injecting the bug proves the test sees the bug;
injecting the rival proves it sees the rule. Both arms compare against
`data.feature_names.len()`, never against a width computed a second way.

Also recorded in gp.md: the "must never under-count" line was written as a
proposed kb.pl invariant while the tree already held this counter-example. An
invariant generalised from the defect just fixed generalises the fix, not the
code; the question that would have caught it is "what else does `preprocess` do
that I have not mirrored", which is about the other function and not the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:35:55 -07:00
Edward Langley 95657dc90b kb: inject the rival fix, and a preflight inherits every branch it guards
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Both from the first use of the read-the-reviews rule, which found a P1 Codex
review on each open PR.

The preflight one is the sharper. encoded_width guards preprocess, which
dispatches Numeric -> Date -> Categorical, and it mirrored two of three.
improvise-x1ns was the categorical arm undercounting the (missing) one-hot;
Codex then found the DATE arm missing entirely — a date column takes the
categorical fallback because '2024-01-15' does not parse as f64, so a column
holding one distinct date promises 1 where build_date materialises 5, or 7
with a time. Two findings, one shape, one inference branch apart.

A preflight is a second implementation of the thing it guards, so it inherits
every branch of it, and the branch nobody mirrored is the one nobody was
thinking about. Note the second finding contradicts an invariant proposed in
the same hand-back that fixed the first: the counter-example was already in
the tree.

And the Wave M agent's own best insight: check_the_checker proves a test sees
the BUG, not that it encodes the RULE. Two implementations both repair ',.2f'
and differ only on '.2f3', so the discriminating injection is the other
plausible fix rather than the original fault.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:34:12 -07:00
Ed Langley 730f39a514 Merge pull request #95 from fiddlerwoaroof/docs/read-the-reviews
docs: read the reviews before merging; authorship will not identify them
2026-09-21 10:33:57 -07:00
Edward Langley 7c72f60713 Merge branch 'main' into wave/gate-and-coverage 2026-09-21 10:33:47 -07:00
Edward Langley 267769a497 Merge commit 'ea206bf' into wave/format-and-fit 2026-09-21 10:33:45 -07:00
Edward Langley ab6bb41da3 Review of this branch: two checks that could not fail, one drift left live
changed_mode settled only Normal -> RecordsNormal, but the host's
settle_normal_mode settles BOTH members of the pair, so a RecordsNormal
over an ordinary grid stayed put on the client and became Normal on the
host. Esc out of RecordsEditing resolves through entered() to
`enter-mode records-normal`, so the mirror-image drift was reachable by
the same route the fixed one was. Now total, with the pass-through arm
and both negative rows asserted.

a_compacted_seed_folds_to_the_same_cache_as_the_whole_history left
GridCache::records at its default, so a seed() that OMITTED the arm
compacted to an equal cache and the check could not see the omission.
The history now sets it; deleting the arm from seed() fails the test.

grid_updates' records arm had no test at all: grid_of setting the field
is covered by lockstep, but a client that connects before records mode is
entered learns only through the diff.

Evidence for all three, with the injected faults:
context/test-evidence/{clients,command}.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:32:54 -07:00
Edward Langley f6fca7ec98 A formula is evaluated for the view being projected (improvise-ac2z)
rebuild_layout recomputes formulas for the HOST's none_cats, and grid_in's
elsewhere path built another view's GridLayout against that cache. The
cache is keyed by the coordinates the recompute used, so a Total keyed
without a Product coordinate is not found by a view with Product on
Column, and prune_empty drops the formula's columns entirely. Absent
columns, not wrong numbers.

grid_in::rederived_for clones the model and recomputes for the target
view's none_cats. Three conditions gate the clone -- another view,
different none_cats, at least one formula -- so a client on the host's own
view pays nothing and a formula-free document pays nothing ever.

This is an interim and the cost is why: a whole-model clone plus a
fixed-point recompute per projection on exactly the broken path. The
ticket's first candidate, keying formula_cache by none_cats so one Model
holds more than one view's derived values, is the durable answer and is an
improvise-core change outside this wave's ground.

The characterization test is replaced by the positive one, and the
none_cats carve-out is dropped from
a_clients_grid_does_not_depend_on_which_view_its_host_is_on -- which is
the general property this is an instance of, and which fails under the
same injection.

Evidence: context/test-evidence/command.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:30:15 -07:00
Edward Langley 618801701e The browser asks the grid what kind of grid it is (improvise-e5pf)
One substitution at three decision points: wherever the host asks
layout.is_records_mode(), resolve.rs asked state.view.mode.is_records().
The two agree only while the mode and the layout agree, and
Command::ChangeMode(AppMode::Normal) is the message that separates them --
the host settles Normal against the layout the batch ends on, reduce_view
folds the variant it was handed.

GridCache::records carries the fact, projected by grid_of off
layout.is_records_mode() and diffed like the extent. Deriving it on the
client from row_frame.cats would have been a second implementation of
is_records_mode (improvise-edp's disease); deriving it from the mode is
the bug. Like Extent it frames nothing, so apply does not re-fit and its
batch position is not load-bearing.

The client's settling has one home, resolve::changed_mode, reached by
every site that can emit AppMode::Normal -- including entered(), which is
how a bound `enter-mode normal` gets there. The three decision points ask
state.grid.records, so a commit at a records cell declines rather than
composing typed_cell_command against a grid whose columns are
[coords.., +cat, measures.., +].

lockstep now DRIVES the keys: in_lockstep presses on both ends and
refreshes the client from a real projection rather than from the host's
slice, which is what let the old records coverage start from agreement.
The reachable path is Tab (category panel) then Esc; c/f/v are Ignored in
RecordsNormal and R is a stranding decline, which a first draft of the
test found the hard way.

Two resolve.rs fixtures put a records mode over a non-records grid -- a
state no host can produce -- and had to be corrected with the fix.

Evidence and both injected faults: context/test-evidence/clients.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:28:34 -07:00
Edward Langley 7bc26f80d5 Review round 1: three checks that did not check what they named
All three found by the adversarial review of 019cbc1, each by injection.

1. every_model_command() is NOT compiler-maintained, and both this PR's prose
   and reduce.rs's own doc comment said it was. variant_name's exhaustive match
   forces an arm in variant_name, not an entry in the value list -- deleting
   ModelCommand::TogglePruneEmpty from it failed the new completeness check and
   NOTHING ELSE in the workspace. So the diff is two independent hand-written
   lists: strictly better than one list against its own length, and not the
   derived enumeration improvise-5ubs got for the two enums. Doc comment
   corrected; the prose in context/code/command.md and the evidence entry now
   say what it is. Closing it properly wants improvise_protocol::variants made
   pub so declared::<ModelCommand>() can bound the list -- another wave's crate,
   reported rather than taken.

   The assertion message was also ONE-DIRECTIONAL ("a name only on the right
   ..."), which is the shape that injection produces and which invites the wrong
   repair: delete the case on the left, degrading both sides together. It now
   names both directions and says which is which.

2. SELF_TEST_DUPLICATES case 3 was named for the ambiguity guard and never
   reached it: both its headings were module-qualified, so canonical() returned
   on the exact-match arm before touching by_leaf. Dropping the guard
   (`if matches` for `if len(matches) == 1`) left --self-test at exit 0. The
   fixture is now one BARE heading over an ambiguous leaf plus BOTH qualified
   spellings, so the injection collides with one of them whichever way
   next(iter(set)) falls -- deterministic across five runs, verified.

3. The evidence entry said five pre-existing records-mode tests fail under the
   settle-before-rebuild injection; measured, it is six. And "three hundred
   lines above" is four hundred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:27:24 -07:00
Edward Langley f128087999 review: correct six prose claims, four of them false
An adversarial review of this branch found that the prose was worse than the
diff — including, twice, in the exact genre the branch was congratulating itself
for catching. Recorded rather than quietly rewritten, because that is the shape.

Must-fix, all verified before correcting:

- `fit.rs`'s comment on the exact check still read "this is the check that
  enforces the bound in both directions of a drift" — verbatim the sentence
  gp.md §14 now spends 25 lines refuting. Gotcha 48's third clause exactly: a
  fact stated twice is changed by grep, not by edit. I refuted the copy in the
  prose and left the copy in the code.
- "false for eight months" was fabricated. `encoded_width` landed 2026-09-19,
  the sentence 2026-09-20; the repository itself is six months old. A paragraph
  whose point is that unchecked cost claims are checkable closed on one.
- "NumericCategory was the one artefact family with no coverage" is false: it
  was one of three, and `ValueOfAMeasure` and `AbsentCoordinate` still have
  none. Corrected in both files that said it, and the remaining gap is named.

The decline's stated reason was wrong, which matters most because a decline is
the artefact nothing checks. It claimed one-hot is the only encoding of a
category whose splits are spellable; `is_expr` is a `primary`, so an ordinal
split IS spellable as a weighted indicator sum. Verified by running
`parse_formula`, not by re-reading the grammar, and now pinned by
`an_ordinal_split_on_a_category_is_spellable_as_an_indicator_sum` so the
paragraph cannot go quietly wrong again. e8no stays declined on grounds that
survive: a category's items carry insertion order rather than magnitude, and the
emitted formula spells one IS per item either way, so the width is not saved.

Also weakened two overclaims — "marking it a Label yields the same fit" (false:
the feature count moves the RNG draw sequence, gotcha 37b) and "asserted rather
than argued" (the dichotomy is argued from spell()'s exhaustive match; the test
asserts one instance) — narrowed "the only coordinate predicate is is_expr" to
what is true of it, corrected an uncheckable test count, named which two of the
three consumers of parse_number_format are actually guarded, and moved an
evidence block that had been inserted under the wrong module heading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:26:27 -07:00
Edward Langley 1dd6950ed3 A view-scoped ModelCommand names its view (improvise-lltt)
Nine ModelCommand variants configure a view and named it implicitly: the
effect read App::view_mut(), the HOST's slice. Since improvise-jytm made
ViewState::active_view authoritative per client, a client on another view
moved the host's view instead of its own -- its grid did not change and
every other client's did.

Each of the nine now carries `view: String`; model_effects maps it to
effect::ViewTarget::Named, and the nine effects carry a ViewTarget rather
than reading an ambient one. There is no operandless form left:
App::view_mut() is deleted and replaced by view_mut_of(&ViewTarget), so a
future geometry effect has to say which view it writes. The host's key
path passes ViewTarget::Active, which is right -- the TUI is the host and
has no session to ask. A Named view the document has not got is a no-op
rather than resolve_view's fallback onto the default.

Second half, which the ticket did not predict: naming the view made a
reframe of a view the host is NOT on reachable, and adopt_cursor_epoch
could not see one -- cursor_epoch is bumped only by the host's own
reset_cursor, so those clients were reset by nobody and told nothing
(gotcha 24's strictly-worse-than-nothing state). App::last_reframed_elsewhere
is the second trigger; its opening frames are taken lazily inside
view_mut_of, the one door such a write goes through, so the held-key path
still hashes one view and improvise-mcq4's declined cost is not paid.

Evidence and the three injected faults: context/test-evidence/command.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:23:27 -07:00
Edward Langley 019cbc1f72 Check the LayoutGeometry case list for completeness (improvise-26dm)
assert_eq!(cases.len(), 13) catches a case being DELETED from the list and
never one being ABSENT from it, and the two are written identically as an
assert_eq over a number. The adversarial review of PR #78 added a fourteenth
pub struct declaring LayoutGeometry, pointed at by nothing, and the suite
stayed green. doctrine(a_list_checked_for_length_reads_like_a_list_checked_
for_completeness).

improvise-5ubs's mechanism cannot reach this one -- an effect is a struct
implementing a trait, so serde has no derived enumeration to diff against. The
WIRE vocabulary does have one, and model_effects maps each ModelCommand variant
back to the effect it came from (gotcha 38), so the list is now diffed against
the LayoutGeometry effects reachable through ModelCommand. The two sets are
equal today, which is the evidence the substitution lost nothing. Injection:
drop TogglePruneEmpty -- the old count said 12 != 13, this names the effect.

reduce.rs's test module, every_model_command and effect_type_name become
pub(crate) so the enumeration and the name reader have ONE home rather than a
second copy in effect.rs.

Residual, stated in the evidence rather than closed: total over the wire, not
over the effects. An effect no ModelCommand names is still invisible -- exactly
PR #78's probe. A second [...; 13] array survives one function away in
every_layout_geometry_effect_is_visible_to_the_narrow_fingerprint; filed, not
smuggled in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 10:19:20 -07:00