Commit Graph

978 Commits

Author SHA1 Message Date
Edward Langley 3fcb88b291 docs: the landing page on a phone, and two stale instructions in the plan
Review findings on PR #47.

A 120-column cast shrunk into a 390px viewport computed a **4.83px** terminal
font — measured in Chromium against the assembled tree. That was tolerable when
the landing page sat at `/landing/`; since the user's ruling it is the first
thing a phone visitor sees. Under 900px each player now scrolls sideways at a
legible size instead: **11.95px**, measured the same way, with the page's own
`scrollWidth` still equal to its `clientWidth` — the overflow is contained
inside the player, so there is no horizontal PAGE scroll.

That takes `docs/index.html` to 209 lines, past improvise-e61's "under 200".
The criterion is reinterpreted rather than crammed to fit, and said so on the
ticket: the number was a proxy for single file, vanilla, no build step, no
framework, all of which hold. A limit met by one line and checked by nothing was
never really met.

`context/plan.md` carried two instructions that this PR made actively
misleading:

- §2.4 still told a re-recorder to use 100x30. That is the instruction whoever
  picks up improvise-2oxt would follow, and it disagrees with both the existing
  casts and the player. It now says 120x37, and says the two must be changed
  together.
- §4.2 still said to set Pages to branch `main` / folder `/docs`. That used to
  fail loudly — there was no `docs/index.html`, so it was a 404 — and since
  improvise-e61 it fails QUIETLY: the landing page renders perfectly and its
  call to action 404s, because `app/` exists only in the assembled tree. It now
  says GitHub Actions, and says why the other setting is worse than it looks.

Verified: 6/6 harness shell specs still pass against the assembled tree, the
four casts still play 37 lines each at the root, and the call to action still
resolves 200. cargo test 1311, both comment checkers clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:20:59 -07:00
Edward Langley 63f9357c1e refactor(protocol): one keymap static, and the help tab bar costs nothing closed
Self-review of the branch, before opening the PR.

improvise-protocol had built `default_keymaps()` twice — `resolve.rs` and
`help.rs` each held their own `OnceLock`. `Keymaps::shared()` is the one
table; a second static is a second thing to reason about even when the two
agree by construction.

And `render_model` built all five help pages on **every frame** to fill
`help_titles`, which the page draws only while the overlay is open. Each page
resolves its key rows against the keymap, so that was some thousands of hash
lookups per repaint for a list nobody was looking at. `help_titles` is now
gated the same way `help` is: empty means closed.

No behaviour change. 1321 tests pass; the 7 Chromium shell tests still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:20:12 -07:00
Edward Langley 96adc66030 fix: the attribute handling was still a line scanner, and carried two holes
Adversarial review of the previous commit: scan() survived 16 literal-shape
attacks, and both HIGH findings were in the code that did NOT get rewritten.
The attribute handling still asked the RAW LINE the questions this commit's
whole argument says a raw line cannot answer.

- Bracket depth was counted over the raw line, so `#[cfg(feature = "[")]`
  set attr_depth=1 with no reset path and exempted EVERY REMAINING LINE IN THE
  FILE. The raw-string spelling of it is the exact defect class this rewrite
  was for: scan() knew those bytes were string content and line 150 asked the
  line instead. The tree has 14 `#[arg(...)]` and 3 `#[command(...)]` clap
  attributes whose help strings are free text.
- `s.startswith("#[doc")` is an UNANCHORED PREFIX, so `#[doc(hidden)]` counted
  as a comment and flagged correct code. Fourth instance of that class in this
  campaign, eleven lines above the comment naming the previous three.

Also from the same review: `#![doc = "..."]` is the inner spelling of a doc
comment and was never counted (unlimited free crate doc); an inner attribute
between two `//!` blocks split one crate doc in half, where the rule exists to
separate a crate doc from an ITEM doc — so it now ends a run only where the doc
KIND changes; and a block comment closing on a line with code got one free line.

Attributes are now asked of the scanner: `scan()` returns the code-only text,
and depth is counted over that. `_is_attribute` is deleted — dead since the
logic moved inline, and an orphaned explanation is what --check-evidence exists
to catch one directory over.

The coverage finding was the important one. Two of scan()'s four literal
mechanisms had NO self-test case: reverting char-literal or plain-string
handling left the suite green. Both are load-bearing in opposite directions —
without char literals, `'"'` opens a string that eats the file (false negative);
without plain strings, a multi-line string holding `//` lines is a false
positive, the CI-breaking direction. Both now have cases, and so does every
shape above. The attribute-depth mechanism needed a fixture with code AFTER the
attribute, because comments inside one are counted either way: the first
fixture I wrote passed under its own injection.

Self-test: 17 violations caught, 9 legitimate shapes allowed. Every mechanism
proved discriminating by reverting it alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:19:24 -07:00
Edward Langley 233a5cc635 fix(core): one resolution of a records cell, so the producer stops guessing
improvise-tom1. The user has ruled that a remote client must NOT see another
client's staged drill edits, so the invisibility is correct and was never the
defect. What remained was that three places answered "what text is in this
cell" — and only two of them still do.

GridLayout::records_value is now the resolution: it returns the CellValue, and
records_display is one to_string over it, so the text can never disagree with
the value it spells. project::cell_display used to call records_display and
then GUESS numeric-ness with text.parse::<f64>() — the one place the producer
guessed rather than knew, and it was wrong: a records coordinate column holds
an item NAME, so a Year category with items 2024/2025 projected a coordinate
the browser right-aligns as a number. Nothing in the suite could see it,
because every records fixture in the tree names its items Region/East/North.

A correction to the ticket worth more than the code: src/ui/grid.rs is NOT a
third answer. Since improvise-anr9 the TUI grid takes its text off GridCache
and then overlays drill_state.pending_edits — one derivation plus a
session-local overlay, which is exactly what the ruling wants, and
App::display_value_of does the same for the editor. The ticket was written
before anr9 landed.

Both halves of the ruling are now asserted, with the leak injected as the
fault: teaching the producer to prefer pending_edits, which is the plausible
"fix" the ticket exists to forbid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:19:00 -07:00
Edward Langley fb41255c49 docs: quote the benchmark as ranges over two runs, not one run's numbers
A second run of benches/grid_projection put the 'large' row's col_widths share at
45% where the first said 64%. 45-46% is what reproduces; 64% was noise, and a
single-run number written down as a fact is the same defect as an argument
written down as a measurement -- which is what the benchmark existed to avoid.

What does not move between runs is what the decision rests on: ratio stays
1.2-1.5x and col_widths stays at roughly half the frame. improvise-jmjz's title
and notes corrected to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:18:28 -07:00
Edward Langley 414d02c4a6 feat(protocol): one home for UI strings (improvise-x7sw, ktv1, k099)
Three tickets asked one question — where do UI strings live relative to a
serde wire enum — and answering it three times is the disease this unit exists
to cure. One answer, in `improvise-protocol/src/help.rs`:

**A UI string is keyed by a command name, never by a key, and never hangs off
the enum that crosses the wire.** `AppMode::label`/`hint_text` are gone;
`help::mode_label`/`help::hint_line` replace them, so the enum a client
deserializes carries no English at all (improvise-k099). **And wherever a
string names a key, the key is looked up rather than spelled**:
`Hint { keys, command, caption }` takes `keys` off `Keymaps::default_keymaps()`,
`command` is the semantic value a localized front end keys off, and `caption`
is the default English — the `AxisColor`/`axis_fg` shape.

- **improvise-x7sw**: `help::which_key` is the which-key popup's rows, keys and
  captions together. The client already holds the keymaps, which is the answer
  the ticket preferred, so `ViewState::transient_prefix` stays an address.
- **improvise-ktv1**: the help pages are data — `help::pages()` — with every
  key row derived. `src/ui/help.rs` keeps its ratatui styles and loses ~310
  lines of content; `web/app.js` paints rows it does not author, and
  `harness/tests/shell.spec.js` asserts that `?` really draws them.

**What this deletes is the measure of it.**
`every_hint_key_resolves_in_its_mode` and its 60-line hint tokenizer are gone:
they existed to hold a hand-written string honest against the keymap, and the
string is derived now, so the property is true by construction
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
`Keymap::binding_hints` goes with them — `which_key` is the one walker, and the
binding dump reads it. What replaced the test checks the *other* direction, a
mode advertising a command nothing binds, which was never checkable before.

Two defects the derivation exposed, both fixed here: `key_label` dropped
modifiers, so Ctrl-arrows read as bare arrows, and `Char(' ')` printed a literal
space instead of `Space`.

**What is deliberately not derived**, and says so: the import wizard's keys
(its whole keymap is one `Any` binding into `HandleWizardKey`, which decides
per step inside the command — a real hole, left visible), `gg`/`yy`/`ZZ` and
the ex-commands (`HelpLine::Literal`), and the captions, which no test can
judge. `context/code/protocol.md` §8 has the argument.

Cost, stated: the hints changed shape. They are longer, they name arrow keys
beside `hjkl`, and two sub-lists on the Panels page lost their visual indent.
That is the price of the keys being true.

Evidence: 1321 tests pass, and the 7 Chromium shell tests pass against the
assembled site. Two faults were injected into the help overlay — dropping the
`paintHelp` call, and removing the `mode != Help` guard so it is never hidden —
and each fails the new spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:18:17 -07:00
Edward Langley 57a67e54cf refactor(protocol): delete GridUpdate::Invalidated; say why Items and Axes stay
improvise-n89z. Three Projection mutations had no producer. They get two
different answers, and the distinction is the finding.

GridUpdate::Invalidated is DELETED. It was not merely unused, it was
REDUNDANT: `grid_updates` diffs, so it always knows which cells moved and sends
Cells, and `GridCache::seed` compacts — there is no state in which a server
knows the contents are stale and not which ones, and if there were, the answer
is a fresh Snapshot through `rebase`. Meanwhile its stated meaning had migrated
into running code, because PR #23 gave `invalidate_relabelled` the job of
blanking exactly what a reframing invalidates, totals included. Three documents
still described it as load-bearing, which is worse than silence: a reader
reasoning about the fold's guarantees assumed something produced it.

CategoryTreeUpdate::Items and ::Axes STAY, with the argument written down.
They are derived rather than invented — the effects that move a category tree
come in three shapes, by the same recipe that built ModelCommand — and the
producer takes the coarse arm on purpose, because Dirty::of cannot say WHICH
category moved (an EffectKind never can), so emitting them needs the tree
observed and diffed for a widget that costs a few hundred bytes.

The two tests that reached the blanking through the retired arm now reach it
through a reframing, which is what really produces it; the row one is strictly
stronger, since blanking everything could not tell "blanks what it reframes"
from "blanks the lot". `the_retired_invalidated_spelling_does_not_parse` keeps
it from returning as a serde alias, with the injected-fault discipline
`the_retired_spellings_do_not_parse` earned: well-formed input first, error
KIND asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:16:29 -07:00
Edward Langley 72b0d09119 docs: finish the reduce_full deletion in the prose that outlived it
Self-review caught three references the deletion commit missed, which is gotcha
48's rule arriving on schedule: when a fact is stated in more than one place,
changing it is a grep, not an edit.

- context/code/command.md still had a `### reduce_full / reduce_key` section
  calling it "the signature the three hosts share" -- the exact claim the ticket
  was filed against, two hundred lines below the paragraph that corrects it.
- ADR 001 stated in the present tense that apply_command/reduce_full do not
  check, and recorded improvise-ondj giving both a Checked signature without
  saying that one of them has since gone. The ADR now says why deleting beat
  typing: having no caller was the condition that let the defect hide, and a
  Checked on a function nobody calls fixes the type and not that.
- The test-evidence entry's "no longer compiles" is now "no longer exists".

Also records the AddItem/AddItemInGroup success gating and the four effects that
reset unconditionally because they cannot report a miss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:14:28 -07:00
Edward Langley 69e04ff0b0 feat(protocol): name the records-mode creating columns instead of leaving them nameless
improvise-poia. `+` and `+cat` projected `None`, which the protocol defines as
"no projected cell", so a client could not tell a column that creates something
from a padded slot — gotcha 42's conflation, a third time.

The ticket offered two designs and both assumed a cell write. Neither happens:
add_measure emits exactly one effect, AddItem { _Measure, name }, and
add_category emits AddCategory(name) and deliberately writes no coordinate on
the current row (improvise-2zq). The record under the cursor is not part of the
operation and neither is any value — what the user types is a NAME. So there is
no distinct command to invent and no two-command flow to sequence.

What was missing was never an address but a recognition. CellId::Create carries
the kind and nothing else, because the kind plus the typed name is the whole
operand. It is not a SetCell target, so editable stays Record-only and
record_at_cursor still answers None — both by matching, which the compiler now
forces.

No client composes it yet: keys.rs::commit returns early for the whole of
records mode, and that guard is broader than its stated reason. Filed as
improvise-gcvb, with the two things that make it more than a one-liner — an
inserted +cat column means the TUI does not advance, and AddCategory can abort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:14:04 -07:00
Edward Langley 93f795291e fix: a refused add must not invalidate anyone's cursor
Self-review of improvise-9mo9's own commit. AddCategory was gated on success and
AddItem/AddItemInGroup were not: the reset sat after the `if let Err`, so a typo'd
category name bumped cursor_epoch and broadcast Command::ResetCursor to every
client for a change the document never made. The epoch is document-wide, so the
cost was paid by everyone.

SetNumberFormat's negative arm cannot see this -- it tests a command that succeeds
and reshapes nothing, where this is one that reshapes nothing because it was
refused. a_refused_model_command_invalidates_no_cursor is the second arm, proved
by moving the reset back outside the match and watching it fail on the bystander's
cursor.

Also drops apply_command's now-dead `Command::Project(_)` arm: since the fallback
stopped folding, both returned Dirty::NOTHING and the first was unreachable
duplication.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:13:35 -07:00
Edward Langley a6451363ca fix: replace the line scanner with a real one; three more bypasses were open
Adversarial review demonstrated that the previous fix closed one class and
left three, then reintroduced the bug this file's own history is about.

- A ``` fence in a PLAIN `//` comment earned the doctest exemption. rustdoc
  runs no test there ("running 0 tests"), so two lines exempted anything after
  them. The cheapest bypass yet, and `--list-doctests` in the same file already
  had it right: one of the two was wrong and it was this one.
- A block comment opened after code on the same line was invisible, since only
  a line STARTING with /* was seen. That is the most natural way to write one.
- Nested block comments closed at the first */ and the rest read as code.
- And the false positive: `in_block` had no reset path, so a /* inside a raw
  string swallowed the rest of the file. Latent — no .rs file starts a line
  with /* today, so the block fix had zero effect on this repo — but 8 files
  hold raw strings and this repo's habit is Rust fixtures inside them. The
  first such fixture breaks CI with a diagnosis pointing at a string.

A line-based scanner cannot decide any of these, so `scan()` now walks
characters, tracking nested block comments, strings, raw strings (with hashes)
and char literals. `#![...]` ends a run where `#[...]` stays transparent,
because a crate doc and an item doc separated by an inner attribute are two
blocks a reader does not meet as one. `#[doc = "..."]` counts: it is the one
attribute that is a comment.

The self-test carries all of it: 12 bypasses, 6 shapes that must stay legal.
Each new fix was proved discriminating by injecting it alone.

One of those injections caught a VACUOUS test of my own: with raw-string
handling disabled, a plain-string fallback reached the same verdict, so the
fixture proved nothing. It now contains r#"x"y, which a non-raw parse closes
early — so the fault is visible. That is the defect this repo is named for,
found in the test written to prevent it.

Closes improvise-mbmf, which PR #45 claimed to close and did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:13:00 -07:00
Edward Langley acc3eb3b00 docs(active-view): follow the Broadcast variant rename through the prose
`Broadcast::ToAsker` became `Broadcast::To(SessionId, _)` when the deleted-view
case needed a one-session audience that is not the asker; three doc references
still spelled the old name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:12:51 -07:00
Edward Langley 2ccd21f3e8 docs: the landing page is the front door, the app moves to /app/
User ruling on improvise-kh8, overturning the arrangement the previous commit
shipped: "The landing page should be served at the root of the repository and
link to the in-browser demo." The argument for the other arrangement is left
standing in the ticket rather than deleted — it is overruled, not wrong to have
considered — and the root is now `docs/index.html`.

The app moves under `app/` WHOLE, both bundle directories included. Every path
in `app.js` and `worker.js` is relative, so moving the bundle together keeps
every one of them correct and the wasm-pack generated glue untouched;
`worker.js`'s `fetch("demo.improv")` resolves against the worker's own URL, so
the seed follows for the same reason. The four `stamped` guards in
`assemble.sh` follow the files: they are the only check that the cache-bust
substitution worked, and a guard aimed at a path nothing writes fails in the
direction that looks like success.

`harness/tests/shell.spec.js` navigates to `/index.html` six times, including
the `?ws=` and `?doc=` variants, and every one of those now lands on the
landing page. All six are repointed at `/app/index.html`. That file is outside
this unit's declared scope and is the second deliberate stray in this PR.

VERIFIED AGAINST THE ASSEMBLED TREE, not against `docs/` directly, because a
path move is what re-arms the trap this page already caught once. With real
bundles from `web/build.sh`:

  * `npx playwright test tests/shell.spec.js --project=chromium` — 6 passed,
    with `/app/{index.html,app.js,style.css,worker.js,demo.improv}` and both
    `?v=` stamped bundles all served 200;
  * the four casts play 37 lines each at the site root, no failed request and
    no page error;
  * the full journey: load the root, click the call to action, land on `/app/`,
    and the grid draws out of the in-page worker — 12 cells, all editable,
    a "Revenue" header, `#status` reading "in-page worker · demo.improv".

The call-to-action check was proved able to fail by pointing the link at
`./aap/`: 404, while the page still looks entirely correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:12:43 -07:00
Edward Langley 3d8ea00872 docs: decline improvise-drg's Effect::apply narrowing, with counts
improvise-drg proposed narrowing each Effect::apply to the slice it touches. It
was filed with three unpicked mechanisms and a note to decide before claiming.
Declined, and the counts are the argument.

Over the 49 impl Effect blocks: 8 touch only view_state, 9 only model_state, 32
cross-cut. At this wave's base commit the split was 9 / 16 / 24, so the narrow
set fell from 25 to 17 in one wave -- and the mover was improvise-9mo9, earlier
on this branch, which made nine reshaping effects call app.reset_cursor(). That
is architectural, not incidental: a reshape lives in model_state.workbook and the
cursor it invalidates lives in ViewState, so the effects that matter most are the
ones that reconcile the two slices.

Three more grounds. The type-level documentation already exists and is checked --
Effect::kind() has no default and apply_effects reads that one answer three times
-- so a narrowed signature would be a second statement of the same fact, free to
disagree. Every offered mechanism weakens a guard: three traits need three queues
(destroying batch order, which gotchas 21/23/43 lean on) or an enum wrapper (a
fourth classification beside EffectKind), and default methods give `apply` a
default whose wrong answer is silent, which is exactly what kind()'s no-default
rule exists to prevent. And finer-grained effect tests are not blocked: the 64
tests in effect.rs already drive apply against a one-line App fixture.

Approach (B) -- rebinding the slice at the top of a body -- stays available as a
per-function style choice and is not mandated.

The bead carries the `human` label, so this is recorded as a recommendation with
its evidence rather than settled unilaterally.

Closes improvise-drg

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:11:32 -07:00
Edward Langley 2e89de220a feat(protocol): project the title bar, as a widget rather than a ViewState field
improvise-z754. draw_title read workbook.model.name, model_state.dirty and
model_state.file_path; none was projected, so a browser could not draw a title
bar at all.

A projection, not a ViewState field: these are facts about the DOCUMENT, shared
by every client and writable only by the server. On ViewState they would be
document state wearing a session slice's clothes, and reduce_view would need
commands nothing composes. The per-widget cache is the shape for "a local copy
of what a widget draws, replaced by a Projection".

It is the one widget no EffectKind can gate, and that is the whole design.
`dirty` is raised by any EffectKind::Model(_) and cleared by effect::Save,
which is ViewOnly and correctly so; SaveAs moves file_path and is also
ViewOnly. So Dirty::of reports NOTHING for exactly the effects that change this
widget most visibly. ClientSession::document_update therefore OBSERVES — build
the desired cache, diff it against the mirror — the same answer record_versions
got for the same reason.

`stored_as` is a name, never a path. The TUI already drew only file_name(), and
a socket server projecting file_path would hand a remote client the host's
directory layout. `None` carries the fact ClientFrame::Save fails on.

draw_title now draws from project::document_of, so the two front ends share
one derivation. web/app.js deliberately does NOT paint it yet — every harness
fixture mocks a FAILING peer, so a title element added now would be unrun
JavaScript, which gotcha 48 forbids. Filed as improvise-5jna, fixture first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:11:11 -07:00
Edward Langley 67b4a5f1c2 fix(active-view): move this client off a view it just deleted
The host-side half of `adopt_deleted_views`, and a desync that predates the
split: `Workbook::delete_view` moved the document's active view and nothing
moved `ViewState::active_view`, so the cursor was read under a key naming a view
that was gone. It was invisible while `draw_status` read the workbook's copy;
improvise-k69y makes it read the client's, which turns it into a wrong badge.

`ClientSession::view_in` also goes through `App::resolve_view` now, rather than
carrying its own copy of the fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:10:56 -07:00
Edward Langley 69fe27e1d6 fix(active-view): close the deleted-view hole and unify view resolution
Two findings from running the code-review skill on this branch.

`Workbook::delete_view` used to move the document's active view and
`adopt_active_view` pushed the result into every mirror, so deleting the view a
client was on moved that client. The split removed both, leaving that client
naming a view the document has not got — projected the default's grid while its
own slice says otherwise. `Sessions::adopt_deleted_views` answers it, observed
rather than predicted: the check is whether a mirror still names a view the
document has, not whether the command was a `DeleteView`.

That needs a one-session audience that is not the asker, so `Broadcast::ToAsker`
becomes `Broadcast::To(SessionId, Command)` and `Host::relay` reads the address
off the message rather than taking `from`.

And "what does this name name in this document" was answered twice — in
`App::view_name` and again inside `project::grid_in`. `App::resolve_view` is now
the one home both go through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:09:14 -07:00
Edward Langley 8a4ee06570 bench: measure grid_of's whole-grid cost, and decline the narrowing it was for
improvise-i5tk's acceptance criterion 1 was a benchmark, on the ground that "this
can dominate TUI rendering" is exactly as unevidenced as "it is fine".
benches/grid_projection.rs is that benchmark, and it says no.

One grid_of call, dense pivot grids, this machine:

  typical                  100 x  4 =   400 cells  whole-grid   1.902ms  ratio 1.2x  col_widths 45%
  large                   1200 x  8 =  9600 cells  whole-grid 185.644ms  ratio 1.3x  col_widths 64%
  18k cells (gotcha 41)   3000 x  6 = 18000 cells  whole-grid 402.435ms  ratio 1.5x  col_widths 42%
  pathological            9000 x 10 = 90000 cells  whole-grid   5.235s   ratio 1.5x  col_widths 44%

ratio is whole-grid over a 45x12 terminal window: 1.2-1.5x, against a fix whose
stated cost is changing what a cache index MEANS in the TUI and putting every
scroll site in scope. Wrong trade, so i5tk is closed on the measurement.

Why the ratio is so low is the bigger finding. Two of the three whole-grid passes
a frame performs are viewport-independent by construction:
ui/viewport.rs::compute_col_widths takes no viewport and walks every (row, col)
twice -- once for cell text, once for the column totals it must also size -- and a
column total must sum every row including scrolled-past ones (gotcha 44).
grid_of's own cell_display loop is the third pass and the only one windowing would
touch. So the whole grid is evaluated about three times per frame, every result
discarded, and at 18,000 cells a keypress costs four tenths of a second. Filed as
improvise-jmjz; its fix is caching or one fused pass, which this benchmark has now
distinguished from the windowing it rules out.

No criterion crate: plain Instant, and test = false so cargo test does not run a
target that takes seconds by design.

Closes improvise-i5tk

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:07:30 -07:00
Edward Langley 10cebfffd9 docs(active-view): record the split, its evidence, and the two gaps it opens
`context/code/command.md` gains the derivation for improvise-jytm: what moved,
what projects per client, why `effect::SwitchView` is `ViewOnly`, and why
`adopt_active_view`/`Broadcast::ToOthers` are deleted rather than left.

`context/test-evidence/command.md` and `clients.md` follow the tests that were
replaced, each entry naming the fault injected to prove the new check can fail.

Two gaps filed rather than smuggled in: improvise-lltt (a view-scoped
`ModelCommand` names no view) and improvise-azhb (`Initial View` no longer
follows the TUI user).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:06:18 -07:00
Edward Langley 8b3de934fc feat(protocol): project the page-axis item lists so [ and ] work in a browser
improvise-p1ym. GridCache::page_coords carried (category, selected item), so
PageNext/PagePrev resolved to a command name the browser held but could not
compose an argument for. They reached Host::handle, which answers an empty vec,
so a smoke test found them inert on the live page.

The item list is not a second field beside the selection: page_coords becomes
Vec<PageAxis>, and `items` is Some exactly when the axis is Axis::Page. That
puts gotcha 27's split in the type — a Filter axis constrains without being
navigable, so it carries no list and no caller can compose a cycle for one.
GridLayout::page_coords stays filtering_cats(), which is right for the bar,
and navigation.rs::page_cat_data stops spelling the Page-vs-Filter rule a
second time: it reads the layout's list.

The browser's page_cycle mirrors PageNext/PagePrev line for line, odometer
carry included, and `[`/`]` leave the still-needs-the-host list.

Two decisions stated in context/code/core.md. The list is
effective_item_names — the same call that already chose the default selection,
so selection and list come from one place; page_cat_data used
ordered_item_names before, which for a _Measure page axis let the default
selection be a formula target `[`/`]` could not reach. And the list is NOT
filtered to visible items: the TUI cycles hidden ones, and making the browser
differ would be the divergence rather than the fix.

The lockstep test's first version was vacuous — it rebuilt the client's cache
from the host and compared it against that same host. It now drives two
independent documents and compares View::page_selection; the injection that
reverses the odometer's significance now fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:05:54 -07:00
Edward Langley 0a9e342d2e ci: run the checker's self-test before the checks that depend on it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:05:32 -07:00
Edward Langley 3faf17b645 Merge branch 'fix/checker-block-and-fence-bypasses' into ci/gate-the-comment-cap 2026-09-20 11:05:24 -07:00
Edward Langley 53ffe9f193 fix: close three demonstrated bypasses of the comment cap, and self-test it
Adversarial review of PR #44 constructed all three, each compiling clean with
no warnings, each passing the checker.

Block comments were invisible: the scanner only ever tested startswith("//"),
so a six-line /* */ passed — and /** */ is a DOC comment that rustdoc renders
exactly as /// does, making `/**` the cheapest way to write the long argument
the rule exists to forbid. There are zero block comments in the tree today,
which is the only reason this was unexploited; #44 is the change that gives
anyone a reason to look.

Fence state was per-FILE, so one unclosed ``` exempted every later comment in
the file. A dangling doctest would fail the build in a lib target, but doctests
do not run for bins, tests/, examples/ or harness/ — all default scan roots.
Fence state now belongs to the comment block: any non-comment line clears it.

Attributes terminated a run, so `#[inline]` between two pairs of doc lines
split one argument of four into two legal runs of two. The module docstring
had always promised attributes are "ignored"; the code disagreed with it, and
disagreed in the direction that passes.

Closing the last of those found two real violations the old checker could not
see. crates/improvise-command/src/command/cmd/text_buffer.rs carried TWO doc
comments straddling #[derive(Debug)] — both attach to the struct, so rustdoc
rendered both; a leftover from the comment wave. transport.rs had a three-line
run split by #[allow].

A checker with ten silent successes and no test of its own is the thing it
warns about, so --self-test runs every bypass this file has been fixed for
plus three shapes that must stay legal. Proved discriminating by reverting the
attribute fix: it fails and names that case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:05:22 -07:00
Edward Langley 555d9f1ca7 feat(protocol): fifteen more bindings resolve on the client (improvise-e24v)
`cycle-panel-focus`; the category panel's `cycle-axis-at-cursor`,
`open-item-add-at-cursor`, `filter-to-item`, `toggle-cat-expand` and
`delete-category-at-cursor`; the view panel's `switch-view-at-cursor`,
`create-and-switch-view` and `delete-view-at-cursor`; tile select's
`set-axis-for-tile` and `cycle-axis-for-tile`; the help overlay's
`help-page-next`/`help-page-prev`; `commit-buffer` in all three kinds; and the
two cell writes ADR 002 lets a thin client name, `clear-cell` and `paste`.

Each mirrors its `Cmd` line for line, status sentence included — a client that
emitted the mutation without the sentence beside it would diverge on the status
line and nowhere else. Two orderings are copied rather than rationalised:
`CommitBuffer` writes its optimistic status first (improvise-6t3) and
`TileAxisOp` writes its axis first.

**A divergence found and fixed on the way.** `effect::HelpPageNext` clamps at
the last page and `HelpPagePrev` saturates at zero, while `reduce_view`'s
`Command::HelpPage` arm *wrapped*. Two derivations of one fact, disagreeing,
invisible because `h`/`l` in the help overlay had always been upstream keys and
nothing had ever sent the command. `walking_off_the_end_of_the_help_pages_
agrees_with_the_host` was written first and failed — client 0, host 4 — and the
wire was then changed to match the effect, since the effect is what ships in
the TUI. Whether both should wrap instead is improvise-trd3.

**Three names the ticket listed do need enrichment**, filed as improvise-4bwa:
`toggle-prune-empty` words its status off `View::prune_empty`, which is in no
cache; `delete-formula-at-cursor` needs `(target, target_category)` and
`FormulaCache` carries only the raw text; and `view-back` emits
`ApplyAndClearDrill`, which is deliberately off the wire (gotcha 38) and is
blocked on ADR 001 layer 2 rather than on a projection.

The lockstep harness is new and is the evidence: it applies what the client
composed to a **second authoritative host** and compares the documents as
`.improv` text (`Debug` prints the symbol table's `HashMap`s, so it differs per
process — gotcha 18). Four injected faults — a wrong axis, a wrong status
sentence, a constant view index, and `AddCategory` for `AddItem` — each fail a
named test; the second one did *not* until the fixture was made to sit on a
category header as well as an item row.

`the_command_names_that_still_need_the_host_are_these` shrinks by exactly what
closed. 1318 tests pass, up from 1311.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:05:17 -07:00
Edward Langley 3a32d1348c docs(landing): drop the duplicated idleTimeLimit
Every cast header already carries idle_time_limit 2.0; repeating it in the
player config is a second answer to one question, and the one that drifts is
whichever is not the cast. Found reviewing against design-principles "a
derivation done twice is the defect".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:04:37 -07:00
Edward Langley 161e36ea60 docs: a landing page, served beside the app rather than in front of it
improvise-e61. Vanilla, single file, 192 lines, no build step: the four casts
improvise-d4w recorded, played by asciinema-player pinned from jsdelivr.

improvise-kh8 recorded a collision to resolve before starting — Pages already
serves the standalone web app at the root, so a landing page either replaces
it, sits beside it, or is dropped. It sits beside it at `/landing/`, because
kh8's own note is right that the live app is the better front door, and a page
the deploy never copies is a file nobody can reach. `web/assemble.sh` copies
it, so the browser harness serves the same tree the deploy publishes and there
is still one answer to "what ships".

The player config deviates from the tickets, deliberately. improvise-d4w's
description and improvise-e61's criteria both say 100x30; every cast header
says 120x37, so the recordings never complied with their own ticket. A 30-row
player clips the bottom seven rows of a 37-row cast, which is exactly where
the tile bar and the status line are, so the page declares the geometry the
casts actually have.

e61 also asked for the casts to be checked against the current UI before
embedding. They were: the tile bar's short_label vocabulary and the mode line
still match, but the casts predate improvise-2zq's `+cat` column by five
months and every one of them opens on the recorder's shell prompt. Filed as
improvise-2oxt rather than silently embedded as current.

Verified by driving the page in Chromium: four players construct, each renders
37 terminal lines after play, no failed request and no page error — against
both `docs/` directly and the assembled `site/landing/`. Counting the players
alone was proved worthless first: with autoPlay false the cast is not fetched
until the click, so a page whose every cast path 404s constructs four players
and throws nothing. Breaking `./casts/` to `./cats/` passes the construction
check and fails the play check with four 404s. improvise-bgtf carries that as
a harness spec, since nothing in CI asserts any of it yet.

README: `cargo install improvise` was false — the crate is not on crates.io —
and the Pages description named two of the three gates a deploy waits on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:04:00 -07:00
Edward Langley 5b7b53466f feat(active-view): split the document's default from each client's view
improvise-jytm, improvise-6c6q, improvise-k69y.

`Workbook::active_view` becomes the document's *default* — what a freshly
connected client opens and what `.improv` writes as `Initial View` — and
`ViewState::active_view` becomes authoritative per client. Two clients may sit
on different views of one document: the model is shared, the UI is not.

- `App::switch_to_view` writes only the client's slice; `App::view()`/
  `view_mut()`/`view_name()` resolve it against the workbook.
- `effect::SwitchView`/`ViewBack`/`ViewForward` are `ViewOnly`, which takes
  `ViewBack`/`ViewForward` off `ModelCommand` — their operand is one client's
  history stack (the `ApplyAndClearDrill` argument).
- `grid_in`/`category_tree_in`/`tile_bar_in` project against the session's view;
  `ClientSession` observes its own move through `projected_view`.
- `apply_switch` takes a `SessionId`, moves only the asker's mirror, and relays
  `Broadcast::ToAsker` only when the document refused. `adopt_active_view` and
  `Broadcast::ToOthers` are deleted: nothing produces them any more.
- `draw_status` reads the client's copy (improvise-k69y).

`the_two_active_view_names_never_diverge` is replaced, not deleted, by
`the_documents_default_view_outlives_this_clients_switches` and
`a_fresh_session_opens_the_documents_default_view`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:02:59 -07:00
Edward Langley c8ebe3cae6 refactor: delete reduce_full and stop apply_command folding a client's view slice
ui/reduce.rs claimed every deployment mode reached the model through it. That was
aspirational, and it is what let improvise-ondj's missing basis check sit
unnoticed: reduce_full had no production caller anywhere in the workspace, so
nothing exercised its Command::Model arm and nothing could notice it skipped the
check.

Deleted rather than wired up, on the argument in its own doc comment: "Per-client
grid updates need a viewport and come from Sessions::project_all." It returned
half of what a host needs. The browser worker was the natural candidate caller and
reaches the model through Host::handle -> Sessions::apply_checked, which is the
half that has the viewport. Its only caller was a test helper, which now spells
the two lines it was.

apply_command's fallback arm no longer folds. It routed a wire view command into
app.view_state -- the authoritative slice -- which nothing in production did and
nothing should: Sessions::apply_checked folds it into the asking client's mirror,
because the model is shared and the UI is not. A client's SetStatus moving the
host's status line was behaviour that was safe only because the arm was dead.
view_slice_commands_land_in_the_authoritative_view_state pinned exactly that, so
it is inverted rather than deleted, and its fixture writes a value into the status
line first so the assertion can tell "the fold was removed" from "the field was
empty anyway". Restoring the fold fails it.

Sessions::apply is now #[cfg(test)]. Host::commands answers the whole frame with
CheckedBatch::against, so a door that answers one command and then dispatches is
the lost update improvise-955n fixed; cfg(test) makes "no production caller" a
fact the compiler keeps rather than a note verified at one commit.

Closes improvise-s17o

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:00:25 -07:00
Edward Langley 0f14b2a39e fix(protocol): a formula-owned cell projects CellId::Derived, not Record
improvise-ir7l. project::cell_id decided Record vs Aggregate purely on whether
a regular category sat on Axis::None; it never asked whether the coordinate
named a formula target. So a formula cell was a Record, render.rs::editable
said true, and a write to it landed, was stored, and was then shadowed forever
by formula_cache — not a revert, a permanent silent shadow, on the demo's
front page.

Rather than a second "but not really" flag beside CellId (the two-answers
shape ADR 002 exists to forbid), a formula-owned cell now projects a different
kind of identity. Derived carries the formula's target and no surrogate, so
the write is unconstructible rather than refused.

The predicate is Model::formula_for(&CellKey), one home, which already existed
three times open-coded inside model/types.rs. The TUI lands in the same change:
EditOrDrill calls the same predicate and refuses, or the two front ends
diverge.

Two orderings are load-bearing and tested: Derived is asked before the record
branch (the coordinate is stored), and after the aggregate branch (gotcha 10 —
an aggregating formula cell is still drillable).

Two tests were passing while their names were false for the Profit column and
are renamed and two-sided now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:59:59 -07:00
Edward Langley 0e193ec5c8 fix: reset the cursor in every effect that renumbers the grid
Only TransposeAxes and CycleAxis reset the cursor, while eight siblings that
renumber just as thoroughly did not. SetAxis is the sharp case: CycleAxis *is*
SetAxis to Axis::next(), so a pair differing only by an argument disagreed.

reset_cursor is also the only writer of cursor_epoch, which is the only thing
Sessions::adopt_cursor_epoch watches, so each gap was invisible in both
directions at once: the host's own cursor stayed stale and no client was told.
Gotcha 24 records that the second half is strictly worse than doing neither.

SetAxis, HideItem, ShowItem, ToggleGroup, TogglePruneEmpty, AddCategory,
RemoveCategory, AddItem, AddItemInGroup and RemoveItem now reset. The item
effects are in because "an append cannot move an existing position" holds only
of the outermost category on an axis: expand_category runs inside the cross
product, so an item appended to an inner category repeats once per outer block.
SetNumberFormat stays out and keeps the negative arm.

CommitAndAdvance's +cat branch depended on the missing reset — it stayed put so
the cursor would land on the column it had just created (improvise-2zq). It now
says so with an explicit SetSelected, placed after AddCategory's abort so a
refused category still leaves the batch alone.

The new test asserts of every case that row_items/col_items really changed
before asserting anything about a cursor; two faults were injected to show both
arms discriminate. Reasoning in context/code/command.md, evidence in
context/test-evidence/command.md. SetPageSelection under prune_empty is the one
residual and is filed as improvise-7kvw.

Closes improvise-9mo9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:57:34 -07:00
Edward Langley 19bb5d3b22 refactor(protocol): ClientState and key resolution are the protocol's (improvise-0yq8)
`resolve`/`step` and `ClientState` lived in `improvise-wasm-client`, and
nothing about either was a browser fact: both depend only on `ViewState`, the
protocol widget caches and the announced window, all already protocol types.
The move costs no new dependency and makes design-principles §5 true rather
than aspirational — a new front end now needs exactly two things, its own
event → `KeyEvent` mapping and painting.

- `state.rs` → `improvise-protocol/src/client.rs`
- `keys.rs::{resolve, step, KeyOutcome}` → `improvise-protocol/src/resolve.rs`
- `socket.rs::{typed, typed_cell_command}` → `resolve.rs`, since `step` calls it
- `BrowserKey` stays in `improvise-wasm-client/src/keys.rs`: it *is* the
  browser half of the two things a front end still owns
- `render_model` becomes a free function, `ClientState` being foreign now

`tests/lockstep.rs` is kept deliberately. It drives `reduce_key` — the host's
`Cmd`/`Effect` path — against `resolve`; those are still two derivations in two
crates, one unreachable from a browser, so the move changed which crate
`resolve` is spelled in and not that it is a mirror.
`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`
does not fire.

1311 tests pass, unchanged from the baseline: this commit moves code and
changes no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:56:33 -07:00
Edward Langley 10bb66b082 ci: gate the comment cap, which has been advisory since it was written
scripts/check-comment-runs.py was invoked by no workflow. The rule seven
agents spent a wave enforcing, and that carried seven silent successes of
its own, gated nothing.

Held back deliberately until every unit of the comment wave had landed: a
gate that passes before the work is done would be the same class of lie the
checker itself carried. Both checks pass against main as of 52507a6 —
"comment runs: none longer than 2 lines" and "451 entries, all name a test
that exists".

Closes improvise-9c8c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:47:54 -07:00
Ed Langley 52507a6678 Merge pull request #42 from fiddlerwoaroof/fix/checker-silent-success
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Browser bundle + static site (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
fix(checker): three silent successes in check-comment-runs.py
2026-09-20 10:43:27 -07:00
Ed Langley 5f871f7035 Merge pull request #41 from fiddlerwoaroof/comments/command
docs(command): move improvise-command's reasoning out of comments into context/
2026-09-20 10:43:18 -07:00
Ed Langley 04e4e3de78 Merge pull request #37 from fiddlerwoaroof/comments/core
chore(core): cap improvise-core's comments, move the arguments to context/
2026-09-20 10:43:09 -07:00
Edward Langley ecce5df30f ci: stop running every job twice on PR branches
`on: push` unrestricted plus `on: pull_request` means a branch with an open PR
fires BOTH triggers, so every job ran twice — visible as the (push) and
(pull_request) pairs in the checks list, twelve successful checks where six
would do. Across this campaign's twelve PRs that is a lot of wasted runner time,
and the browser harness job alone is four minutes each.

Restricting push to main keeps exactly one run per commit: a PR branch is
covered once by pull_request, main once by push. pages-deploy is unaffected —
it already gates on `github.event_name == 'push' && github.ref == 'refs/heads/main'`.

Checked the other workflows: claude.yml is event-driven and release.yml already
scopes push to tags, so ci.yml was the only one doubling.

Deliberately does not touch claude-code-review.yml, which PR #35 is changing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:42:32 -07:00
Ed Langley b6e372898f Merge pull request #35 from fiddlerwoaroof/cleanup-ci
Cleanup ci
2026-09-20 10:40:25 -07:00
Edward Langley 47ef287349 chore: comment out nix test CI 2026-09-20 10:34:45 -07:00
Edward Langley 3224991f4e chore: more disk for nix 2026-09-20 10:31:31 -07:00
Edward Langley 338d0287d2 fix(checker): anchor the test attribute — #[cfg(test)] contains 'test'
Seventh silent success, found by CC-command on PR #41, and the THIRD instance
in this campaign of one class: matching a substring where an exact token was
meant.

'test' in s also matched '#[cfg(test)]', so every cfg-gated fixture was accepted
as a test by --check-evidence — a gate written to reject exactly those. Two
plain fixtures cleared it: Host::model and Sessions::apply_caught_up.

The first instance was the bd dead-ID audit's unanchored exclusion list, where
a real ticket improvise-gp12 would have been silently excluded because it
starts with a crate name. The second was --check-evidence accepting any fn.
This is the third.

Now matched by exact attribute path (test, tokio::test, async_std::test,
wasm_bindgen_test). Fault-injected: a #[cfg(test)] fn under a ### heading is
reported as an orphan, a #[test] fn is not. Re-ran over the five merged units:
215 entries still pass, so none of them relied on the hole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:30:17 -07:00
Edward Langley 288cbca8cf docs(command): demote 27 helper headings to subordinate #### entries
A `###` heading in context/test-evidence/ is a promise that a test of that name
exists. 27 of mine named unannotated helpers -- fixtures, accessors and
tokenizers -- and the shipped --check-evidence accepted them because it matched
any `fn`. Under the strict rule (PR #42) they are orphans.

Each is now a `#### Helper -- `name`` under the #[test] whose scope it belongs
to, opening with a line that says it is not a test. No narrative is deleted: a
helper's scope is the scope of the tests that use it, and that is worth stating
where those tests are described.

Two of the 27 -- `model` and `apply_caught_up` -- are `#[cfg(test)] fn`, which
the fixed checker still accepts as a test. Demoted anyway, and reported.

Verified against gh/fix/checker-silent-success's checker, which was borrowed and
restored rather than committed: 184 entries, all name a #[test].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:55 -07:00
Edward Langley c43932ad79 style: cargo fmt after the module-doc rewrites
Nine stray blank lines left where a multi-paragraph //! became two lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:03 -07:00
Edward Langley 17ad0f1d1d docs(command): move command/keymap.rs and cmd/** arguments to context/
keymap.rs: the dispatch/registry-vs-table split (this file stays the authority
on it) and the improvise-0hb Sequence-flattening decline, with its three
findings.

cmd/**: the two CommitAndAdvance orderings, CommitSave's mode-change-first,
add_category's deliberate no-coordinate-write, EditOrDrill's mode-agnostic
message choice, the SaveAndQuit and CreateAndSwitchView declines, and fit.rs's
whole module doc including the measured wasm bundle cost.

crates/improvise-command now reports no comment run over two lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:02 -07:00
Edward Langley 629f54919f docs(command): move ui/host.rs, viewport.rs and cat_tree.rs to context/
host.rs: the one-App-per-document argument, why ClientFrame::Key answers
nothing, why Host::save bypasses Sessions, and the broadcast-before-projections
ordering. The 955n evidence -- including arm 3's check-before-apply trade that
clause 4 must pay back -- goes to the evidence file.

viewport.rs/cat_tree.rs: the residue of two derivations that moved to
improvise-protocol, and why each residue is the shape it is.

ui/ now reports no comment run over two lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:02 -07:00
Edward Langley e9b800d7a9 docs(command): move ui/project.rs's arguments to context/
The diff-not-declare rule, Dirty::of's derivation (now recorded in
context/code/command.md, which gotcha 35 should point at rather than the doc
comment), axis_frame's three group-header edge rules, the Broadcast audience
argument and Applied's three cases all move. Test narratives, including the
injected faults for hkt6 and the renamed-axis case, go to the evidence file.

One orphaned doc paragraph dropped: a struct doc describing a type that no
longer exists at that site had fused with fn diff's. Noted in the evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:02 -07:00
Edward Langley 115b0bbb20 docs(command): move ui/app.rs's arguments to context/
The residue-tag taxonomy, the record-as-unit-of-atomicity argument, the
process-local XOR fingerprint, and apply_effects reading dirtiness,
layout_version and the record fingerprints off one EffectKind, go to
context/code/command.md. Test narratives go to context/test-evidence/command.md.

One comment was wrong rather than long: entering_records_mode_sorts_existing_data
carried a stray unfinished first line from a neighbouring test. Dropped, and
noted in the evidence entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:02 -07:00
Edward Langley cf8f4f9984 docs(command): move ui/effect.rs's arguments to context/
EffectKind's no-default rule and the simple_effect! mandatory-kind argument,
the abort ordering Save/SaveAs/AddCategory rely on, and
ReturnToNormalUnlessMoved's observe-don't-predict, go to
context/code/command.md. Effect test bug-narratives go to
context/test-evidence/command.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:28:02 -07:00
Edward Langley b2e69e85f3 docs(command): move ui/reduce.rs's arguments to context/
The Checked/CheckedBatch argument -- one basis per frame, checked once, two
tiers hoist and the record tier cannot, and the create-exemption stated over a
property -- goes to context/code/command.md. Test bug-narratives go to
context/test-evidence/command.md, keyed by test name.

The ```text fence illustrating the E0502 that killed the lifetime-tied Checked
moves with its argument rather than being deleted; it was never a runnable
doctest, only a block the checker exempts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:27:59 -07:00
Edward Langley 1dd9fbda8e chore: ignore __pycache__, which the checker generates on every run
scripts/check-comment-runs.py is Python, so running it leaves
scripts/__pycache__/*.pyc — interpreter-version-specific bytecode that a wave
agent then committed, because nothing ignored it. Found by review on PR #41.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:27:27 -07:00
Edward Langley 590b20ba2e fix(checker): fence state and exemption are two facts, not one
Sixth silent success, and I introduced it fixing the fourth. Found by review.

Deciding 'are we inside a fence' and 'is this fence exempt' from one flag meant
a ```text block's BARE CLOSING ``` looked runnable and opened an exemption --
so prose after a non-runnable fence evaded the cap entirely. Reproduced: four
prose lines after a closed ```text block reported clean.

They are now tracked separately, and the delimiters belong to the block they
bound, so a runnable fence's closing ``` is exempt too -- otherwise it lands in
the prose run and a genuine two-line doc reads as three.

Fault-injected both directions: prose after a closed ```text block is a
violation; a runnable block with one prose line either side is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 10:26:13 -07:00