Commit Graph

1499 Commits

Author SHA1 Message Date
Edward Langley 29e41a32e0 docs(kb): what adversarial review found, and four vacuity shapes
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
From PRs #143/#146/#147/#150/#152/#156/#157 and the adversarial review of the
first five.

The finding worth the review's cost: `Repr::Nonfinite(&'a str)` could only be
produced by a deserializer that borrows from its input, so the new wire
spelling round-tripped through `from_str` and `from_slice` and nothing else —
the same JSON document escaped as `inf` was refused, and `to_value` did
not round-trip its own output. Both of the PR's tests used `from_str`, which
is precisely the one path that borrows.

Four vacuity shapes, each confirmed by injection rather than argued:

- A disjunctive assertion can accept the bug it guards — one branch here was
  exactly the error message the defect produces.
- A law asserted over an immutable `CmdContext` is a tautology: a `Cmd` is
  pure over its context by construction, so `execute` twice without applying
  effects asserts `f(x) == f(x)` and no rival can fail it. Applying the
  effects REFUTED this one rather than rescuing it.
- A rival must fail the property, not the setup — `AddFormula` and
  `RemoveItem` are both `CellData`, so the obvious test proved the neighbour.
- Every agent appends to the same evidence file at EOF: four conflicts in ten
  PRs. Loud, but the resolution is the hazard, since `--check-evidence` cannot
  see a dropped entry.

kb_consistent: TRUE, 803 facts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:49:23 -07:00
Ed Langley 7cef9f8ba0 Merge pull request #157 from fiddlerwoaroof/fix/en4s-formula-panel-item-delete
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
test(project): guard the formula a target ITEM's deletion takes with it (improvise-en4s)
2026-09-22 19:41:00 -07:00
Ed Langley c95ba4a796 Merge pull request #153 from fiddlerwoaroof/docs/kb-tui-and-remotes
docs(kb): the TUI composed-row rule, and a trap in our own remote topology
2026-09-22 19:40:50 -07:00
Ed Langley e49c4f97bb Merge pull request #146 from fiddlerwoaroof/fix/ipqu-warning-order
test(gp): pin the FitOutcome::warnings order the doc comment and gp.md both state (improvise-ipqu)
2026-09-22 19:38:10 -07:00
Ed Langley 3443ec1646 Merge pull request #150 from fiddlerwoaroof/fix/yss5-date-mismatch-warning
fix(io): a date that does not match the detected format is reported, not dropped in silence (improvise-yss5)
2026-09-22 19:37:49 -07:00
Ed Langley 870322d8d1 Merge pull request #143 from fiddlerwoaroof/fix/a36c-count-exact
test(core): count_aggregation asserts the count, not that it is at least one (improvise-a36c)
2026-09-22 19:37:38 -07:00
Ed Langley 0f74691b97 Merge pull request #155 from fiddlerwoaroof/fix/htl6-view-forward-key
fix(tui): `,` walks the view history forward (improvise-htl6)
2026-09-22 19:35:30 -07:00
Ed Langley 971b432c3b Merge pull request #154 from fiddlerwoaroof/fix/swzk-view-history
fix(tui): view history records a move that happened (improvise-swzk)
2026-09-22 19:34:46 -07:00
Ed Langley 16a18feec9 Merge pull request #148 from fiddlerwoaroof/fix/lm5e-effect-fields
test(cmd): sixteen effect assertions name a field, not just a type (improvise-lm5e)
2026-09-22 19:34:37 -07:00
Edward Langley 5e127fe9bf test(project): guard the formula a target ITEM's deletion takes with it (improvise-en4s)
`Dirty::of(ModelChange::CellData)` reports `formulas: true`. That was deliberate
over-reporting in the cheap direction, costing one redundant `Projection` and
buying nothing — until improvise-g3no made `Model::remove_item` drop the formulas
targeting the item. `effect::RemoveItem` is `CellData`, so a deletion can now
take a formula out of the document, and tightening that arm would leave every
connected client's formula panel naming a formula the document no longer has,
forever, with nothing in the stream to correct it.

`removing_a_category_empties_the_clients_formula_panel` covers the
`LayoutGeometry` twin only. This is its item-level sibling.

The setup is deliberately not a mirror of the sibling's. Written that way —
connect, `AddFormula`, `RemoveItem` — it does fail under the rival, but on the
SETUP assertion, because `AddFormula` is itself `CellData` and with the arm
tightened the formula never reaches the client at all. That answers whether
`AddFormula`'s projection arrived, which is the neighbouring question. So the
formula is added before `Sessions::connect` and the client takes it from the
bootstrap seed, which no `Dirty` gates; the rival then lands on the assertion
that means something. The sibling does not have this problem against its own
rival and is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:34:19 -07:00
Edward Langley b1a989d8cf docs(kb): the command lane's doctrine, and a second vector for the base trap
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
From PRs #144/#148/#151. `AGENTS.md`'s remote note gains the worktree-base
vector: a branch cut from the orchestrator's local `main` inherits whatever
that checkout merged but did not push, and nothing in the branch says so —
which is how #148's diff came to name the three orchestrator-only files and
read as a rule violation rather than a base problem.

Four facts. A fold test must fold the batch the producer really emits — a
`Binding::Sequence` flattens into one vec, and a per-step batching cannot see
intra-batch order at all. A type name is not a field, measured: one 14-site
rival left 14 of 16 assertions passing, while an assertion on a *fieldless*
effect is complete rather than smoke. A registry with two arms demotes the
weaker caller, and neither direction is safe by default. And a clean text
merge can delete an evidence entry that no gate checks.

Two gate traps: `--check-evidence` prints the comment cap FIRST, so reading it
through `tail -1` drops the half that was always there; and a compound command
refused by the worktree shell guard runs NEITHER part.

kb_consistent: TRUE, 798 facts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:34:07 -07:00
Edward Langley 5de61e48f4 Merge remote-tracking branch 'gh/main' into fix/swzk-view-history 2026-09-22 19:26:03 -07:00
Edward Langley e645ec883d fix(tui): , walks the view history forward (improvise-htl6)
`view-forward` was bound in NO keymap, in either front end, from the day it was
written. The TUI reached it as `:view-forward`; a browser could not reach it at
all, because `declined()` refuses to enter CommandMode at the door. It survived
because pressing `<` twice is the only way to observe the forward stack, and
until PR #115 no client maintained one.

THE KEY IS `,`, AND `>` IS WHY THE OBVIOUS ANSWER IS WRONG. `<` is `view-back`
and `>` is `drill-into-cell`, so the pair that LOOKS symmetric is not one: `>`
moves within the grid, `<` moves between views. Binding the forward half to
anything reading as `>`'s partner would deepen the confusion that hid the gap.
`,` is the unshifted twin of `<`, so both halves of the history sit on one
physical key, and it carries no direction-in-the-grid meaning.

IT IS DELIBERATELY NOT A CTRL- KEY. Ctrl-R, the universal "forward through a
history" spelling, is browser reload. The keymap is SHARED between the two front
ends, so a key chosen for the terminal is one the browser must live with, and
improvise-clso is already open for exactly this against Ctrl-S.

The absence assertion PR #117 placed in
`the_forward_step_mirrors_its_cmd_and_no_key_reaches_it` is INVERTED rather than
deleted: renamed `..._and_one_key_reaches_it`, it now asserts the binding exists,
keeps `view-back` as the control, and pins the key itself so the binding cannot
drift while the presence check stays happy.

Reasoning: context/code/tui.md.
Evidence: context/test-evidence/{command,clients}.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:25:18 -07:00
Edward Langley 19eed82e66 Merge gh/main into fix/lm5e-effect-fields
Conflict was context/test-evidence/command.md only: improvise-gjat (PR #144)
and improvise-lm5e appended different sections at the same point. Both sides
kept — the entries are disjoint, 16 from lm5e and one from gjat — and verified
line-by-line that neither side lost content, because a clean-looking textual
resolution is exactly how evidence entries go missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:15:45 -07:00
Edward Langley b3490da54f Merge remote-tracking branch 'gh/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
2026-09-22 19:14:28 -07:00
Ed Langley 57be9144d1 Merge pull request #151 from fiddlerwoaroof/fix/6uva-script-cursor
fix(cli): a script's command is built against the cursor it will run against (improvise-6uva)
2026-09-22 19:13:58 -07:00
Ed Langley f7573bff27 Merge pull request #144 from fiddlerwoaroof/fix/gjat-sequence-fold
test(navigation): a sequence folds in order; the old law was a tautology (improvise-gjat)
2026-09-22 19:13:40 -07:00
Edward Langley b9bc46a96f style(io): keep the new comment inside the two-line cap
The comment added with the previous commit ran to three lines and
scripts/check-comment-runs.py failed the `test` job on it. The reason it was not
caught locally is worth recording: --check-evidence is ADDITIVE, so the run does
answer the cap, but the output was read through `tail -1` and the cap's line is
the FIRST of the two. A gate filtered down to the line you expect is not a gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:12:56 -07:00
Edward Langley 0673d202b3 fix(tui): view history records a move that happened (improvise-swzk)
`App::switch_to_view` is guarded — it writes `active_view` only if the document
has the view. `SwitchView::apply` pushed the leaving frame onto
`view_back_stack` BEFORE calling it, under the prediction `previous != self.0`.

That predicate answers "did the user ask for a different name", where the
history means "did this client go somewhere". The two differ on exactly one
input: a name the document has not got. So `:view NoSuchView` left the user
where they were with their own current view pushed onto their own back stack —
`<` then "went back" to where it already was, and the real frame behind it
needed a second `<` nobody knew to press.

The wire path was already correct and is not the model for the fix: `reduce_view`
cannot check (the client holds no `Workbook`), so `apply_switch` relays the exact
inverse `[ViewBack, ForgetView]` (improvise-6c6q). The host holds the document,
so it does not need an inverse — it needs to look. `apply` now clones the
previous name, switches, and pushes only if `active_view` actually differs
afterwards.

That is `ReturnToNormalUnlessMoved`'s rule one effect over, and the same answer
`cursor_epoch` gives instead of matching on `CycleAxis` and `document_generation`
gives instead of matching on `LoadModel` (gotchas 24, 39, 45): observe the state
that moved, never predict it from the request. It also subsumes the case the
prediction was written for — switching to the view you are already on changes
nothing — so the special case is gone rather than joined by a second one.

A guard spelled here as `workbook.views.contains_key(&self.0)` would have been
correct today and a second copy of `switch_to_view`'s own condition, free to
disagree with it the day that condition grows a clause.

Reasoning: context/code/command.md. 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-22 19:12:34 -07:00
Edward Langley b855faf3c5 fix(cli): an explicit coordinate still beats the cursor (improvise-6uva)
Found by the ChatGPT Codex review of PR #151 at head 64bcb82, and correct.

`clear-cell`, `yank`, `paste` and `drill-into-cell` are registered with
`register`, and their INTERACTIVE arms took `|_args, ctx|`: they discarded the
arguments and used `ctx.cell_key()`. That was harmless while a script only ever
reached the pure arm, which reads the coordinates. The moment the headless loop
re-parsed interactively, `clear-cell Region/East` passed the syntax gate and
then cleared whatever the cursor was sitting on — the origin, `Region/North`.

So this ticket's own lesson has a third instance, inside its own fix: two
construction arms that mean different things demote whichever caller reaches the
weaker one, and here the "weaker" one was the arm that had the context. The four
arms now go through `key_from_args_or_cursor` — explicit coordinates win, the
cursor is the fallback, which is what the keymap path wants anyway since a
binding carries no arguments.

`an_explicit_coordinate_still_beats_the_cursor` writes three cells and clears
the one the cursor is NOT on, so it separates "cleared the named cell" from
"cleared the cursor cell". Restoring the arg-discarding arm fails it.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:07:33 -07:00
Edward Langley d7ae99722b fix(io): charge a date mismatch only to a row that lands (improvise-yss5)
Found by ChatGPT Codex reviewing PR #150 at af7fae0e, and correct. The tally was
incremented while walking a record's categories, before the record was known to
survive that walk. A row whose Date is unparseable and whose LATER required
category is missing is skipped entirely — it never lands in the imported model —
yet it had already been charged to the derived column. So the warning counted a
row nothing else in the import reports, and its value depended on the order the
categories happen to be walked in.

Mismatches are now collected per record and merged only after missing.is_none().

The regression asserts the fixture's category order explicitly, because it only
discriminates while Date precedes the missing category: a later change to
proposal order would otherwise make the test pass by never reaching the bad
date. Before the fix it reports three warnings where one is correct; the rival —
merging before the missing check rather than after — reproduces exactly that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:06:45 -07:00
Edward Langley cb3d588ed5 Merge remote-tracking branch 'gh/main' into fix/yss5-date-mismatch-warning 2026-09-22 19:03:28 -07:00
Edward Langley 810398e4a4 docs(kb): the TUI composed-row rule, and a trap in our own remote topology
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
PRs #145/#149. New gotcha 50: `fill_line` takes a `Keeps` because the three
rows sharing a line disagree about which side yields, and the one key table on
screen is the one derived from the keymap — with the import wizard as the
deliberate inverse, since its derived line comes from the mode keymap while
the real keys are per-step.

AGENTS.md gains the remote trap, and it is the orchestrator's fault: briefs
said "merge `main`", and because `origin`/`gitea` take `main` directly while
GitHub gets branches, `origin/main` runs ahead of `gh/main`. An agent doing
the habitual thing pulled four unmerged commits — another PR's whole diff —
into a branch based on `gh/main`. Nothing in CI can see it; the result
compiles and passes. Codex caught it. The window is transient, which makes it
worse: the hazard is absent exactly when you would think to check.

Closes improvise-t14d, improvise-rr8n and improvise-vgp4 — the last
stale-open, fixed by 94a3d17 two days before it was filed.

Measured on main at 78a7bbf: 1700 passed / 0 failed / 7 ignored over 38
binaries, longest single binary 0.69s. kb_consistent: TRUE, 792 facts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:01:31 -07:00
Ed Langley 78a7bbf4f9 Merge pull request #149 from fiddlerwoaroof/fix/rr8n-one-hint-table
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
fix(tui): one key table on screen, and it is the derived one (improvise-rr8n)
2026-09-22 18:59:49 -07:00
Ed Langley 84c73e8739 Merge pull request #145 from fiddlerwoaroof/fix/t14d-title-bar-escape-hatch
fix(tui): the title bar's escape hatch outranks the document name (improvise-t14d)
2026-09-22 18:59:14 -07:00
Edward Langley 64bcb821da fix(cli): a script's command is built against the cursor it will run against (improvise-6uva)
`improvise cmd` and `improvise script` resolved every command through
`CmdRegistry::parse` — the pure arm, which builds `CursorState::default()`: row
0, col 0, and a grid of zero rows and zero columns. The interactive arm, which
builds `CursorState::from_ctx(ctx)`, was never reached from a script. So every
`Move` and `enter-advance` snapshotted a zeroed cursor at PARSE time, clamped to
(0, 0) and emitted `SetSelected(0, 0)` — worse than a no-op, since it RESETS the
cursor. Nothing could position it, so every cursor-reading command was pinned to
the origin.

Reproduced at a9cad15 on a document with Region on row carrying North, South,
East: `move-selection 2 0 . hide-selected-row-item` and a bare
`hide-selected-row-item` both wrote `hidden: Region/North`. After the fix the
first writes `hidden: Region/East`.

The repair is `parse::segments` + `parse::parse_segment_interactive`, with the
headless loop re-parsing per segment, because command n must see command n-1's
cursor. Line atomicity is preserved by keeping the existing whole-line
`parse_line` as a syntax gate, so a line that parsed before still parses and the
only behavioural change is which cursor the commands see.

Two things the ticket did not predict. The TUI's `:` line had the identical
defect — `ExecuteCommand` also used the pure arm — and is fixed the same way.
And doing that surfaced a second instance of the same shape one level down:
`register_pure` built an interactive arm that refused EMPTY args while its own
pure arm accepted them, so `w` with no path parsed one way and refused the
other, breaking `improvise cmd 'w'` and `:w` the moment either re-parsed
interactively. Both arms are now `parse`.

Evidence: context/test-evidence/cli.md; reasoning: context/code/cli.md section 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:59:08 -07:00
Ed Langley 6e10bb3afa Merge pull request #142 from fiddlerwoaroof/docs/kb-session-facts
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
docs(kb): the session's rulings, deferrals and doctrine
2026-09-22 18:54:28 -07:00
Edward Langley 915d0fefeb fix(tui): one key table on screen, and it is the derived one (improvise-rr8n)
Two hand-written key tables were drawn BESIDE derived ones, in a different
notation, and one of them was false.

The tile bar's non-TileSelect hint read " Ctrl+^v<-> to move tiles" while
Ctrl-Left/Right/Up/Down are bound in Normal to enter-tile-select -- pressing one
ENTERS tile mode and moves nothing. It was the only persistent on-screen
documentation of those keys, and it spelt Ctrl+ where key_label, and therefore
every derived hint and help row, spells Ctrl-.

The fix DELETES both of the tile bar's hints rather than rewriting one, which
departs from the ticket's acceptance criteria and does so on the ticket's own
evidence. rr8n proposed rewriting the string and suppressing the STATUS BAR's
hint in TileSelect. But the derived status line is a strict superset of the tile
bar's literal -- it carries Space, h/l and Tab, which the tile bar's omits, as
the ticket's own finding 2 records -- and it cannot go stale, being read off the
keymap. Suppressing it would have deleted information and kept the table that had
just been shown to rot. In Normal the derived line already says T:tiles, so only
the Ctrl-arrow spelling is lost, and it was never documented correctly.

The wizard goes the other way and there the ticket is right. Its overlay draws
correct per-step hints; the status bar drew one static line under it, wrong on
four of eight steps, including telling the user Esc cancels on Done where Esc
goes BACK. That line is derived too, but from the wrong input -- the wizard's
real keys are resolved per step inside HandleWizardKey. So draw_status suppresses
its hint fallback for that one mode.

The rule, stated in context/code/tui.md: keep whichever table is derived from the
thing it describes.

Five tests. Four faults injected, each caught by a distinct test; the sharpest is
suppressing the hint fallback unconditionally, which passes both tests aimed at
the suppression and is caught only by the counterweight asserting TileSelect
still advertises its keys. Two things that let the false hint survive are
recorded: every_hint_key_resolves_in_its_mode cannot see a literal in a widget,
and the widget's own characterization test renders at 80 columns, where the hint
does not fit and is never drawn.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:51:12 -07:00
Edward Langley 6bd6cb5771 fix(tui): the title bar's escape hatch outranks the document name (improvise-t14d)
`fill_line` composed the title bar's two segments by concatenating them and
letting ratatui clip. ratatui clips the RIGHT, and the right segment is
" ?:help  :q quit " -- the only thing on screen telling a stuck user how to get
help or get out. The left segment is decoration whose length is unbounded, so
the affordance was the first thing sacrificed: measured through a TestBackend, a
document named "Quarterly Regional Sales Analysis" stored as
quarterly-regional-sales-2026.improv loses ":q quit" at 100 columns.

Two smaller faults in the same three lines. The padding was `left.len()` --
BYTES -- and the title's own format string spells a "·", two bytes and one cell,
so every document's right segment sat one cell in from the edge it is anchored
to, before any non-ASCII name made it worse. And there was no truncation at all,
so the failure was a mid-token clip of "?:" rather than a graceful drop.

`Keeps` makes the priority a per-call-site decision rather than a global rule,
because it genuinely differs: the title bar keeps the RIGHT (the hatch outranks
the name), while `minibuffer_line` and `draw_status` keep the LEFT, which is what
they already did -- there the left is the user's own typing plus the cursor, or
the status message, and right priority would let a long message eat the text
being edited. Truncation goes through ui::text::truncate, so widths are display
cells throughout and an overflow is an ellipsis.

Five tests, each rendered through a real TestBackend because the loss happens in
ratatui's clip and a helper's return value cannot see it. Three faults injected,
each caught by a distinct test; the reasoning and the injections are in
context/test-evidence/tui.md and context/code/tui.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:51:07 -07:00
Edward Langley af7fae0eba fix(io): a date that does not match the detected format is reported, not dropped in silence (improvise-yss5)
extract_date_component returns None for a value the detected format cannot
parse, and the caller's `if let Some(..)` simply did not push the derived
coordinate — so the row silently left the derived Date_Month/Date_Year category
with nothing reported. Same shape as improvise-2cw9/jc95/5hfh: a value the code
cannot interpret became an absence rather than a report.

Narrower than the ticket reads, and the reason is a property of one function.
detect_date_format accepts a format only when EVERY distinct non-empty sample
parses, and extract_date_component makes the identical parse call — so through
the shipped CLI and TUI wizard a mismatch cannot occur. The reachable door is
ImportPipeline's `pub proposals`, where a library caller sets date_format
itself. a_column_of_mixed_date_formats_is_not_proposed_as_a_time_category guards
that reachability claim rather than leaving it as prose.

One warning per derived column, in the house wording csv_parser already uses.
The row still lands with the coordinates it does have: this reports, it does not
relocate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:46:36 -07:00
Edward Langley 676092b29e test(cmd): sixteen effect assertions name a field, not just a type (improvise-lm5e)
Sixteen command tests asserted `effects_debug(..).contains("<EffectName>")` and
nothing about the effect's fields, so they answered *which effect was emitted*
where they meant *which category, item, view, path, cell or axis it named*.

Run rather than argued: one patch made fourteen command sites emit the right
effect type carrying a wrong constant field — add-category naming "Wrong",
set-cell writing 0.0 at the wrong key, set-axis ignoring its argument,
`:w <path>` saving elsewhere, paste writing a constant instead of the yanked
value, the tile bar acting on a fixed category, page cycling always landing on
the first item, the panel cursor landing on 0 of the wrong panel, and the
formula and view panels deleting and switching to "Other". Against that,
`cargo test -p improvise-command --lib` reported 525 passed, 6 failed, and
fourteen of the sixteen were among the survivors. With the strengthened
assertions and the same rival: 510 passed, 21 failed.

Each test now asserts post-fold `App` state where the effect is cheap and safe
to apply, and the effect's own fields by `Debug` where it is not. Assertions
naming a fieldless effect are left alone: there the type name is the whole
claim.

One thing the ticket did not predict: reading the effects showed `TileAxisOp`
indexes every tileable category including the virtual ones, so tile cursor 0 is
`_Index` and not the first regular category. The tests now say so.

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-22 18:40:02 -07:00
Edward Langley 755e3477c5 docs(kb): drill decomposition, aggregate scope, and a closed ticket read 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
PRs #140/#141. Gotcha 27 gains the one home for what a virtual view must BE
(`view::{records_spine,records_steps,drill_steps}` as data; `cmd/grid.rs::
steps_into_active_view` as the TUI's translation). Gotcha 44 gains the
finding: `RecordHash` is document-scoped and `AggregateHash` is layout-scoped
and cannot be otherwise, so `App::resolve_aggregate` answers only where the
host stands — that, not ADR 001 layer 2, is what blocks browser drill.

Gotcha 29 described improvise-lltt as an open gap. It is CLOSED, and verified
so: every view-scoped `ModelCommand` carries `view: String` and `model_effects`
maps it to `ViewTarget::Named`. A closed ticket cited as an open gap is the
mirror of a dead ID — it reads as a promise that work remains.

improvise-gd28 closed by REMOVING its dependency on improvise-7b8v rather than
forcing past it: the dependency encoded the premise #141 disproved. Files
improvise-t0ma, improvise-obtu, improvise-t219.

kb_consistent: TRUE, 788 facts. Dead-ID audit: 360 ids, all resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:38:51 -07:00
Edward Langley 0bd8097261 Merge remote-tracking branch 'gh/main' 2026-09-22 18:35:14 -07:00
Ed Langley 7400421af8 Merge pull request #141 from fiddlerwoaroof/feat/gd28-aggregate-scoping
test(project): browser drill is blocked by surrogate scope, not ADR 001 layer 2 (improvise-gd28)
2026-09-22 18:35:01 -07:00
Edward Langley 17a582ae4d test(gp): pin the FitOutcome::warnings order the doc comment and gp.md both state (improvise-ipqu)
`fit_measure` concatenates three warning tiers with two `splice(0..0, ..)`
calls. The resulting order — the budget's notices, then the feature table's,
then preprocess's — is stated in the field's doc comment and in
context/code/gp.md section 14, joined into a user-facing status line, and was
checked nowhere: swapping the two statements left all 74 improvise-gp tests
green.

No pre-existing fixture produced more than one tier, which is why nothing caught
it. `all_three_warning_tiers_at_once` adds two things to the wide-category
fixture: a `_Measure` item with no cell anywhere (a header column entirely
missing in every row, which preprocess warns about) and a `Cost` cell at an
unused coordinate (one visible record with no `Score`, which feature_table
skips). Neither disturbs the budget arithmetic.

The assertion is on the three positions, not on the concatenation containing
them, and the helper panics on an absent tier so a tier that stops speaking
fails loudly rather than making the ordering trivially true. Two rivals, each
moving a different boundary, each caught only by this test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:29:35 -07:00
Edward Langley db6593b1b2 docs(kb): gotcha 40 was stale in three sentences (improvise-ilvl)
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
The replay audit found `layout_version` documented as bumping "per
`ModelChange::LayoutGeometry` effect". It does not: `observe_reshape` diffs
`view_shapes` before and after the batch and bumps only where a view's shape
actually moved, so an idempotent `set-axis` bumps nothing. The source comment
at `apply_effects` has said so since improvise-pdvm; the repo map was the
stale copy, and it said the opposite in three sibling sentences — the
parenthetical, the "both tiers are read off the `EffectKind`" claim, and the
"bumps inside the loop" claim, which is also false (it runs once, after).

The observed form subsumes the gotcha 21 argument rather than restating it:
an aborted batch moved no shape, so there is nothing to not-claim.

Also files improvise-6uva (P1) and improvise-up8g, and records the audit on
improvise-ilvl. kb_consistent: TRUE, 783 facts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:27:02 -07:00
Edward Langley f176719fdb Merge main into feat/gd28-aggregate-scoping
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:26:46 -07:00
Edward Langley 71f05675da test(navigation): a sequence folds in order; the old law was a tautology (improvise-gjat)
`law_sequence_associativity` asserted `effects_debug((a ++ b) ++ c)` equals
`effects_debug(a ++ (b ++ c))` — the associativity of `Vec::extend`, true for
any deterministic `execute`, and never applying an effect. No command behaviour
could move it. `doctrine(fold_tests_must_test_order_not_the_axiom)` names
exactly this shape.

Replaced with the property production relies on: a `Binding::Sequence` flattens
into one effect vec (gotcha 21), the batch folds onto the `App` in order, and
each `Move` composed against the same ctx emits an absolute `SetSelected` — so
the moves do not compose and the last write wins. Four orderings are asserted,
two of which land somewhere the others do not.

Two injected faults, each leaving the old assertion passing and failing the new
one: `Move::execute` skipping a move that moves nothing (old ok, new
left: (0, 1) right: (0, 0)), and `App::apply_effects` folding in reverse (old
ok, new left: (1, 0) right: (0, 0)). The second is why the batch is flattened
rather than applied per command — a first version applied each separately and
could not see it.

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-22 18:25:02 -07:00
Edward Langley fbb62ce578 Merge remote-tracking branch 'gh/main' into fix/a36c-count-exact 2026-09-22 18:24:00 -07:00
Edward Langley 7384a00117 test(core): count_aggregation asserts the count, not that it is at least one (improvise-a36c)
`assert!(val >= 1.0)` on a fixture whose answer is exactly 1.0 cleared the bar
for every inflated count. The ticket's fault (`len + 999.0`) is not the worst
case: it moves the empty count from 0 to 999 and so fails
eval_agg_over_no_rows_is_zero_only_where_an_identity_exists. Preserve the empty
case and all 1699 workspace tests pass while COUNT over any non-empty set is
wrong by 999.

Two assertions, because one does not discriminate. The exact 1.0 at Region=East
fails the +999 rival; a Count arm returning a constant 1.0 satisfies it, so the
unscoped COUNT over all three regions being 3.0 is what fails that one. Both
rivals injected; both fail, at different assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:23:47 -07:00
Edward Langley 3be1653385 docs(kb): the session's rulings, deferrals and doctrine
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
Records the campaign's accumulated facts: the Pages-tracks-main ruling, the
reserve-space-do-not-fit-it rule behind the header stability work, the
decimals-by-default decision and its grammar placement, and the doctrine
learned from this session's failures — a ready ticket may already be fixed,
a prose count is checked by no one, a clean merge is not a tested merge.

kb_consistent: TRUE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:23:34 -07:00
Ed Langley 2ccf2e5e2c Merge pull request #140 from fiddlerwoaroof/feat/gd28-drill-decompose
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
refactor(view): a virtual view's shape is model-side data, not an effect list (improvise-gd28)
2026-09-22 18:21:28 -07:00
Edward Langley 19ba09fc8b fix(test): make the session load-bearing, not the view NAME (review)
The first version of this test had the exact defect it was written to expose.
It derived the grid from a hard-coded `grid_in(&app, "Other", WHOLE)`, so the
session, the switch and `apply_checked` could ALL be deleted and every
assertion still passed. It proved a grid built for a NAME where its own name
claims a grid a SESSION WAS SHOWN -- a check adjacent to the one it means,
which design-principles S7 calls this repo's characteristic defect.

The view is now read off `sessions.get(id).view().active_view`, asserted to
have moved, and asserted to differ from the host's; the grid is derived from
that and from the session's own viewport. Injected fault: delete the
`apply_checked` line -- the test now fails with `left: "Default" right:
"Other"`, which it could not do before.

Found by the ChatGPT Codex review on the PR, which was right about this for
the same reason it was right about the two predicates on #140.

1700 passed, 0 failed, 7 ignored. --check-evidence unchanged at 824/302.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:18:24 -07:00
Edward Langley c14ab052a5 test(project): browser drill is blocked by surrogate SCOPE, not by ADR 001 layer 2 (improvise-gd28)
improvise-gd28 records browser drill as blocked on the addressed command form,
because `ApplyAndClearDrill` is session-local and unaddressable. That argument
is about the COMMIT path. Entering a drill commits nothing, and after the
view-stack work already on this ticket a browser has `<` -- so what is missing
is only the step that builds `_Drill`.

Every command that step needs is already on the wire and already unaddressed:
`CreateView`, `SetAxis` and `SetPageSelection` each NAME their view
(improvise-lltt), and `SwitchView` is view-half and per-client. What a browser
cannot supply is the operand. It holds `CellId::Aggregate(AggregateHash)`, an
opaque u128, so it cannot compose the filter coordinates itself -- ADR 002
working as designed, and it points at the host resolving the surrogate.

`App::resolve_aggregate` does exactly that and is tested. This test shows it
answers only where the HOST stands. A `RecordHash` falls back to `DataStore`'s
maintained index and resolves against the document, which is why a connected
client's `SetCell` works from any view; an `AggregateHash` has no such index
and cannot have one, since an aggregate has no existence outside the layout
deriving it. So it is layout-scoped, and that layout is built from the host's
active view -- which since improvise-jytm is not the client's.

The test seats a session on a view the host is not on, takes a surrogate out of
the grid `grid_in` derives FOR THAT SESSION, and shows the host answering
`None` to an identity it had just projected. The control is what makes it
discriminating: the same surrogate resolves once the host is moved onto that
view, so the failure is the host's view rather than the surrogate. Without it a
blanket `None` would pass.

What this rules OUT is the useful part. It is not ADR 001 layer 2: a drill's
mutation is addressed to no one session -- it configures the shared `_Drill`
view exactly as `CreateView`/`SetAxis` already do -- and only its operand's
resolution is session-scoped. It is not improvise-fdk1 either: `edit_or_drill`
declines records-mode editing out loud before reaching a cell, so a browser
cannot edit inside a drill grid at all, no coordinate rename is reachable, and
no second staging scheme is created. The remaining work is one seam, and the
cheap shape is the one every other view-scoped variant already uses: let the
command name its view and resolve against a layout derived for that name.

No behaviour changes here. 1699 -> 1700 tests, the one added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:11:29 -07:00
Edward Langley ab227cdef6 fix(view): restore the two predicates a mis-targeted rival-revert corrupted
Both faults were injected on purpose to show the extracted functions are on
the live path, and the revert of the second one replaced the WRONG occurrence:
`str.replace(".is_measure()", ".is_index_or_dim()", 1)` hit the first match in
the file, which is `drill_constraint`'s pre-existing predicate at line 78, and
left `records_steps`' injected one at line 137 in place. So the commit carried
two faults where it had shown one:

- `drill_constraint` stopped recognising a formula targeting `_Measure`, so the
  derived measure stayed in the constraint and `matching_cells` returned no
  source rows -- the defect `drill_into_formula_cell_returns_data_records`
  exists to catch.
- `records_steps` put `_Index` and `_Dim` on Row and Column and then overwrote
  both with `Axis::None`, while never aggregating `_Measure` away, so entering
  records mode lost its spine entirely.

Found by the ChatGPT Codex review on the PR, reading the diff, and both of its
findings were exactly right.

The process failure is the part worth recording: after reverting the rivals I
re-ran clippy, fmt, --check-evidence, the node suites, the eight-crate wasm
gate and Playwright, and every one of them passed over the broken logic. The
one gate that could see it -- `cargo test --workspace` -- was measured BEFORE
the injections and never again, so the PR body's "identical, 1699 passed"
was a figure that was checkable and false. An injected fault is not reverted
until the full suite has been re-run; the targeted test passing again only
shows the targeted fault is gone.

Re-measured after this commit, on the orchestrator's machine: 1699 passed, 0
failed, 7 ignored over 38 binaries -- identical to the base at `a9cad15`, which
is what this PR claims and now actually has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:07:30 -07:00
Edward Langley fe8d5404e2 refactor(view): a virtual view's shape is model-side data, not an effect list (improvise-gd28)
`DrillIntoCell::execute` and the records-mode command each spelled out what
their view must be as a run of `effect::SetAxis { view: ViewTarget::Active }`
pushes, interleaving the model question ("what is this view") with the UI
question ("how does a TUI keypress get there"). The two spellings shared the
`_Index`/`_Dim` spine and nothing kept them in agreement.

`view::layout` now answers the first question as data: `records_spine()`,
`records_steps(model)` and `drill_steps(key, none_cats)`, each a `Vec<ViewStep>`
of `Axis`/`PageSelection` assignments naming no effect, no `App` and no front
end. `cmd/grid.rs::steps_into_active_view` is the whole of the second question
for this front end -- a browser composing `ModelCommand`s against `_Drill` will
answer it differently without touching core.

`drill_constraint`/`drill_snapshot` were already model-side and are untouched;
core.md S8 records why their order cannot be derived a second time. `DrillState`
deliberately does not move: it holds an `Rc` and its `pending_edits` are
uncommitted writes belonging to whoever staged them, so it stays on `App` where
gotcha 30's session-local-residue tag puts it.

Behaviour is unchanged and no test expectation moved, which is the strongest
evidence a pure separation can produce. Measured on the orchestrator's machine,
before and after:

  cargo test --workspace   1699 passed, 0 failed, 7 ignored over 38 binaries
  node web/tests           56 tests over 6 files
  node web/tests/bundle    4 tests over 1 file
  playwright (harness)     68 passed, 4 skipped
  --check-evidence         823 entries, 301 citing tests, 83 prose headings

Two rivals injected against the extracted functions, to show they are on the
live path rather than a parallel copy: `drill_steps` emitting `Axis::Page`
instead of `Axis::Filter` fails `drilling_fixes_its_coordinates_on_filter_not_page`,
and `records_steps` filtering on `is_measure()` instead of `is_index_or_dim()`
fails `save_with_unfinished_record_is_readable`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 18:01:18 -07:00
Ed Langley a9cad15827 Merge pull request #137 from fiddlerwoaroof/fix/f9qf-pin-the-shell
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
fix(scripts): one shell and one environment for every checker (improvise-f9qf)
2026-09-22 16:15:27 -07:00
Ed Langley 1ae2d2e75a Merge pull request #139 from fiddlerwoaroof/fix/k844-importance-overflow
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
fix(gp-tree): a share of a large importance overflowed f32 twice over (improvise-k844)
2026-09-22 16:10:03 -07:00
Edward Langley a14955cfb4 docs(tooling): the seventh sh-override is a TOML array, which no shebang gate can see
Reasoning: context/code/tooling.md section 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 16:09:12 -07:00
Edward Langley f52c2df0c7 fix(dist): the extra-artifacts build honours its script's shebang (improvise-f9qf)
`build = ["sh", "scripts/build-web-release.sh"]` overrides the shebang the
same way the old local gate did, so the script ran under dash on the release
runner while declaring bash. It is the seventh such site and the only one
outside a shell file; scripts/checkers.sh cannot see a TOML array, so this one
is fixed by reading rather than by a gate.

The script is mode 100755, so dropping the `sh` runs it through its own
`#!/usr/bin/env bash`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 16:08:33 -07:00