220944e26c69c6066a68d33ecd206efeb58f1784
371 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
787e354b9b |
doc-site: name context/code/pr-review.md as an omission
The `build` job fails on main and on every branch merged from it: doc-site: context/code/pr-review.md is neither published nor a named omission NOT a merge loss -- scripts/build-doc-site.mjs in this branch is byte-identical to main's, and neither lists the file. PR #96 added context/code/pr-review.md without a manifest line, which is exactly the gate improvise-res9 added: a doc added without a line publishes nothing and says nothing, so the generator refuses. Omitted rather than published, and the line is cloud-agents.md's rather than tooling.md's. tooling.md explains checkers a reader of the code will meet; pr-review.md is about THIS REPOSITORY'S PROCESS -- where a Codex review appears, how to tell it from our own agents' comments, why absence is not approval. Nobody visiting a website is about to read a PR thread. Both homes updated, because an omission is restated beside the manifest AND in doc-site.md section 2, and the prose there counted them ("Neither is referenced...") so it went stale at three. This strays into scripts/, which is Wave N's ground. Flagged in the hand-back; it is a one-line data entry either way if the orchestrator would rather publish it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
3d0850c132 |
Merge main (d7bfcbb, PR #96) into wave/operandless-commands
Two textual conflicts, and one the merge resolved cleanly and wrongly. effect.rs: #96 extracted the LayoutGeometry list into layout_geometry_cases() shared by two tests, one of which compares it against the wire vocabulary; my side had added ViewTarget arguments and carried the base's assert_eq!(cases.len(), 13). Kept #96's structure with my arguments and dropped the count -- the list-versus-vocabulary check is strictly stronger and a count beside it would be a second answer to the same question. Verified the count came from the merge base, not from my side, before dropping it. test-evidence/command.md: both sides appended entries at the same point. Kept both. app.rs auto-merged CLEANLY and did not compile: improvise-4dpj's new a_batch_that_leaves_records_settles_out_of_records_normal constructs effect::SetAxis, which now carries a view operand. Fixed with ViewTarget::Active -- the host's own key path, which is what that test is. Checked rather than assumed, per the brief: - Test names diffed BOTH ways against the union of the two parents, not the count. Union 1510, merged 1509, exactly one name absent and it is the intended one: a_hosts_none_cats_leak_into_another_views_formula_columns, the ac2z characterization test my own branch replaced with a_formulas_columns_survive_a_host_that_aggregates_differently. Nothing else lost, nothing unexpected gained. - #96's the_enumeration_names_every_variant_the_derive_declares is still discriminating against the reshaped ModelCommand variants: dropping TransposeAxes from every_model_command() fails it by name. - The test-support feature stays off in shipped builds: cargo tree --edges normal --target wasm32-unknown-unknown finds no serde_json. And the finding the brief asked for, which is the opposite of a disagreement: #96's switching_views_across_records_ness_settles_against_ the_view_switched_to is the HOST-side twin of the client fix in this branch. Same batch, same crossing in both directions, and independently the same phrase in its message. Neither is redundant -- nothing in the projection stream carries the host's ViewState, so a host-side guard cannot see a client's mode or the reverse. Cross-referenced in context/test-evidence/clients.md so neither is deleted as a duplicate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d7bfcbbd93 | Merge gh/main (PR #96) into orchestrator main | ||
|
|
f1d98534f4 |
Merge pull request #96 from fiddlerwoaroof/wave/gate-and-coverage
Three checks that could not fail: duplicate evidence headings, the records-mode settle, the LayoutGeometry case list |
||
|
|
60bb3c56f1 | Merge gh/main (PR #97) into orchestrator main | ||
|
|
81fe546594 |
Settle the mode by observing the grid, not at each composition site
ChatGPT Codex's P1 on #98: switch_view_at composes [SwitchView(target), ChangeMode(Normal)], and changed_mode settled that against the grid the client STILL HOLDS -- the outgoing view's. Records -> pivot emitted RecordsNormal and pivot -> records emitted Normal, which is the cross-view drift this PR exists to close, by a second door. The hand-back predicted this as follow-up 3 and called it unreachable because R is a stranding decline in the browser. That is right about the TOGGLE route and wrong about this one: records-ness is auto-detected from the axes (gotcha 1), so a view switch changes it with nothing in the batch saying "records". One door was enumerated and there were two. So the remedy is general rather than a patch to switch_view_at. changed_mode is DELETED; every site emits the plain AppMode::Normal; and ClientState::fold settles after every fold against the grid it HOLDS. Route-independent, so no enumeration of composition sites is load-bearing any more -- the same shape the repo already prefers in cursor_epoch over matching on CycleAxis, document_generation over matching on LoadModel, and the dirty flag over a table of command variants. improvise_protocol::settled_mode is the one rule and all three ends call it: ClientState::fold, ClientSession::fold (the server's mirror, so the two cannot diverge in a field nobody currently reads today) and App::settle_normal_mode. App::normal_mode is gone -- it was that function's host-side duplicate, and my change orphaned it. Bidirectional coverage drives a real Host: a session connected to a records-mode host, the pair folded locally and sent as a ClientFrame::Commands, every reply delivered in arrival order. The composed ChangeMode carries the STALE answer deliberately, because composing the friendly value would let the records -> pivot direction pass under the fault. Both directions fail with settle_mode no-opped, each proved separately since the loop stops at the first. Evidence, the injections and why the first fix's unreachability argument was too narrow: context/test-evidence/clients.md, context/code/protocol.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
e7c63e9530 |
kb+docs: strengthen the check, do not settle for correcting the prose
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
Three facts from Wave N's Codex round. The first is the one that generalises. improvise-26dm's round 1 found its OWN defect — the right-hand side of a completeness diff was a second hand-written list, not the derived enumeration its prose claimed — and shipped an honest correction of the prose. Codex asked for the fix instead and was right: the disclosure was accurate and still the weaker artefact, because the fix was four lines of plumbing. Correcting prose is the fallback, not the deliverable. Two traps, both of which caught someone: cfg(test) and pub(crate) are different walls, and only one is a visibility keyword. The follow-up said 'pub(crate) -> pub' — filed by the agent that wrote it and repeated by me when I authorised 'one token' — because both of us read the item line and not the attribute above it. And cargo tree shows DEV edges by default, so asking whether a new feature leaked serde_json into the wasm bundle reports it PRESENT. --edges normal reports 0. I verified both readings myself. Same class as the difftastic grep trap: a command that looks like it answers the question, answering a neighbouring one, in the direction that reads as a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
67905d9440 |
scripts: pr-reviews, because twenty PRs merged with their reviews unread
Prints a PR's reviews and says whether each targets the CURRENT head or an earlier one, because that changes what you do: a finding against an old commit may be fixed or still live, and both have happened this week. --wait blocks until a review lands against the head. Observed latency is ten to fifteen minutes after a push, so a merge four minutes later races the review that was about to arrive. When it times out that is a decision to merge unreviewed, not a clean bill. The detection rule is placement, not authorship: Codex posts under the owner's account, the same one our agents use, and ENDS with its attribution where ours OPEN with theirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
498582deb1 |
Bound every_model_command() with the derive (improvise-26dm, Codex P1)
The right-hand side of the LayoutGeometry completeness diff was a second hand-written list. Both this branch's own review and ChatGPT Codex found it, and the previous commit corrected the PROSE; Codex asked for the FIX. This is it. the_enumeration_names_every_variant_the_derive_declares diffs every_model_command() against variants::declared::<ModelCommand>() -- serde's derive-generated variant list, improvise-5ubs's mechanism. The chain is now derive -> every_model_command() -> model_effects -> case list, every link checked, which is what the ticket asked for. Injection is Codex's own, run end to end: a ModelCommand::ProbeGeometry whose effect declares LayoutGeometry, with the model_effects and variant_name arms the compiler forces, omitted from BOTH lists. Before: the whole workspace green. After: staged, each stage naming the probe -- every_model_command() never constructs ["ProbeGeometry"] then, with the enumeration repaired and only the case list short, the_layout_geometry_cases_are_every_wire_effect_of_that_kind right: [... "ProbeGeometry" ...] The staging is the point: the two tests answer different questions. SCOPE OVERRUN, declared. `variants` was `#[cfg(test)] pub(crate)`, not merely `pub(crate)`, and `declared()` needs serde_json, which improvise-protocol carries only as a dev-dependency -- so the authorised one-token change could not work. What it took instead, all plumbing, no behaviour change: improvise-protocol/Cargo.toml serde_json optional + a `test-support` feature improvise-protocol/src/lib.rs #[cfg(any(test, feature = "test-support"))] pub improvise-command/Cargo.toml dev-dep enables that feature The feature is OFF in every shipped build: `cargo tree --edges normal --target wasm32-unknown-unknown` shows no serde_json, and all eight wasm-gated crates still check clean. Drop this commit if the crossing is unwanted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
c2bc939608 |
improvise-x1ns (2nd): the preflight counts a date measure's parts
P1 from ChatGPT Codex on #97, live at
|
||
|
|
ab6bb41da3 |
Review of this branch: two checks that could not fail, one drift left live
changed_mode settled only Normal -> RecordsNormal, but the host's
settle_normal_mode settles BOTH members of the pair, so a RecordsNormal
over an ordinary grid stayed put on the client and became Normal on the
host. Esc out of RecordsEditing resolves through entered() to
`enter-mode records-normal`, so the mirror-image drift was reachable by
the same route the fixed one was. Now total, with the pass-through arm
and both negative rows asserted.
a_compacted_seed_folds_to_the_same_cache_as_the_whole_history left
GridCache::records at its default, so a seed() that OMITTED the arm
compacted to an equal cache and the check could not see the omission.
The history now sets it; deleting the arm from seed() fails the test.
grid_updates' records arm had no test at all: grid_of setting the field
is covered by lockstep, but a client that connects before records mode is
entered learns only through the diff.
Evidence for all three, with the injected faults:
context/test-evidence/{clients,command}.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
f6fca7ec98 |
A formula is evaluated for the view being projected (improvise-ac2z)
rebuild_layout recomputes formulas for the HOST's none_cats, and grid_in's elsewhere path built another view's GridLayout against that cache. The cache is keyed by the coordinates the recompute used, so a Total keyed without a Product coordinate is not found by a view with Product on Column, and prune_empty drops the formula's columns entirely. Absent columns, not wrong numbers. grid_in::rederived_for clones the model and recomputes for the target view's none_cats. Three conditions gate the clone -- another view, different none_cats, at least one formula -- so a client on the host's own view pays nothing and a formula-free document pays nothing ever. This is an interim and the cost is why: a whole-model clone plus a fixed-point recompute per projection on exactly the broken path. The ticket's first candidate, keying formula_cache by none_cats so one Model holds more than one view's derived values, is the durable answer and is an improvise-core change outside this wave's ground. The characterization test is replaced by the positive one, and the none_cats carve-out is dropped from a_clients_grid_does_not_depend_on_which_view_its_host_is_on -- which is the general property this is an instance of, and which fails under the same injection. Evidence: context/test-evidence/command.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
618801701e |
The browser asks the grid what kind of grid it is (improvise-e5pf)
One substitution at three decision points: wherever the host asks layout.is_records_mode(), resolve.rs asked state.view.mode.is_records(). The two agree only while the mode and the layout agree, and Command::ChangeMode(AppMode::Normal) is the message that separates them -- the host settles Normal against the layout the batch ends on, reduce_view folds the variant it was handed. GridCache::records carries the fact, projected by grid_of off layout.is_records_mode() and diffed like the extent. Deriving it on the client from row_frame.cats would have been a second implementation of is_records_mode (improvise-edp's disease); deriving it from the mode is the bug. Like Extent it frames nothing, so apply does not re-fit and its batch position is not load-bearing. The client's settling has one home, resolve::changed_mode, reached by every site that can emit AppMode::Normal -- including entered(), which is how a bound `enter-mode normal` gets there. The three decision points ask state.grid.records, so a commit at a records cell declines rather than composing typed_cell_command against a grid whose columns are [coords.., +cat, measures.., +]. lockstep now DRIVES the keys: in_lockstep presses on both ends and refreshes the client from a real projection rather than from the host's slice, which is what let the old records coverage start from agreement. The reachable path is Tab (category panel) then Esc; c/f/v are Ignored in RecordsNormal and R is a stranding decline, which a first draft of the test found the hard way. Two resolve.rs fixtures put a records mode over a non-records grid -- a state no host can produce -- and had to be corrected with the fix. Evidence and both injected faults: context/test-evidence/clients.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
7bc26f80d5 |
Review round 1: three checks that did not check what they named
All three found by the adversarial review of
|
||
|
|
f128087999 |
review: correct six prose claims, four of them false
An adversarial review of this branch found that the prose was worse than the diff — including, twice, in the exact genre the branch was congratulating itself for catching. Recorded rather than quietly rewritten, because that is the shape. Must-fix, all verified before correcting: - `fit.rs`'s comment on the exact check still read "this is the check that enforces the bound in both directions of a drift" — verbatim the sentence gp.md §14 now spends 25 lines refuting. Gotcha 48's third clause exactly: a fact stated twice is changed by grep, not by edit. I refuted the copy in the prose and left the copy in the code. - "false for eight months" was fabricated. `encoded_width` landed 2026-09-19, the sentence 2026-09-20; the repository itself is six months old. A paragraph whose point is that unchecked cost claims are checkable closed on one. - "NumericCategory was the one artefact family with no coverage" is false: it was one of three, and `ValueOfAMeasure` and `AbsentCoordinate` still have none. Corrected in both files that said it, and the remaining gap is named. The decline's stated reason was wrong, which matters most because a decline is the artefact nothing checks. It claimed one-hot is the only encoding of a category whose splits are spellable; `is_expr` is a `primary`, so an ordinal split IS spellable as a weighted indicator sum. Verified by running `parse_formula`, not by re-reading the grammar, and now pinned by `an_ordinal_split_on_a_category_is_spellable_as_an_indicator_sum` so the paragraph cannot go quietly wrong again. e8no stays declined on grounds that survive: a category's items carry insertion order rather than magnitude, and the emitted formula spells one IS per item either way, so the width is not saved. Also weakened two overclaims — "marking it a Label yields the same fit" (false: the feature count moves the RNG draw sequence, gotcha 37b) and "asserted rather than argued" (the dichotomy is argued from spell()'s exhaustive match; the test asserts one instance) — narrowed "the only coordinate predicate is is_expr" to what is true of it, corrected an uncheckable test count, named which two of the three consumers of parse_number_format are actually guarded, and moved an evidence block that had been inserted under the wrong module heading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
1dd6950ed3 |
A view-scoped ModelCommand names its view (improvise-lltt)
Nine ModelCommand variants configure a view and named it implicitly: the effect read App::view_mut(), the HOST's slice. Since improvise-jytm made ViewState::active_view authoritative per client, a client on another view moved the host's view instead of its own -- its grid did not change and every other client's did. Each of the nine now carries `view: String`; model_effects maps it to effect::ViewTarget::Named, and the nine effects carry a ViewTarget rather than reading an ambient one. There is no operandless form left: App::view_mut() is deleted and replaced by view_mut_of(&ViewTarget), so a future geometry effect has to say which view it writes. The host's key path passes ViewTarget::Active, which is right -- the TUI is the host and has no session to ask. A Named view the document has not got is a no-op rather than resolve_view's fallback onto the default. Second half, which the ticket did not predict: naming the view made a reframe of a view the host is NOT on reachable, and adopt_cursor_epoch could not see one -- cursor_epoch is bumped only by the host's own reset_cursor, so those clients were reset by nobody and told nothing (gotcha 24's strictly-worse-than-nothing state). App::last_reframed_elsewhere is the second trigger; its opening frames are taken lazily inside view_mut_of, the one door such a write goes through, so the held-key path still hashes one view and improvise-mcq4's declined cost is not paid. Evidence and the three injected faults: context/test-evidence/command.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
78ca25b5e0 |
Test both arms of the records-mode settle (improvise-4dpj)
settle_normal_mode matches `Normal | RecordsNormal`, and replacing that with a
one-way `Normal` left the ENTIRE workspace suite passing: the existing crossing
test drives the layout both ways, but both branches end in `enter-mode normal`,
so the settle is handed `AppMode::Normal` either way. It discriminates "settles
at all" and "settles against the ending layout", never "settles from either
mode".
Gap 1, a_batch_that_leaves_records_settles_out_of_records_normal: the
production repro -- SetAxis { "_Index", Axis::None } from a records grid.
Injection is the one-way guard; it is the only test in the workspace that fails.
Gap 2, switching_views_across_records_ness_settles_against_the_view_switched_to
(raised by ChatGPT Codex on PR #91, verified correct on main first): the batch
SwitchViewAtCursor emits, both ways. A genuinely different path -- records-ness
is auto-detected from the axes, so nothing in the batch says "records".
Injection is moving the settle back above rebuild_layout; both fail by name.
Scope note in the evidence: this drives the effects, not the view-panel key
path, which needs a CmdContext in another wave's ground this round.
Also carries the command.md half of improvise-hsoe's three duplicate headings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
383e33a1a0 |
Refuse a duplicate test-evidence heading (improvise-hsoe)
--check-evidence verified that a heading names a test that RUNS, and a duplicate names one just as well, so both copies pass. PR #92 shipped two entries under one heading; the older described a fixture the test no longer had. Refuse rather than warn, on the improvise-rm4s precedent, naming the file and every line in file order. Keyed by the RESOLVED test, so `### f` beside `### m::tests::f` collides too -- the case rm4s's qualification repair creates. Six SELF_TEST_DUPLICATES cases, including the three clean arms: two different tests sharing a leaf are ambiguous and not duplicate, prose repeats freely, and two headings naming two tests are fine. Resolves the two tui.md duplicates: the older column-header copy called THE_MEMBERSHIP_CORRECT_SPANS a live tripwire three hundred lines above the entry recording its removal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
e045f9871b |
improvise-x1ns: the preflight counts the (missing) one-hot; improvise-e8no declined
improvise-x1ns (found by ChatGPT Codex on PR #90, live on main at
|
||
|
|
3775d4ea1a |
improvise-711n: a trailing conversion letter no longer erases the precision
`parse_number_format` read the precision as `fmt[i+1..].parse::<u8>()` over
everything after the last `.`, so `,.2f` — the printf spelling the repo-map's
own `.improv` example carries — failed to parse and fell through to zero
decimals, silently, in the TUI, the browser projection and CSV export.
The precision is now the leading digit run after the last `.`, with any
conversion letter ignored. The spellings that are legal are stated once, in
`context/code/core.md`, along with why the trailing `f` is accepted rather than
refused (the grammar takes the format line as an opaque `rest_of_line`, so
refusing it rejects documents this repo already writes) and why the reading is
printf's rather than Excel's (`.00` is the spelling the two disagree on).
`export_csv_quotes_commas_per_rfc4180` asserted the value `starts_with("1,200")`
against a `,.2f` fixture, so it could not tell two decimals from none. It is now
an exact equality, and reverting the fix fails it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
d41c8b8e6f |
Merge main (1a9f45a) into wave/protocol-render-gaps
#91 and #93 landed while this wave was in flight and appended to the same four context/ files. That conflict is why CI never ran: a pull_request workflow builds refs/pull/N/merge, and a CONFLICTING PR has no merge ref, so no run is ever created. Neither the close/reopen nor the empty commit could have helped. Resolution: keep both sides. Only context/test-evidence/protocol.md conflicted textually; the other three auto-merged. Verified with --no-ext-diff that nothing of this branch was dropped -- the only removals are main's own rewrites, of the frame-basis paragraph in context/code/command.md and of the minibuffer-exit entry (renamed _to_plain_normal -> _to_records_normal) in test-evidence/command.md. NOTE the first three "nothing was dropped" checks were WORTHLESS and read as clean: an external diff tool is configured, so `git diff | grep '^-'` was grepping a side-by-side format for unified-diff markers and matched nothing whatever the content. --no-ext-diff is what actually answers the question. A check that returns empty for the wrong reason is indistinguishable from one that passes. Fixed while re-reading, and pre-dating the merge: this branch had TWO entries headed a_window_narrows_the_cells_and_neither_the_totals_nor_the_widths -- the original, describing a fixture that no longer exists and repeating the claim the PR #92 review measured false, and the superseding one. Merged into a single entry whose subject is what each of the three fixtures was blind to. The evidence checker cannot see this: it verifies that a heading names a real test, and a duplicate heading names one just as well. Merging gh/main ( |
||
|
|
902a77c174 |
review: two corrections from the #92 adversarial review, and a P2 they found
1. THE JMJZ WIDTH ASSERTION COULD NOT SEE A WINDOWED WIDTH, and the evidence
said it could. Injecting `if drawn { widen(..) }` -- widths from the window only
-- left 1476 passing, 0 failing. Two reasons, the second structural: the measure
was named `Revenue` (7 chars) against a value of `300021` (6), so the assertion
passed on the header alone; and comparing grid_body's widths against
compute_col_widths is INCAPABLE of testing windowing, because compute_col_widths
IS grid_body with an empty window. Windowing degrades both sides identically.
The same function twice is not a check, and it reads exactly like one.
Fixture is now the reviewer's shape -- a total that cancels and a one-character
header -- reordered once more so BOTH assertions discriminate: with the large
values last, the windowed row and the cancelled total were both 1.0, so the
totals assertion went blind to the row-window fault while the widths assertion
happened to catch it. Large values first:
for ri in rows.clone() -> totals [888888888.0] against [1.0]
if drawn { widen(..) } -> widths [10] against [11]
10 rather than 5 is a smaller gap and a better witness: the missing character is
the minus sign of a row outside the window.
2. THE T6DB none_cats PARAGRAPH REPORTED A NEGATIVE RESULT FROM A SETUP THAT
LACKED THE CONDITION -- and correcting it found a P2. The fixture was said to put
Product on Axis::None in one view and on Column in another. It did not:
add_category defaults to Column and nothing moved it, so all three views had
none_cats {_Index, _Dim} and the stated input to the hazard was absent.
Setting it explicitly failed the property on the first run, and not on a cell
value: Second loses BOTH Total columns when the host stands on Default.
rebuild_layout fills Model::formula_cache for the HOST's view, and grid_in on the
elsewhere path builds another view's layout against that stale cache -- the Total
cells are keyed without a Product coordinate, the other view asks for one, finds
nothing, and prune_empty drops the columns. Filed improvise-ac2z (P2). Not local:
grid_in takes &App and recompute_formulas needs &mut Model.
Guarded by a_hosts_none_cats_leak_into_another_views_formula_columns, a
characterization test whose message says that failing means the bug is fixed. The
property skips pairs whose none_cats differ and asserts compared > 0, so a future
fixture cannot skip everything and pass.
3. Smaller, all taken. "Nothing else fails" on the t6db injection was wrong: it
fails three tests, including the pre-existing prune-invalidation one -- the #78
fix had more coverage than it was credited with. And the 5ubs should_panic named
one thing and exercised another: pointed at a struct it reached only the
split_once branch, and all three panics shared one literal so `expected` could not
discriminate. The parse is now a `parse_expected` seam fed hand-written mutated
messages, each refusal with its own wording, plus a happy path.
1470 -> 1475 passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
20322ada6f | Merge gh/main (PR #91) into orchestrator main | ||
|
|
40ce4bcbee |
docs: the map pointed at the wrong file, and that manufactured a false claim
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
Root cause of PR #91's blocker 1, and it was mine. The row 'Change what a browser resolves without the host' named improvise-wasm-client/src/keys.rs. That file is 187 lines holding only BrowserKey — a KeyboardEvent flattened to an improvise key. The resolve function and KeyOutcome live in improvise-protocol/src/resolve.rs and are shared by both front ends. So the author consulted the map, opened the file it named, grepped it honestly, and wrote a true-and-irrelevant negative claim into context/ — the one place design-principles §6 says a reason must not be wrong, because nothing can find it later. A stale pointer does not merely fail to help. It manufactures confident false claims, because the reader has done exactly the work the map told them to do. The whole wasm-client inventory block was stale with it: keys.rs listed at 1597 lines against an actual 187 with a description of what resolve.rs does, render.rs 669 against 888, socket.rs 444 against 289, lockstep 606 against 1126 — and state.rs listed at all, when ClientState moved to improvise-protocol/src/client.rs. My earlier line-count refresh missed every one of them for the reason I documented at the time: the wasm-client/ shorthand prefix resolves to no path, so a path-walking refresh skips the block silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
a10e95eb5f |
fix(mode): settle Normal/RecordsNormal once, after the rebuild (review of #91)
Two blockers from the adversarial review. BLOCKER 2, a new wrong state this PR introduced: resolving `AppMode::Normal` per-effect reads the batch's OPENING layout (gotcha 23), so a batch that CROSSES — `:toggle-records-mode . enter-mode normal` — settled against the grid it started on and left `RecordsNormal` over a pivot grid. Base was correct there. The fix moves the resolution to one place, `App::settle_normal_mode` called after `rebuild_layout`, where there is no crossing case to get wrong. That is strictly simpler than what it replaces: `ChangeMode` is an ordinary `simple_effect!` again, `ReturnToNormalUnlessMoved` and `CloseWizard` write plain `Normal`, and the per-edit coverage question the review filed as finding 3 is gone because there is one edit. Removing its call site fails exactly three tests, workspace-wide. And the prose defended the staleness with an example that does not bear the weight: `:toggle-records-mode` works, but because `ToggleRecordsMode` and `ViewBack` overwrite the answer in both directions, so it exercises nothing. Replaced by a test that drives the crossing both ways. BLOCKER 1, a false claim in `context/`: "keys.rs emits no ChangeMode" is true and irrelevant — keys.rs holds only `BrowserKey`. The live browser key resolver is `improvise_protocol::resolve`, which emits `Command::ChangeMode(Normal)` in five places, and `reduce.rs` folds it with no resolution. The browser front end still has the sihr bug; it is LIVE, not latent, and it compounds because the client's only records-ness fact is the mode it just lost. Named honestly, with improvise-e5pf cited. Still not closeable here — a client holds a `GridCache`, not a `GridLayout`. Also the two corrections: the thpe test's doc comment no longer claims a discrimination it does not have, and the 2ud deferral notes that `registry.rs`'s `visible_cols: 8` is production, not a test helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
94b9408425 | Merge remote-tracking branch 'gh/main' into wave/command-tui-gaps | ||
|
|
46c3cfa158 |
docs: two things self-review turned up
The lockstep fixtures no longer fabricate a basis, but they do still assume a cursor at the origin: published_basis asks Sessions::connect for a window at offsets zero while client_of builds its grid at the cursor's offsets. They coincide in every fixture here. Recorded rather than closed. And the keypress path got improvise-jmjz's fix for free: compute_col_widths is one walk now rather than two, so App::apply_effects' own width computation roughly halved (134ms to 68ms at 18,000 cells). That is improvise-748f's held-key path and was not the ticket's subject. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
a449149998 |
docs: cite the two follow-up beads filed for this wave
improvise-6nsd (a prune-by-write renumbers the grid without moving layout_version) and improvise-26dm (the fourth length-not-completeness list, in ui/effect.rs). Both read back after bd create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
27399b6a28 |
kb+docs: prose that calls a change free is a claim, and it is checkable
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
Wave K's own prose said improvise-zv8t cost nothing — "no document that fitted before stops fitting" — reasoning that a tree splitting on a numeric category was refused as an artefact anyway. False: the refusal is conditioned on used_features, so a fit that never split on the column SUCCEEDED, and the wide columns are exactly the ones a fit has least reason to split on. Measured with a throwaway probe on both sides. The sentence sat one paragraph below the "permanent refusal" claim the previous wave had just corrected, in the same section, green on either reading. So prose_that_calls_something_permanent_forecloses_the_ticket has a sibling. A cost claim is as checkable as a permanence claim and is checked the same way: construct the document the claim says does not exist. Filed improvise-e8no for the cost, with a note not to close it by making the forcing conditional on cardinality — that is inference by another name. The branch's own adversarial round found a bug the branch introduced: forcing by column NAME while budgeting by column INDEX, and a document may legally name a category and a measure the same thing. Repaired as a data dependency rather than a moved line, so the injection is a compile error. GridLayout::new has now caused FOUR bugs. improvise-iot6 was the last production instance — every remaining call site is test code, which I verified rather than took on report: two of them sit inside cfg(test) blocks well below the module head and neither was named in the hand-back. Suite re-measured here: 1464 over 35 binaries, 1 ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
ae1570b508 |
docs(command): every reframe is NOT also a reshape, measured (improvise-9v0j)
9v0j asks for the general relation between view_frame and view_shape, which are separated by example and never stated as a property. One direction is the known asymmetry -- AddCategory of an empty category reshapes without reframing. The other was open, and the ticket guessed "probably yes, and if not, layout_version under-reports, which per gotcha 35 leaves a client wrong forever". Probed by driving each ModelCommand through apply_command and reading cursor_epoch() and layout_version(). Every command in reshaping_commands() moves both; a ClearCell that prunes a row out of a pruning view moves the cursor epoch and NOT layout_version. That is improvise-mcq4 working as designed -- a cell write changes data, not the view's configuration, and view_shape hashes configuration. So the proposed property test would FAIL and writing it would be wrong. What remains open is narrower and stated as a question rather than answered: the grid has renumbered while layout_version has not moved, so a client still claiming the old layout tier is accepted. The record tier covers a write to the pruned record; it says nothing about a write to a surviving one whose row index moved. Whether that is a hole in ADR 001's middle tier or correctly delegated is an ADR question. No code change: view_frame/view_shape live in ui/app.rs, which is another wave's ground this round. Also recorded: the probe needed the right fixture to say anything. Against app_with_two_pruning_views, seven of the twelve commands are no-ops because that document has no Product category, and a table of false/false rows reads exactly like a finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
822547a332 |
perf(project): one walk of the grid instead of three (improvise-jmjz)
grid_of evaluated the whole grid roughly three times per frame: compute_col_widths walked every cell for its text, then every cell again for the column totals it must size, and grid_of then evaluated the window a third time. Every result was discarded at the end of the frame. ui/viewport.rs::grid_body is now the one walk. It evaluates each cell once and derives all three answers from that value -- widen the column, add to the column's sum, and if the position is inside the requested window keep the CellValue for cell_display to dress. compute_col_widths survives as grid_body with an empty window, because the keypress path wants widths and no cells, and two width rules would be the disease improvise-edp names. MEASURED, orchestrator's machine, cargo bench --bench grid_projection, before and after: typical (400 cells) 1.840ms -> 0.687ms large (9,600) 172.6ms -> 50.0ms 18k (gotcha 41) 294.4ms -> 95.2ms pathological (90,000) 4.481s -> 1.320s About 3x, which is the number of passes removed -- the shape the acceptance criterion asked for rather than a speedup argued from the structure. Three things the ticket did not predict. The benchmark's col_widths column now measures something ELSE. It read 45-47% and now reads 64-82%, which looks like a regression and is the opposite: compute_col_widths IS the fused walk, so its share rose while the frame fell by two thirds. A ratio whose denominator moved is not comparable across the change, and the benchmark now says so. The windowing ratio did not improve, and that is correct: the pass that remains is exactly the one a viewport cannot narrow. improvise-i5tk's decline stands and the two findings are now cleanly separated. It is not solved, only three times cheaper -- 18k cells is still 95ms, 5.7 frames. The next lever is caching ACROSS frames. Guard: a_window_narrows_the_cells_and_neither_the_totals_nor_the_widths. Fusing three consumers into one walk creates a hazard the separate passes did not have, and the natural "optimisation" (iterate rows rather than 0..row_count) makes a column total the sum of the visible rows, silently. Watched fail with left: [1.0] against right: [300021.0]. All 1469 pre-existing tests unchanged, which is also the evidence the outputs did not move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
8c47377edf | Merge gh/main (PR #90) into orchestrator main | ||
|
|
3e3ebec25a |
kb+docs: an injection that does not fail names an untested path
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
Five facts from Wave J. The one worth reading is the first: check_the_checker is written as "inject the fault and watch it fail", and the case where it PASSES is the more informative result, because it is evidence about the test rather than about the code. On improvise-sihr, forcing ReturnToNormalUnlessMoved alone back to Normal left the characterization test green — which is how the untested path was found. An empty colon line takes ExecuteCommand's is_empty branch and settles through ChangeMode, so the effect settling every colon line that actually runs had no test. There are two tests rather than one because of it. Also records why ChangeMode could hide the bug at all: it was a simple_effect!, which declares a one-field newtype writing one place, so there was nowhere for a rule to live and the exit mode had to be a constant in the keymap and in each commit command. Gotcha 16 already warns the macro's kind argument must be mandatory; this is a second cost of the same shape. And that improvise-9c9o's ticket pre-diagnosed its own fix as a protocol change, and was wrong — the basis is App's own bookkeeping, so it was achieved by moving where the re-base happens. A pre-diagnosis is a hypothesis wearing the authority of a decision. Inventory: three protocol files were absent entirely — help.rs (925), client.rs (255) and resolve.rs, which at 2477 lines and 46 tests is the largest file in the crate. Filed improvise-e5pf: Command::ChangeMode(Normal) is on the wire and reduce_view folds it without the records resolution. Latent — keys.rs emits no ChangeMode and a view-half command never reaches the host's slice, both verified rather than assumed — and closeable only by enriching the projection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d42253ef38 |
review(gp,io): the zv8t forcing defeated its own budget, and two narrower fixes
Adversarial review round on the branch. One real finding, two prose/comment corrections, one fixture widened. F2 (the real one, and introduced by zv8t). `as_categories` forces by column NAME; `encoded_width` counts by column INDEX. A document may legally name a regular category and a raw measure the same thing — the repo already ships that fixture in `a_category_and_a_measure_of_the_same_name_are_refused` — and the measure column was then forced categorical by the name match while the preflight still counted it numeric by index. Measured at 40 records: preflight 41, actual 80. `no_ambiguous_column_names` ran AFTER `preprocess`, so the expansion the preflight exists to prevent was materialised and only then refused; at the default max_records that is a 20k-row one-hot matrix built before the error. That is `doctrine(guard_the_cost_not_the_result)` defeated by an inconsistency this ticket introduced — not the bounded approximation that `doctrine(a_budget_check_is_advisory_or_exact_and_must_say_which)` licenses. The repair is a data dependency rather than a moved line: `as_categories` runs the check and returns `Result<Overrides>`, so the Overrides `preprocess` needs cannot exist until the header has been accepted (`doctrine(an_invariant_is_made_unrepresentable_not_validated)`). The regression guard is therefore the SIGNATURE and the injection is a compile error — swapping the two statements back gives E0425, verified. What it does not cover is stated in the evidence rather than implied: a deliberate deletion of the check from `as_categories` is still possible; the accidental reorder is not. F5. The `encoded_width` comment claimed the preflight "must count it the same way". It does not, quite: `build_categorical` counts MISSING_CAT as a distinct value and the preflight does not, so it under-counts by one per category with an absent coordinate. Bounded by the exact check and pre-existing for genuinely categorical columns — but the comment asserted an identity that does not hold. F4. The evidence for the two spelling tests said that on the unfixed tree the text "could only have been `Expr::Ref(\"Year\")`". There is no text on the unfixed tree at all — `fitted_formula` refuses before emitting. The argument for the assertion survives; the stated reason was a guess about a branch that does not run, and an evidence file is the one place that should be exact. And the iot6 fixture: `prune_empty` prunes BOTH axes and the test asserted rows only. `Feb` is now the empty column, the precondition is a pair on each layout, and the CSV header is asserted to carry `Jan` alone. 1464 passing, up from 1463. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
e3fcdc8bc6 |
test(lockstep): take the fixtures' basis from the server, not from a recipe (improvise-1l9o)
furnished_client and client_ready_to_edit built their BasisCache by walking the client's own grid for Record cells and calling basis::published(0, 0, records) with every version hard-coded to zero -- a small re-implementation of project.rs::basis_update's record selection, in a file whose entire purpose is to rule out re-implementations. Both now take the Projection::Basis out of a real Sessions::connect snapshot. The reason the ticket calls it a nit is the reason it needed a test: a fresh App really is at generation 0, layout 0 and every record at 0, so the fabrication and the publication are indistinguishable on every existing fixture -- all 23 lockstep tests pass under either. So the change alone is structural and untested by construction. the_fixtures_basis_tracks_a_host_whose_layout_has_moved moves a tier first and then asserts the fixture's claim names the host's current tiers; re-fabricating the two document tiers fails it (0,0) against (0,1) and fails nothing else. Worth recording: SetNumberFormat was the first attempt and does not move layout_version, although it declares Model(LayoutGeometry). layout_version is OBSERVED from view_shapes rather than read off the declaration (improvise-pdvm), and a number format is not part of a view's shape hash. Classification is not a promise about the counter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
c186909de6 |
docs: the review's two findings on this branch
`Command::ChangeMode(AppMode::Normal)` is on the wire and `reduce_view` folds it without the resolution, so a client that emitted it would keep improvise-sihr's behaviour. Not live — `keys.rs` emits no `ChangeMode`, and `apply_command` never folds a view-half command into the host's own slice — and not closeable here: a client holds a `GridCache`, not a `GridLayout`, so records-ness is not a fact it has. Named as a gap, with the shape that would close it. And the improvise-9c9o re-base costs one extra `view_frame` per batch that both switches and moves geometry, which the prose now states rather than leaving for the next person to measure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d8df9196f5 |
decide(protocol): keep ColWidths diffed against the mirror, for a different reason (improvise-44ad)
The move -- diff col_widths against the label-folded cache, as hkt6 already obliged for totals -- is DECLINED, and the two things learned are worth more than the change would have been. THE TICKET'S STATED COST DOES NOT EXIST. It says the move breaks a_client_on_a_socket_is_seeded_and_gets_its_edit_back, and that that assertion is gotcha 33(c)'s standing evidence, so rewriting it inside a change 33(c) guards is the improvise-q0vb / PR #12 shape. Measured by deleting the re-fit from fit_to_labels: three existing unit tests fail and the socket test is not one of them. What it actually asserts is that a reframe CARRIED widths -- a statement about the producer's diff, not about the consumer's re-fit. 33(c) is triply guarded elsewhere. THE REAL REASON IS COUPLING. The widths a label-fold invents are each column's own label width: an approximation the consumer applies until the authority catches up. Diffing the producer's output against it makes what the server sends depend on a fallback rule defined in the client -- invisible today, silent the day a second client implements the fold. The redundant ColWidths per reframe buys out of that, and over-sending is the direction Dirty::of is deliberately sloppy in. Landed anyway: the half the ticket calls "the better test either way". a_client_never_holds_widths_of_a_different_length_from_its_labels asserts the invariant 33(c) is really about, at EVERY step of a fold that widens before its widths arrive and narrows before they shrink -- not merely at the end, which is where a message-count assertion looks. Watched fail at step 0 with the re-fit deleted. The totals/col_widths asymmetry is now stated rather than discovered: totals is fold-relative because hkt6 found a real hole; col_widths has none, the re-fit being a safe fallback rather than a stale value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
e50473df71 |
docs(gp): fitted_formula takes Features, not Overrides
Second self-review finding in the same paragraph, and the same class: §11 said "`crate::dataset` and `fitted_formula` take the caller's `Overrides`". Only the first does. `fitted_formula` takes a `Features`, which `Features::of` read off whatever `Dataset` it was handed — the Overrides are two steps upstream. The conclusion was right and the mechanism was wrong, which is the kind of sentence that survives review because the reader checks the conclusion. Rewritten to name the actual chain. Also verified while checking it, and worth recording because it looked like a drift and is not: the two test call sites still passing `Overrides::default()` to `crate::dataset` describe the SAME dataset production now builds. In `preprocess` the `present.is_empty()` drop happens BEFORE `ov.force` is consulted, so forcing an entirely-missing column categorical changes nothing — `an_empty_category_before_a_measure_does_not_break_the_role_check`'s precondition is as strong as it reads. The other site's fixture has only Label categories, so `category_columns` is 0 and the two Overrides are literally equal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
877d3e193b |
refactor(help): a chord is recognised by its modifiers, not its spelling (improvise-thpe)
`compact` tested whether a label it had just formatted began with "Ctrl-" or "Alt-". No false positive today — the reviewer enumerated every `format_key_label` output — so this is the shape, not the behaviour. `Hint::keys` is now `(KeyCode, KeyModifiers)` and `compact` takes the pairs, so the string is not in scope to parse; `Hint::labels` and the help pages format last through the one spelling in `keymap::key_label`. The regression guard is the signature, as it is for `Checked`. `Keymap::labels_for` — the "throw the modifiers away" step the ticket names — has no caller left and is deleted: a consolidation that keeps its old path keeps the defect available. The rule also got wider and total over the bitset: a chord is any modifier beyond shift. Injected a temporary `KeyModifiers::SUPER` spelled `Super-x` and ran it — the old filter keeps it, `compact` drops it. Binding dump byte-identical across the change (4406 lines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
c4bf74608f |
fix(protocol): a group run is opened by a header, not by a name (improvise-h0iv)
group_runs coalesced adjacent positions by group NAME, so two runs under two different headers sharing a name read as one and both front ends drew a single span across the boundary. THE TICKET'S PROPOSED FIX DOES NOT WORK, and that is worth more than the fix. h0iv suggested comparing the (cat, name) pair, FrameGroup carrying cat for exactly that reason. But its own reachability case is ONE category -- Month grouped into Q1 nested under Region, giving North/Q1 and South/Q1 adjacent -- so both headers are (Month, Q1), equal as values, and the pair separates nothing. A test pins that so the next reader does not re-propose it. What does separate them is that they are two distinct FrameEntry::Group entries, which groups() already borrows from self.entries: std::ptr::eq on the header is the literal statement of "the same header opened both". THE PRODUCT QUESTION, decided here and reversible in one comparison. Whether two adjacent same-named runs are one span a user wants spanned is a matter of taste, which is why #77 filed rather than fixed. What settled it is not the span but the indicator: the existing wasm-client test merged Month/Q1 (expanded) with Fiscal/Q1 (COLLAPSED) and its own message said the merged span is drawn with the first one's indicator -- a collapsed group rendered as expanded, with no affordance saying otherwise. A span boundary is taste; a wrong collapse glyph is a wrong fact. That test's justification was also spent: it said the rule must be name-only "because that is what src/ui/grid.rs uses", and since improvise-8qlr grid.rs calls group_run_heads. There is no second copy left to agree with, which is the general point -- a consolidation's frozen behaviour reads as decided afterwards, and nothing says it was frozen only for the duration of the move. The agreement fixture gains the case it could not see: two_level_rows minus the ungrouped Sep, so the two Q1 runs abut, end to end through a real GridLayout and both renderers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
e6e39a03ff |
review(gp): the zv8t prose claimed a cost it has, and the cost is measurable
Self-review of
|
||
|
|
8e9842b89f |
fix(cursor): the frame basis follows active_view, not the next geometry effect (improvise-9c9o)
PR #78 round 2's `was == self.view_name()` guard fixed a spurious invalidation and introduced its mirror: the first geometry effect after a mid-batch `SwitchView` skipped the check entirely, because there was no basis for the new view yet, so the code re-based and skipped. Under-reporting, which gotcha 35 says is the expensive side — a stale cursor has nothing in the stream to correct it. The re-base now happens at the switch, in the loop, as soon as the name moves, so by the time the geometry arm runs the name and the basis agree by construction and the guard is gone as unreachable. `SwitchView` stays `ViewOnly`: the basis is `App`'s own bookkeeping and none of it is on the wire, so this is not the protocol change the ticket feared. Also deletes a fixture line in the sibling test that was a complete no-op — `views["Second"].clone_into(&mut views["Default"].clone())` clones into a temporary that is dropped — and replaces it with the assertion it was pretending to be. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
9b81324d6a |
fix(protocol): a frame that lost its origin must not claim to lead the axis (improvise-1wl0)
AxisFrame::first carried #[serde(default)], so a frame whose first was dropped in transit deserialized as 0 -- not an absence but the claim "this frame leads the axis", which is precisely what body_from acts on. A scrolled browser would draw the carried group header above its first row, silently. Dropping the attribute rather than reaching for Option<usize> or a two-variant enum, and the reason is about the wire rather than the type: there is exactly one production producer, reading window.start, so a frame is never partially constructed and "unknown" is not a state an honest peer can be in. Making it representable would only oblige body_from to answer the same question one level down. A missing field is now a loud parse failure at the transport. Same argument that gave Basis no Default (gotcha 43). The Default derive still yields first: 0 and that is not the same hole -- a default AxisFrame has no entries, so the claim is vacuous either way. Test written first and watched fail, printing the forged first: 0. It asserts the key was present before removal, that the mutated value is still a well-formed AxisFrame minus one field, and that the error kind is "missing field" -- the three ways this check could otherwise pass for the wrong reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
99cfb7fb41 |
fix(gp): a numeric- or date-valued category is spellable (improvise-zv8t)
`fit_measure` handed `preprocess` an `Overrides::default()`, so it never said which header columns are document categories — although the very next line told `Features::of`. `preprocess` then inferred Numeric -> Date -> Categorical from the values, so a `Year` category with items 2024/2025 became a numeric column and a category of ISO dates became a date column, and both were refused as encoding artefacts with `Year IS 2024` spellable all along. `fit_measure` now builds its `Overrides` from the same `header[..category_columns]` slice it already hands `Features::of`, forcing each to `Forced::Categorical`, so the one place the document's own answer was known is now the only place either caller reads it from. `Forced::Categorical` already existed; no formula-language change was needed. Three things follow, and the last is a cost. The date decline in gp.md §11 now means what it says. A `DatePart` could arrive over a *category* column by exactly this accident, so that decline was narrower than it read; it now covers a genuine date measure and nothing else. The `Artefact` arms stay and stay correct. `crate::dataset` and `fitted_formula` take the caller's `Overrides`, so a caller that does not force can still produce a `Numeric` role over a category column. The arm is no longer reachable from `fit_measure`, which is the only production entry point — it is not dead code, and deleting it would derive the classification from one caller's habits. Forcing widens the encoding, deliberately: a two-item `Year` is now two one-hots rather than one numeric feature, and a two-hundred-item one is refused by `MAX_FEATURES` where before it was refused one stage later as an artefact. Both refuse, so nothing that fitted before stops fitting; what changes is which guard speaks, and the width guard's message names the repair. `encoded_width` had to learn the same slice for that to hold, or it counted such a column as one feature and handed the refusal to the exact check *after* materialising the matrix — the side of the cost the preflight exists to stay on (§14). Evidence. The two spelling tests needed no fault injected: on the unfixed tree `fit_measure` refuses with the two real messages the ticket named. Each fixture gives the model one regular category, no other feature column, and a target that category alone determines — because the refusal is conditioned on `used_features`, so a fit whose champion happened to be a bare leaf would be accepted and prove nothing. Asserting `formula.raw` contains `Year IS` rather than merely `is_ok()` closes that in both directions. The preflight test did need one: dropping the `col >= category_columns` clause makes it fail by name, with the *exact* check speaking where the preflight should. The prose this corrects was itself the ticket's subject. improvise-wd6n's §11 called `Artefact::NumericCategory` a permanent refusal that "no vocabulary closes", which is false and is what filed improvise-zv8t. §11 and the gp test-evidence entry are both updated to say it is closed rather than open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
6ee8ff389d |
fix(io): CSV export writes the rows the view prunes (improvise-iot6)
`render_csv` built its layout with `GridLayout::new`, which does not apply `prune_empty` — only `with_frozen_records` does. So `:export` in the TUI and the CLI export path both wrote rows the grid does not draw. User-visible, in production, and shipping for as long as the two constructors have differed. The fourth instance of this trap; the third was `project::grid_in` in PR #78, and this is that fix line for line. The fixture is the difficulty, and the ticket says so. The sibling property written for improvise-t6db passed on the buggy tree in its first version, because it made its two views differ by turning pruning *off* — vacuous, since with nothing to prune both code paths agree. So this one contains a row that is actually empty, leaves `prune_empty` ON, and asserts the precondition in both directions before exporting: `with_frozen_records` draws 2 rows where `new` holds 3. If pruning ever becomes a no-op the precondition fails and says so, rather than the assertion passing vacuously. No fault needed injecting — the bug was live. On the unfixed tree the test fails with `["Food", "Gas", "Rent"]` against `["Food", "Gas"]`. None of `export.rs`'s three pre-existing tests mentions pruning at all, which is exactly why this shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
4e5f9f3ab8 |
test(protocol): make the cyclic all() prove it reaches every variant (improvise-5ubs)
AppMode::all() and Rejection::all() walk a total cyclic after(), and both doc comments claimed a new variant could not be omitted. Half true, wrong half: answering after() FOR a new variant is compiler-forced, pointing an existing arm AT it is not, so a variant nothing names is never walked -- measured by the PR #78 reviewer as all().len() == 17 against 18 variants, suite green. Closing it needs an enumeration independent of after(). Stable Rust has none for an enum with data -- variant_count is nightly, a derive macro is a new proc-macro crate, and strum's EnumIter cannot construct AppMode::ItemAdd or Editing at all. serde can: the Deserialize derive already builds the complete variant-name list from the enum definition and publishes it in its "unknown variant ... expected one of ..." message. variants.rs reads that back and diffs it against the names the cycle reached, each taken off the variant's own serialization rather than off Debug. The helper is defensive because an unparsed message would return an empty list, and an empty list passes every cycle; a should_panic test points it at a struct and asserts the message, not merely that it panics. Both overclaiming doc comments replaced. The fourth instance of this shape -- assert_eq!(cases.len(), 13) in ui/effect.rs -- is over a hand-built list of effects rather than enum variants, so serde has nothing to say about it; left filed rather than bodged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
6782e8555c |
fix(mode): a minibuffer opened over a records grid returns to it (improvise-sihr)
`AppMode::Normal` is now resolved through `App::normal_mode()` at the moment the mode is set, so no keymap binding and no commit command carries an exit mode. Two readers, one function. The ticket proposed carrying the entry mode on `MinibufferConfig`; declined, with the reason in context/test-evidence/command.md — carried generically it also returns `CategoryAdd` to `CategoryPanel`, and carried for three modes it is the one-arm shape the ticket itself argues against. The binding table is untouched, so `doctrine(binding_dump_diff)` has nothing to diff: what was lost was the runtime mode, not a binding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
20e9bd4106 |
test(project): pin host-view independence for grid_in (improvise-t6db)
PR #78 fixed grid_in to build a non-active view with with_frozen_records rather than GridLayout::new, which skips prune_empty, and landed with no test of its own. This is the general property: which view the host stands on is not an input to any client's grid. The fixture is the load-bearing part. The reviewer's first formulation made the two views differ by turning prune_empty OFF in the second, which passed on the buggy tree -- with nothing to prune, both constructors agree. So the fixture gives one region no cell at all, leaves all three views pruning, and asserts every view really is two rows before comparing anything. Widened while writing it: an aggregate formula, so the views also differ in none_cats, which grid_in likewise does not rebuild for the elsewhere path. That asymmetry is argued and NOT observed -- see context/test-evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
ada16d211a |
kb+docs: the Pages artifact is reaped by a foreign successful deploy
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
improvise-864a diagnosed over all 100 push-to-main runs rather than the three the ticket named — and the three-run framing was mine, a sampling artefact that made a two-day, sixteen-failure pattern look like a concurrency race between queued deploys. 12/12 artifact-not-found failures had another run's SUCCESSFUL deploy complete inside their own upload-to-deploy window, against 2/56 successes. The decisive observation was a negative one already sitting in a log: a run seeing its own harness-serve and wasm-bundles but not its own github-pages. Same run, same token, same call, same retention — which kills flakiness, eventual consistency, retention, token scope and a v3/v4 mismatch together. One clause of the ticket's own correction is falsified: PR #46's guard aimed at the right quantity for the wrong reason. It still must not ship, because the association is not a mechanism and one run survived a foreign deploy in its own window. improvise-ijql closed additive. The author argued against the precedent I pointed it at and was right: a provenance line qualifies an answer the run gave, a disclaimer qualifies one it did not, and exit 0 is what CI branches on. The rule is now stated over the exit code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
9b22965fdf | Merge gh/main (PR #88) into orchestrator main |