Commit Graph

1522 Commits

Author SHA1 Message Date
Edward Langley e9f7327328 docs: the fmt gate every brief prescribed, and the collapse ruling
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
AGENTS.md's brief template now carries the gate list, because the one I had
been handing agents said `cargo fmt --all`, which reaches only one of this
repo's two workspace roots — and `cargo fmt -p opfs-contract` from the root
exits 1 as a usage error indistinguishable from a real verdict
(improvise-b4ks). Both spellings fail in the direction that looks like
success. The gate is `scripts/fmt-workspaces.sh`.

kb: the improvise-6qet ruling (a collapsed group is one aggregate row), and
the CI lane's doctrine on stubs and fixtures.

Also recovers kb.pl from a truncation: the working tree had lost 276 of 801
facts, most likely to a `kb replace` killed mid-write at a session boundary,
and `kb add` had written onto it with kb_consistent passing. Restored from
HEAD; this commit is 6 insertions, 0 deletions. Filed as improvise-usus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 20:21:05 -07:00
Ed Langley c26e9d3488 Merge pull request #164 from fiddlerwoaroof/edl.review
Edl.review
2026-09-22 20:18:52 -07:00
Edward Langley d0f2bf9789 chore: update cargo deps 2026-09-22 20:18:13 -07:00
Ed Langley 9430ac733c Merge pull request #163 from fiddlerwoaroof/fix/k84e-rebase-rollback
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 clause 4 over the Applied::Rebased path (improvise-k84e)
2026-09-22 20:17:55 -07:00
Ed Langley 46ad715e3a Merge pull request #162 from fiddlerwoaroof/fix/xg1z-editing-overlay
An editing overlay with no buffer shows the cell (improvise-xg1z)
2026-09-22 20:17:45 -07:00
Ed Langley 5c651a0815 Merge pull request #160 from fiddlerwoaroof/fix/m0sx-clearbuffer
Delete the invented Command::ClearBuffer, and guard the class (improvise-m0sx)
2026-09-22 20:17:35 -07:00
Ed Langley 5252cc0044 Merge pull request #159 from fiddlerwoaroof/fix/b4ks-fmt-workspaces
Split a fmt usage error from a fmt verdict across both workspace roots (improvise-b4ks)
2026-09-22 20:17:23 -07:00
Edward Langley 98bdad02dd chore: update cargo deps 2026-09-22 20:16:46 -07:00
Ed Langley d72a68e2ba Merge pull request #158 from fiddlerwoaroof/docs/kb-review-batch
docs(kb): what adversarial review found, and four vacuity shapes
2026-09-22 20:14:30 -07:00
Ed Langley d99b816e0f Merge pull request #156 from fiddlerwoaroof/fix/ub4o-toggle-group-involution
test(command): the toggle-group law was a tautology, and the law is false (improvise-ub4o)
2026-09-22 20:14:19 -07:00
Ed Langley d0a4534f97 Merge pull request #152 from fiddlerwoaroof/fix/55v2-nonfinite-wire
fix(core): a non-finite cell value crosses the wire, in the spelling the format already uses (improvise-55v2)
2026-09-22 20:14:09 -07:00
Edward Langley 5fa3fed146 Test clause 4 over the Applied::Rebased path
improvise-k84e, scoped to what PR #109 left: a frame whose LoadModel
SUCCEEDS, rebases every mirror and emits Snapshots, and is THEN rolled back
by a later command in the same frame.

The document half was already covered by
a_rolled_back_load_leaves_no_ghost_view_and_no_false_status. This covers the
Sessions half, which nothing touched: mutating_frame clones the whole
Sessions before the batch and undo_frame puts the clone back, so the reseed
is undone with it and the computed Snapshots are discarded rather than sent.

It reproduced nothing — the mechanism is sound, as the bead predicted. Its
value is the fault it can see. Injected: `self.sessions = mirrors;` replaced
by `let _ = mirrors;`. The test failed on the projection assertion, naming
the asker's mirror and printing the whole correcting batch — the restored
document being re-sent to a client left holding the discarded one, which is
a client hearing about a load that never happened.

Three things keep it non-vacuous: a separate control asserting the fixture's
load really does rebase both clients, the generation check made before
anything is concluded from the rollback, and a bystander seated where a
wrong relay could move it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 20:11:17 -07:00
Edward Langley 3d1430110c Narrow the fmt gate's exit contract to what it actually maintains
ChatGPT Codex on PR #159: both formatter invocations end `|| rc=1`, so any
failure after the rustfmt probe was reported as "misformatted" although no
verdict was produced. Correct, and the narrower form of the bug this gate
exists for.

Measured: an unparseable source exits 1, identical to a diff; an unknown
rustfmt.toml option is only a warning (exit 0).

Narrowed rather than re-classified, because telling a diff from a parse error
means parsing rustfmt's prose, which is the check-adjacent-to-its-subject this
repo keeps getting wrong. The line the split needs is "did the gate run":
exit 1 names a file and blames the tree; exit 2 names none and blames the
invocation, and every exit-2 cause is checked above the loop.

Two more rivals injected: deleting the `cargo fmt --version` probe, and
turning `|| rc=1` into `|| return 2`. The first exposed a vacuous test -- a
cargo stub that failed for every argument also breaks `cargo metadata`, so the
run exited 2 on the metadata arm and the probe could be deleted with the suite
green. The stub now delegates every subcommand but `fmt`, which is the shape a
missing rustfmt component actually has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 20:09:04 -07:00
Edward Langley b680523296 An editing overlay with no buffer shows the cell (improvise-xg1z)
`render_model` built `editing` as
`is_editing().then(|| buffers.get("edit").cloned().unwrap_or_default())`,
so an `AppMode::Editing` that arrived without its `SetBuffer` painted
`Some("")` — a blank editor drawn over a cell that still holds a value.
The page opens an `<input>` seeded from that string, so the next Enter
commits the empty string and the value is gone. An ABSENT buffer and an
EMPTY one are two different facts, and `unwrap_or_default` collapsed them.

Reachable because the pair is two `Command`s rather than one: this
client's own resolver emits `SetBuffer` then `ChangeMode`, and a
`Snapshot` or a host batch can carry the mode without the buffer.

The fallback is `improvise_protocol::edit_seed`, which is the EXISTING
home of "what an editor opened here starts with" — `seed_at` at the
cursor, the function `enter-advance` already seeds from. A second
reconciliation of the scroll offset inside `render.rs` would have been
gotcha 42's own trap, so this costs one `pub` rather than a copy.

Two tests, and the second is the one that matters for the shape of the
fix: an empty buffer is a state the user typed their way into with
backspace, so refilling it would make the last backspace un-doable. The
over-broad repair — "empty or absent, refill" — fails that test alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 20:06:23 -07:00
Edward Langley 21184bbf64 Delete the invented Command::ClearBuffer, and guard the class
improvise-m0sx. `Command::ClearBuffer(String)` was declared on the wire and
composed by nothing in production: its only occurrences were the enum, the
fold arm, and two of reduce.rs's own tests. Its fold arm removed the buffer
key, while the only two implementations of its own name — the host's
`clear-buffer` command and resolve.rs::write_buffer — both compose
`SetBuffer { value: "" }`, which inserts. So the variant was invented rather
than derived and disagreed with both ends.

Deleted rather than given an effect. Adding a ClearBuffer effect to justify
the variant reverses decision(improvise-cqi, command_set_derived_not_invented),
and it would buy nothing: both readers of the map end in unwrap_or_default(),
so an absent key and an empty one are indistinguishable to every reader.

The guard is the generalisation. vocabulary_is_composed.rs reads the variant
list off serde's derive — the one enumeration the compiler maintains — and
requires each name to be constructed in non-test source, or named in a short
NOT_COMPOSED list with its verdict, checked in both directions.

It found two more orphans nobody had connected to the vocabulary, both
already filed as resolver gaps rather than inventions: SetYanked
(improvise-lut7) and SetSearchMode (improvise-fo0w item 1).

Proved discriminating twice: removing SetTileCatIdx's three production
composers failed the first test naming it, and listing the composed
SetBuffer in NOT_COMPOSED failed the second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:59:41 -07:00
Edward Langley cb3afb559e Split a fmt usage error from a fmt verdict across both workspace roots
improvise-b4ks. From the repo root `cargo fmt -p opfs-contract -- --check`
exits 1 with "package `opfs-contract` is not a member of the workspace" and
never runs --check, so a caller reading $? cannot tell "misformatted" from
"nothing was checked". Reproduced at 7cef9f8.

scripts/fmt-workspaces.sh discovers every [workspace] root and checks each
with `-p <member>` scoped by that root's own --manifest-path. Exit 2 is a
usage error or an unrunnable cargo; exit 1 is a formatting verdict.

Both obvious spellings were measured and both fail in the direction that
looks like success: `--all` crosses the boundary, because it formats local
path dependencies and harness/opfs-contract depends on improvise-io by path
(an injected fault in src/draw.rs was reported by the harness root); and the
bare --manifest-path form is blind to workspace members, because the repo
root is a package as well as a workspace (an injected fault in
crates/improvise-core exited 0).

scripts/fmt-workspaces-test.sh is the check_the_checker. Four rivals were
injected and each failed it. Its fixture had to be corrected twice -- a
missing path dependency and a virtual outer manifest -- and both vacuities
were found only by injecting the rival, never by reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:59:29 -07:00
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 e7de19d011 Merge pull request #147 from fiddlerwoaroof/fix/nzeu-scaling-deser
fix(gp-tree): a deserialized Scaling goes through its one constructor (improvise-nzeu)
2026-09-22 19:43:28 -07:00
Edward Langley 99fda1d2a9 Merge gh/main
Conflict was context/test-evidence/command.md only — the third evidence-file
conflict in this batch, since every agent appends at EOF. Both sides kept and
verified line-by-line against both parents.

1727 passed / 0 failed over 38 binaries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:42:53 -07:00
Edward Langley f94fbedb85 Merge gh/main; the wire adaptor round-trips through any deserializer
`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 through nothing else. Measured on the
pre-fix head:

    from_str  {"Number":"inf"}       -> Ok
    from_str  {"Number":"inf"}  -> Err   (the SAME JSON document)
    to_value then from_value         -> Err   (does not round-trip its own output)
    from_reader                      -> Err

The escaped form is the serious half: escaping is not a distinction JSON
makes, so the wire accepted a subset of the encodings of the spelling it
claims to share with the file. `Cow<'a, str>` fixes all four; the serialized
form is unchanged.

The PR's own tests could not see this — both use `from_str` exclusively,
which is precisely the one path that borrows. A check adjacent to the
question it names, inside the change that defines the question.

Also tightened `a_string_that_is_not_one_of_the_three_tokens_is_refused`: its
second disjunct accepted `data did not match any variant`, which is exactly
the message the above bug produces, so it passed in both worlds.

Conflict was `context/test-evidence/core.md` only, against #143. Both entries
kept and verified line-by-line — neither test cites the file, so a dropped
entry would pass `--check-evidence`.

Gates on the merge: 1728 passed / 0 failed / 7 ignored over 38 binaries,
clippy -D warnings clean, fmt clean, and `cargo check --target
wasm32-unknown-unknown -p improvise-core` clean — the one gate the review
asked for and could not run.

Found by adversarial review (confirmed by injection, not reasoned).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:41:32 -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
Edward Langley f187fa57fe Merge gh/main; fix the whitespace run in the refusal message
Conflict was `context/test-evidence/gp.md` only — this PR and #146
(improvise-ipqu) appended at the same point. Both entries kept, verified
line-by-line against both parents, because --check-evidence cannot see a
dropped entry: it checks that every heading names a running test, and neither
of this PR's tests cites the file, so losing one would pass the gate.

The format string was broken across source lines with no `\` continuation, so
eighteen spaces were baked into what a user reads on --predict against a
corrupt model. Found by adversarial review, which printed the message rather
than reading the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:39:07 -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 ee199a1f96 test(command): the toggle-group law was a tautology, and the law is false (improvise-ub4o)
`law_toggle_group_involution` called `execute` twice against the SAME immutable
`CmdContext`, so it asserted `f(x) == f(x)` for a deterministic `f` — an
assertion no rival of any kind can fail. It was vacuous twice over: its fixture
`two_cat_model()` has no groups, so both sides were the empty string.

Applying the effects between the calls, which is what the bead asked for, does
not rescue the law — it refutes it. A collapsed group contributes no
`AxisEntry::DataItem`, and `row_group_for` is indexed by data row, so with two
groups on the row axis and the cursor at row 0: `z` collapses Q1, `z` again
names Q2 (the neighbour, not the group just collapsed), and a third press
returns nothing because the axis has no rows left.

So this replaces the law with three tests of what the command does, each naming
a category and a group by name. Both rivals were run: an `execute` that always
returns `ToggleGroup { Month, Q1 }` — the stateless one the old test exists to
miss — passes the old law and fails two of the three; an `execute` returning
`vec![]` fails all three.

The behaviour those tests now pin is unchanged and is filed as improvise-6qet:
a collapsed group is drawn but not addressable, so there is no way back to it
from the grid. It is the third bead in that family after improvise-lhi4 and
improvise-0o6x, both of which fixed the header's visibility rather than its
addressability.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:26:31 -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
Edward Langley 5280683f12 fix(core): a non-finite cell value crosses the wire, in the spelling the format already uses (improvise-55v2)
serde_json writes a non-finite f64 as null, and null does not read back as one,
so CellValue::Number(INFINITY) serialized to {"Number":null} and the frame
carrying it was unreadable. The websocket reader skips an unparseable frame and
the worker logs a console error, so one poisoned cell silently dropped a whole
batch — and to_string returned Ok, so the send site could not notice either.

The ticket offered two answers and the format had already chosen. Measured
before changing anything: inf, -inf and nan all round-trip losslessly through
.improv — format_number writes them, the grammar's bare_value accepts them, the
walker maps them back. So refusing them before sending would make a legal saved
document unprojectable to a browser, which is a new failure rather than a fix.
It also settles the ticket's second location: resolve.rs's typed() accepting inf
is correct, not half a defect.

Number now serializes through an untagged adaptor: a JSON number when finite,
one of the three tokens when not. A finite cell's bytes are unchanged, and a
JSON integer still reads as a number rather than as a token.

The tokens are one home — nonfinite_token/parse_nonfinite beside CellValue,
called by the adaptor and by persistence/{format,parse}.rs. Three copies stood
before and a fourth was about to be written. format_f64 is deliberately not
folded in: it is the display layer and these three are a format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:00:13 -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