From bb69a157e097f599ac35e03bf050d3e17cddccbb Mon Sep 17 00:00:00 2001 From: "Grok Bot (Ed Langley)" Date: Mon, 21 Sep 2026 13:33:27 -0700 Subject: [PATCH] beads: file web smoke bugs from external Grok Bot hunt OPFS handshake flake, silent seed/doc failures, narrow-grid clip, Esc leaving formula panel open, and Create-cell silent decline. --- .beads/config.yaml | 4 ++-- .beads/issues.jsonl | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.beads/config.yaml b/.beads/config.yaml index 243a8fc9..aae9d1bb 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -51,10 +51,10 @@ events-export: true # - linear.api-key # - github.org # - github.repo -sync.remote: "git+ssh://git@git.fiddlerwoaroof.com/u/edwlan/improvise.git" +sync.remote: "git+https://github.com/fiddlerwoaroof/improvise.git" export: auto: true git-add: false include-memories: false validation: - on-create: warn + on-create: warn \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 01dd8e78..f7a4b466 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"improvise-16af","title":"[bug] In-page worker intermittent blank grid: OPFS SyncAccessHandle / NotAllowedError","description":"## Summary\nDefault `/improvise/` (in-page worker) intermittently shows a blank grid. Status flips between `in-page worker · demo.improv` and `the in-page worker did not answer`. Console reports OPFS `createSyncAccessHandle` NotAllowedError / AccessHandle conflict.\n\n## Repro\n1. Open https://fiddlerwoaroof.github.io/improvise/ (or local web/) in Chromium.\n2. Optionally open a second tab on the same origin/doc, or reload after a prior session left a handle open.\n3. Observe blank grid and/or watchdog message; DevTools console shows SyncAccessHandle / NotAllowedError.\n\n## Expected\nWorker starts, handshake completes, demo grid paints. Concurrent tabs either share safely or fail with a loud, specific \"document open elsewhere\" status.\n\n## Actual\nIntermittent blank UI; generic watchdog text; OPFS exclusive-access conflict in console.\n\n## Evidence\n- Browser smoke (external Grok Bot agent for Ed Langley), 2026-09-21 PT.\n- `crates/improvise-worker-server/src/bindings.rs`: `storage.attach(\u0026path).await?` / OPFS path.\n- `crates/improvise-io/src/opfs.rs`: `attach` acquires sync access handle.\n- `web/app.js` / `web/shell.js`: handshake watchdog arms when peer never answers.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude). Do not claim this is a black-hole key issue.\n","status":"open","priority":1,"issue_type":"bug","created_at":"2026-09-21T20:32:37Z","created_by":"box","updated_at":"2026-09-21T20:32:37Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-dj1l","title":"kb-merge loses and corrupts clauses: keys are matched by unification","description":"Found by review of PR #68 after it merged. Two faults with one root: scripts/kb-merge.pl matches clause keys by UNIFICATION rather than variant equality.\n\n1. Intra-delta loss. check_self_consistent compares keys with ==, but findall hands back fresh variables for every collected key, so two clauses with variant heads are never == and the duplicate gate passes. foldl_merge then matches by unification, so the second clause removes the first. Two clauses in, one out, reported as success. Reproduced: a delta of two rules over one predicate went 536 before, 2 in delta, 537 after.\n\n2. Destruction and MUTATION. Adding 'probe_fact(X) :- doctrine(X,_).' to a kb holding probe_fact(a). and probe_fact(b). deleted probe_fact(a) AND wrote the rule as 'probe_fact(a):-doctrine(a,A)' -- X was bound by the match, so the clause written is not the clause supplied. The round-trip verify cannot see this: it compares the file against an intended list already corrupted by the same unification.\n\nFix: same_key/2 (copy_term + numbervars) in has_key, conflicts and absent; canonicalize before duplicate detection; a RULE keys on the whole clause, since two clauses with one head shape and different bodies is ordinary Prolog; and check_total/2 asserts the arithmetic the tool already PRINTS -- 'N in delta' beside a total that did not account for N was visible in its own output twice and nothing compared them.","status":"closed","priority":1,"issue_type":"bug","assignee":"claude-agent-scripts","owner":"el-github@elangley.org","created_at":"2026-09-20T23:26:47Z","created_by":"claude-agent-scripts","updated_at":"2026-09-20T23:35:13Z","started_at":"2026-09-20T23:26:51Z","closed_at":"2026-09-20T23:35:13Z","close_reason":"fixed in PR #69, each proved by injection","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-vz54","title":"node --test with an unmatched glob passes with zero tests","description":"Both CI node steps use a shell glob: `node --test web/tests/bundle/*.test.js` (browser job) and `node --test web/tests/*.test.js` (js job). CI runs them under `shell: /usr/bin/bash -e {0}`. Bash leaves an unmatched glob as a literal; node 22 treats it as a glob, matches nothing, and reports success with 0 tests and exit 0. Demonstrated. Rename or move either directory and the gate goes green having asserted nothing.\n\nPre-existing, not introduced by PR #63 - but #63 is the moment web/tests/bundle/ stopped being co-located with the job that builds its inputs, which makes a future move more likely.\n\nNote the local-shell trap that hid it: zsh ABORTS on an unmatched glob, so the probe only reproduces under bash. Local shell != CI shell is a fault-injection axis in its own right.\n\nFix: guard the step so an empty match fails loudly - a non-empty check on the expansion, or a minimum-test-count assertion. Prove it by renaming the directory and watching the step fail.","status":"closed","priority":1,"issue_type":"bug","assignee":"claude-agent-scripts","owner":"el-github@elangley.org","created_at":"2026-09-20T21:05:55Z","created_by":"orchestrator","updated_at":"2026-09-20T23:35:10Z","started_at":"2026-09-20T23:22:05Z","closed_at":"2026-09-20T23:35:10Z","close_reason":"fixed in PR #69, each proved by injection","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-lltt","title":"[bug] A view-scoped ModelCommand names no view, so it lands on the host's","description":"Opened by improvise-jytm, which made `ViewState::active_view` authoritative per\nclient. The view-configuration half of `ModelCommand` — SetAxis, CycleAxis,\nTransposeAxes, SetPageSelection, HideItem, ShowItem, ToggleGroup, TogglePruneEmpty,\nSetNumberFormat — acts on \"the active view\", which is now an ambiguous phrase.\n\n`Sessions::apply_model` routes with no SessionId (gotcha 38, correctly: the change\nis addressed to no one session, because a `View` is document state shared by every\nclient sitting on it). But it must still name WHICH view, and today it names it\nimplicitly: `apply_command` runs the effect against the `App`, and the effect reads\n`App::view()`, which is the HOST's slice. So a connected client on another view\nmoves the host's view rather than its own.\n\nBefore jytm every client was on the same view, so the implicit answer was right.\nIt is now wrong exactly when two clients are on different views, which is the\ncapability jytm exists to add.\n\nThe honest fix is for those variants to name their view — a protocol change — so\nthe command carries the operand rather than borrowing an ambient one. Rejected\nduring jytm: retargeting `app.view_state.active_view` around `apply_command` at the\ndoor. It makes the host's own UI slice a parameter channel, and `reset_cursor`/\n`cursor_epoch` would then write the asker's key into the host's cursor map.\n\nNote the `_Drill`/`_Records` virtual views make this sharper than it looks: a host\nin records mode is on a view no client can name.\n\n## Steps to Reproduce\n\n1. Two clients, A and B, on one document with views \"Default\" and \"Second\".\n2. B sends `Command::SwitchView(\"Second\")`. Its mirror moves; the host's App stays\n on \"Default\" (correct, per improvise-jytm).\n3. B sends `Command::Model { cmd: ModelCommand::SetAxis { category: \"Region\",\n axis: Row } }`.\n4. The axis moves on \"Default\" — the view B is not on — and B's next projection\n shows nothing changed while A's shows a change A did not ask for.\n\nNot reachable from the TUI, which is the only client that drives the App's own\nslice, and not reachable from `web/app.js` today because it sends no SwitchView.\n","acceptance_criteria":"The view-configuration ModelCommand variants name the view they act on, so a connected client's SetAxis moves the view that client is on and not the host's. A test drives two sessions onto different views and asserts an axis change by one is invisible to the other. Deciding NOT to fix is also an acceptable close, but then gotcha 38 must say so.","notes":"THE TRIGGER, added after the adversarial review of PR #48 (2026-09-20). This is deferrable, but it is deferred on a CONDITION rather than on a judgement, and the condition is one line of somebody else's future work away. It is unreachable today ONLY because no shipped client emits Command::SwitchView. That is the whole of the argument. The browser already emits view-configuration model commands -- keys.rs resolves a keypress to ModelCommand::TransposeAxes locally and sends it -- so the machinery on the other side of the ambiguity is live. THE DAY ANY WAVE WIRES A VIEW SWITCH INTO THE BROWSER, THIS GOES LIVE ON THE NEXT TRANSPOSE KEYSTROKE, AND SILENTLY: a client on Second presses transpose, the axes of Default move instead, that client's grid does not change and every other client's does. Two consequences. (1) Do not close this by arguing it is unreachable -- reachability is the thing that is about to change, and gotcha 47 already records that 'unreachable today' was the argument under which the lost update CheckedBatch later fixed had shipped. (2) There is a cheap interim guard worth landing before the real fix: assert that no shipped browser client emits Command::SwitchView, a test over improvise-wasm-client's key resolver. It turns the silent failure into a loud one at exactly the moment the premise stops holding, which is the only moment anybody would otherwise not be looking.\nTHE PREMISE IS ALREADY FALSE (verified 2026-09-20 at 585f25b by wave3 protocol agent, by enumerating the resolver rather than reasoning about it).\n\nThis ticket's own note says it is deferred on a CONDITION -- 'it is unreachable today ONLY because no shipped client emits Command::SwitchView' -- and asks for an interim guard that asserts exactly that. I wrote the guard. IT FAILED IMMEDIATELY. Two live paths, both in the browser's own resolver at crates/improvise-protocol/src/resolve.rs:359-368:\n\n VIEWS: switch-view-at-cursor -\u003e switch_view_at() -\u003e Command::SwitchView\n VIEWS: create-and-switch-view -\u003e CreateView + Command::SwitchView\n\nand the other half is live too: resolve.rs:331 composes ModelCommand::TransposeAxes locally. So the exact scenario the note describes -- 'a client on Second presses transpose, the axes of Default move instead, that client's grid does not change and every other client's does' -- is reachable TODAY from the shipped browser view panel. It is not a future risk; it is a live P2-turned-P1 bug.\n\nEscalated to P1 on that evidence.\n\nThe guard landed anyway, as the_keys_that_compose_a_switch_view_are_these in resolve.rs, but as an ASSERT_EQ against those two names rather than an assert-empty -- it pins reality and fails on a third live path, and its message says the list is not an exemption list. When lltt is fixed the list goes empty and the test goes with it.\n\nNOT FIXED HERE, deliberately: the honest fix is the protocol change this ticket describes (the nine view-configuration ModelCommand variants naming their view), which means every view-writing effect taking a view name instead of calling App::view_mut(). That is a whole work unit and it should not be smuggled into one alongside nine other tickets.\nNOT CARRIED BY PR #78. Releasing the claim so the tracker stops implying someone is on it.\n\nWHAT #78 CONTAINS: only the DETECTOR. the_keys_that_compose_a_switch_view_are_these (improvise-protocol/src/resolve.rs) enumerates every binding every mode can reach and asserts the set composing Command::SwitchView is exactly {switch-view-at-cursor, create-and-switch-view}. That is what overturned this ticket's deferral premise and got it escalated to P1. It fails if a THIRD live path appears; it does not fix any of the two that exist.\n\nWHAT #78 DOES NOT CONTAIN: the fix. No ModelCommand variant names a view; no effect takes a view name; App::view_mut() is still what every view-configuration effect writes through. The bug is exactly as described in the original report.\n\nWHY NOT: the honest fix is the protocol change this ticket describes -- the nine view-configuration variants (SetAxis, CycleAxis, TransposeAxes, SetPageSelection, HideItem, ShowItem, ToggleGroup, TogglePruneEmpty, SetNumberFormat) naming their view, which means every view-writing effect taking a view name instead of reading the ambient App::view_mut(). That touches ui/effect.rs broadly and ui/reduce.rs's mapping, and it is a work unit. Landing it inside a PR already carrying nine other tickets would have got it the least review of anything in the PR, which is the wrong trade for a P1 in the concurrency layer.\n\nONE THING FOR WHOEVER TAKES IT, which the ticket does not say: the bug is already written into a PASSING TEST'S COMMENT. a_reshape_of_one_view_leaves_a_client_on_another_view_alone carries 'Aimed at the host's own view... HideItem writes view_mut()' -- a correct description of the defect, read for as long as it has been there as a fixture note. Worth grepping for siblings of that shape before starting.\n\nUnclaimed, still P1, still open.","status":"open","priority":1,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-20T18:05:49Z","created_by":"wave2-e-active-view","updated_at":"2026-09-21T04:10:12Z","started_at":"2026-09-21T03:28:15Z","labels":["area:active-view"],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -40,6 +41,10 @@ {"_type":"issue","id":"improvise-km8","title":"1.2 Fix Cargo.toml metadata","description":"Add missing package fields: description, repository, homepage, documentation, readme, keywords, categories, license. Description: 'Terminal pivot-table modeling in the spirit of Lotus Improv'. Keywords: tui, pivot, spreadsheet, data, improv. Categories: command-line-utilities, visualization.","status":"closed","priority":1,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:06:48Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:20:17Z","close_reason":"Metadata already present: description, license, repository, homepage, readme, keywords, categories all populated.","dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"improvise-902","title":"1.1 Audit and remove or update context/SPEC.md","description":"Compare context/SPEC.md against actual code. If spec contradicts code significantly, delete it. Optionally move salvageable conceptual content to docs/design-notes.md with a staleness disclaimer. Do not try to bring spec into sync.","status":"closed","priority":1,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:06:46Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:10:05Z","close_reason":"Audited SPEC.md against code. Found minor inaccuracies (storage internals, wizard step count, search mode representation) but no major architectural contradictions. Deleted SPEC.md since it was largely redundant with repo-map.md and design-principles.md. Salvaged product vision and non-goals into docs/design-notes.md with staleness disclaimer.","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"improvise-tik","title":"Phase 1: Repository hygiene","description":"Raise the quality floor of what a stranger sees when they land on the repo. Subtasks cover spec audit, Cargo.toml metadata, publish-readiness, CSV audit, and example files.","status":"closed","priority":1,"issue_type":"feature","owner":"el-github@elangley.org","created_at":"2026-04-09T04:05:45Z","created_by":"Edward Langley","updated_at":"2026-04-09T07:22:34Z","closed_at":"2026-04-09T07:22:34Z","close_reason":"All 5 subtasks complete: SPEC.md audited, Cargo.toml metadata fixed, publish dry-run clean, CSV audit done, example files created.","dependencies":[{"issue_id":"improvise-tik","depends_on_id":"improvise-2fr","type":"blocks","created_at":"2026-04-08T23:37:31Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-902","type":"blocks","created_at":"2026-04-08T23:37:30Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-bv1","type":"blocks","created_at":"2026-04-08T23:37:32Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-km8","type":"blocks","created_at":"2026-04-08T23:37:30Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-n1h","type":"blocks","created_at":"2026-04-08T23:37:32Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-tg27","title":"[bug] Narrow window clips grid columns without usable horizontal scroll","description":"## Summary\nOn a narrow window the grid clips horizontally with no obvious horizontal scroll affordance, so rightmost columns are unreachable without widening the window.\n\n## Repro\n1. Open the web client with a demo that has several columns.\n2. Shrink the viewport width until columns overflow.\n3. Try to reach clipped columns (scrollbars / trackpad).\n\n## Expected\nOverflowing grid content is reachable via horizontal scroll (or another explicit navigation), consistent with `#grid { overflow: auto }` intent.\n\n## Actual\nContent appears clipped; horizontal overflow is not obviously scrollable in the smoke environment (may be a flex/min-width layout trap swallowing overflow).\n\n## Evidence\n- Browser smoke (external Grok Bot agent for Ed Langley), 2026-09-21 PT.\n- `web/style.css`: `#grid { overflow: auto; }` / main flex layout — suspected min-width:auto flex child clipping.\n- Closed `improvise-jq8l` was about `scrollIntoView` for selection, not narrow-window column clipping — different bug.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude).\n","status":"open","priority":2,"issue_type":"bug","created_at":"2026-09-21T20:32:39Z","created_by":"box","updated_at":"2026-09-21T20:32:39Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-x0dc","title":"[bug] Esc from FORMULAS leaves formula_panel_open so panel chrome stays visible","description":"## Summary\nAfter opening the formulas panel with `F`, pressing Esc (or Tab→normal) returns mode to NORMAL but leaves formula-panel chrome visible because `formula_panel_open` stays true.\n\n## Repro\n1. Open web client with a document that has formulas.\n2. Press `F` — formulas panel opens and mode is FORMULAS.\n3. Press Esc (or the panel Tab binding that runs `enter-mode normal`).\n4. Mode indicator shows NORMAL; formulas sidebar/chrome may still be visible.\n\n## Expected\nLeaving the panel mode closes the panel (or Esc mirrors `F`'s toggle-shut: `SetPanelOpen { open: false }` + `ChangeMode(Normal)`), matching TUI intuition that Esc dismisses the panel.\n\n## Actual\n`PanelKeymap` binds Esc/Tab to `enter-mode normal` only. `entered()` emits `ChangeMode` and does not clear `formula_panel_open` / `view_panel_open`. Render still emits `formulas` rows while the flag is true (`render.rs` `formula_rows`), so `paintPanel` keeps the aside visible. By contrast, a second `F` correctly toggles shut (`SetPanelOpen false` + Normal) — see `a_focused_panel_toggles_shut` in `resolve.rs`.\n\n## Evidence\n- Browser smoke (external Grok Bot agent for Ed Langley), 2026-09-21 PT.\n- `crates/improvise-protocol/src/keymap.rs` `PanelKeymap::new`: Esc/Tab → `enter-mode normal`.\n- `crates/improvise-protocol/src/resolve.rs` `entered()` — ChangeMode only.\n- `crates/improvise-wasm-client/src/render.rs` `formula_rows` gated on `formula_panel_open`.\n- `web/app.js` `paintPanel` hides only when `rows.length === 0`.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude). Category panel uses an explicit `category_panel_open` bool painted by the page; formula/view use empty-vec-as-closed, which makes this stickier.\n","status":"open","priority":2,"issue_type":"bug","created_at":"2026-09-21T20:32:39Z","created_by":"box","updated_at":"2026-09-21T20:32:39Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-9cma","title":"[bug] Invalid ?doc= yields blank UI and only generic watchdog text","description":"## Summary\nInvalid `?doc=` values (e.g. empty, `///`) produce a blank UI and only the generic handshake watchdog message — no specific error that the document name/path was rejected.\n\n## Repro\n1. Open `/improvise/?doc=` or `?doc=///`\n2. Observe blank UI; status eventually like `the in-page worker did not answer` (or similar watchdog text).\n\n## Expected\nA specific, actionable status: invalid document name / path rejected (and what is allowed).\n\n## Actual\nWorker start fails or never answers; UI collapses to the same generic watchdog path used for OPFS/browser blocks.\n\n## Evidence\n- Browser smoke (external Grok Bot agent for Ed Langley), 2026-09-21 PT.\n- `crates/improvise-worker-server/src/bindings.rs`: `PathBuf::from(document)` with little/no validation before `attach`.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude). Related to but distinct from OPFS AccessHandle conflicts.\n","status":"open","priority":2,"issue_type":"bug","created_at":"2026-09-21T20:32:38Z","created_by":"box","updated_at":"2026-09-21T20:32:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-iobc","title":"[bug] Bad/missing ?seed= creates empty doc with no loud error","description":"## Summary\nA bad or missing `?seed=` is silent: the worker creates an empty document and the UI never surfaces a loud, actionable error. CORS/404 appears only in the console.\n\n## Repro\n1. Open `/improvise/?doc=probe-missing.improv\u0026seed=https://example.com/missing.improv`\n2. Or `?seed=nope.improv` (404 relative to the page).\n3. Observe an empty document with normal shell status; no toast/status naming the seed failure.\n\n## Expected\nStatus (or a blocking notice) names that the seed could not be fetched/parsed (404, CORS, corrupt), and ideally does not pretend the empty doc was intentional without saying so.\n\n## Actual\n`web/worker.js` `seedBytes()` returns `undefined` on `!res.ok` or fetch throw; comment says \"A missing seed is not an error\"; `start(DOC, undefined)` creates empty model. User sees empty grid.\n\n## Evidence\n- Browser smoke (external Grok Bot agent for Ed Langley), 2026-09-21 PT.\n- `web/worker.js` lines ~13–22 (`seedBytes`).\n- `crates/improvise-worker-server/src/vfs.rs` / `bindings.rs`: open-with-optional-seed.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude).\n","status":"open","priority":2,"issue_type":"bug","created_at":"2026-09-21T20:32:38Z","created_by":"box","updated_at":"2026-09-21T20:32:38Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-x1ns","title":"encoded_width undercounts a forced category that has a missing coordinate","description":"Found by ChatGPT Codex reviewing PR #90 at 99cfb7f. THE REVIEW WAS NOT READ BEFORE THE PR MERGED — orchestrator process failure, see improvise-\u003cthis\u003e's sibling note. Re-verified on main at ce895ff: STILL LIVE.\n\nencoded_width (crates/improvise-gp/src/fit.rs:18) computes has_missing at line 32 and uses it ONLY in the numeric branch:\n\n true =\u003e 1 + usize::from(has_missing), // numeric: feature + .missing flag\n false =\u003e present.iter().collect::\u003cHashSet\u003c_\u003e\u003e().len().max(1), // categorical: present only\n\nbuild_categorical (crates/gp-tree/src/data.rs:359) maps every missing cell to MISSING_CAT and includes it in distinct(\u0026mapped), emitting an ADDITIONAL one-hot.\n\nSo a forced categorical column with N distinct present values plus one record lacking that coordinate passes the preflight as N and materializes N+1 columns. It is caught only by the LATER EXACT CHECK — which is precisely the allocation the preflight exists to prevent. At default max_records that is a large one-hot built before the error.\n\nThis is doctrine(guard_the_cost_not_the_result) defeated, and it is the SECOND time on this code path: improvise-zv8t's own adversarial round found forcing-by-name-while-budgeting-by-index, repaired as a data dependency. This one is the same class one level in — the two branches of one match disagree about what a column costs.\n\nNote the Wave K agent's own review had spotted a related shape (recorded as F5, \"build_categorical counts MISSING_CAT, the preflight does not — under-counts by one per category with an absent coordinate\") and recorded it as READ, NOT INJECTED and bounded by the exact check. Codex is right that \"bounded by the exact check\" is not a defence: the exact check runs after the allocation.\n\n## Acceptance Criteria\n- Count the missing level in the categorical branch.\n- A boundary regression with a FORCED NUMERIC CATEGORY containing a missing coordinate — the interesting case, because forcing is what makes a numeric column take the categorical branch.\n- Prove it by injection: without the fix, the preflight must under-report by exactly one and the test must fail naming that.\n- While there: gp.md §14 documents where the preflight and the exact check \"still differ\". Re-read it — it is the prose that made this look bounded.\n","status":"closed","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T17:13:05Z","created_by":"Edward Langley","updated_at":"2026-09-21T17:56:36Z","closed_at":"2026-09-21T17:56:36Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-ac2z","title":"A host's none_cats leak into another view's grid: formula columns vanish","description":"A client on a view whose none_cats differ from the HOST's is projected a grid\nmissing that view's formula columns entirely. Not a wrong number -- absent\ncolumns.\n\nFOUND BY CORRECTING A FIXTURE, which is the part worth keeping. The wave-I\nhand-back for improvise-t6db reported this hazard as \"argued and NOT observed\",\non the strength of a fixture that was said to put Product on the None axis in\none view and on Column in another. It did not: add_category defaults a new\ncategory to Column and nothing moved it, so all three views had none_cats\n{_Index, _Dim} and the stated input to the hazard was absent. An unprobed path\nreported as a negative result. Setting Product to Axis::None explicitly made the\nproperty test fail on the first run.\n\nMECHANISM. App::rebuild_layout calls recompute_formulas(\u0026none_cats) for the\nHOST's view, populating Model::formula_cache. project::grid_in on the elsewhere\npath builds the other view's GridLayout against that same, stale cache. The\ncache is keyed by the coordinates the recompute used, so with Product on None in\nthe host's view the Total cells are keyed WITHOUT a Product coordinate; the other\nview, which has Product on Column, asks for (Region, Product, _Measure=Total),\nfinds nothing, and -- because prune_empty is on -- drops both Total columns.\n\nMEASURED, two-view fixture, formula Total = SUM(Revenue):\n\n host on Second, projecting Second 6 columns incl. Total x Widget/Gadget\n host on Default, projecting Second 4 columns NO Total columns at all\n\nTHE FIX IS NOT LOCAL. grid_in takes \u0026App and recompute_formulas needs \u0026mut Model,\nso grid_in cannot recompute for the view it is projecting. Candidates, none\nchosen:\n\n - Key formula_cache by view (or by none_cats), so one Model can hold more than\n one view's derived values.\n - Recompute on the session's behalf at a \u0026mut App boundary (Sessions::project\n is one) and cache per session.\n - Declare it out of scope: formulas are evaluated for the host's view and a\n client on another view sees the host's derivation. That is a decision, not\n the current state, which is an accident.\n\nNote this is the SAME SHAPE as improvise-t6db: the elsewhere path of grid_in is\nonly as correct as the inputs it can rebuild, and prune_empty was the first such\ninput. none_cats is the second, and it is worse, because prune_empty was one\nline and this one cannot be fixed where it is observed.\n\nGUARDED MEANWHILE by a_hosts_none_cats_leak_into_another_views_formula_columns\nin ui/project.rs, a characterization test that asserts the bug. Its message says\nso: if it fails, the bug is fixed, and the none_cats guard should come out of\na_clients_grid_does_not_depend_on_which_view_its_host_is_on at the same time.\n\n## Steps to Reproduce\n\n1. Two views over one document, both with prune_empty on, with a formula.\n2. Put a category on Axis::None in view A and on Axis::Column in view B.\n3. Host on B: grid_in(app, \"B\") draws the formula's columns.\n4. Host on A: grid_in(app, \"B\") does not.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:31:45Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:31:45Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-711n","title":"parse_number_format silently yields 0 decimals for the ,.2f spelling our own docs show","description":"Found by the adversarial review of PR #92, as a side finding while printing a test fixture's rendered cells and noticing they said \"1\" where the fixture had set format \",.2f\".\n\nparse_number_format (crates/improvise-core/src/format.rs:14) reads the decimal count as:\n\n fmt.rfind('.').and_then(|i| fmt[i + 1..].parse::\u003cu8\u003e().ok()).unwrap_or(0)\n\nFor \",.2f\" that takes everything after the dot — \"2f\" — and parses it as u8. It FAILS, so unwrap_or(0) yields ZERO DECIMALS. Silently.\n\n## Why this is not hypothetical\n- context/repo-map.md's own .improv format example documents `format: ,.2f`.\n- persistence/format.rs:109 writes `format: {}` with view.number_format verbatim, so whatever a user types round-trips into the file unchanged and is re-parsed the same wrong way on every load.\n- The trailing-f spelling is the Python/printf convention a user would most naturally reach for.\n\nSo a document whose author wrote the spelling our own documentation shows renders every number with zero decimals, and nothing reports it.\n\n## Steps to Reproduce\nSet a view's number_format to \",.2f\" and render any non-integer value. Expect 1.00; observe 1.\n\n## Acceptance Criteria\n- Decide which spellings are legal and say so in ONE place. The grammar is the source of truth for .improv, so if \",.2f\" is legal the parser must honour it; if it is not, loading must refuse it rather than silently reinterpreting it.\n- A test with the \",.2f\" spelling asserting the decimals actually applied. It must fail on today's code.\n- Whichever way it goes, the repo-map example and the format doc must agree with the parser — this bug is partly a documentation bug, and a fact stated twice is changed by grep.\n- Consider the neighbouring spellings while you are there: \".2\", \",.2\", \"0.00\", \"%.2f\".\n\nNOT verified: whether any shipped document or fixture uses the trailing-f spelling, and whether the import wizard can produce it.\n","status":"closed","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:29:53Z","created_by":"Edward Langley","updated_at":"2026-09-21T17:56:34Z","closed_at":"2026-09-21T17:56:34Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -198,6 +203,7 @@ {"_type":"issue","id":"improvise-pby","title":"2.2 Write the README","description":"Replace existing README.md with new one under 250 lines. Sections in order: Title, one-sentence pitch, inline demo GIF, why this exists (Lotus Improv reference), quick start, key bindings, installation (Nix/crates.io/prebuilt - no Homebrew), codebase overview, expectations disclaimer, license. No badges, TOC, or contributing guide.","status":"closed","priority":2,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:07:32Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:02:52Z","close_reason":"README written with all 10 required sections, committed. License updated to Apache-2.0 per user request.","dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"improvise-ihv","title":"2.1 Add Nix tooling for asciinema, VHS, and cargo-dist","description":"Modify flake.nix to add pkgs.asciinema and pkgs.vhs to dev shell nativeBuildInputs. Add cargo-dist if packaged in nixpkgs. Add runtime deps for VHS (ttyd, ffmpeg) if needed. Verify tools work via nix develop --command. Add nix run app or shell script for demo recording workflow.","status":"closed","priority":2,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:07:30Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:30:19Z","close_reason":"asciinema, vhs, cargo-dist added to flake.nix dev shell and verified. cargo-dist wrapper enables 'cargo dist' subcommand. scripts/record-demo.sh added.","dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"improvise-yk4","title":"Phase 2: README and demo artifacts","description":"The main launch work. README is 80% of the launch, demo artifacts are 20%. Covers Nix tooling for asciinema/VHS, writing the README, creating demo GIF and asciinema casts, and verifying all artifacts.","status":"closed","priority":2,"issue_type":"feature","assignee":"spot","owner":"el-github@elangley.org","created_at":"2026-04-09T04:05:49Z","created_by":"Edward Langley","updated_at":"2026-04-14T07:47:43Z","closed_at":"2026-04-14T07:47:43Z","close_reason":"All 5 subtasks closed; artifacts verified present and tracked in git (README.md, docs/demo.gif, docs/demo.tape, docs/casts/*.cast).","dependencies":[{"issue_id":"improvise-yk4","depends_on_id":"improvise-abz","type":"blocks","created_at":"2026-04-08T23:37:37Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-yk4","depends_on_id":"improvise-d4w","type":"blocks","created_at":"2026-04-08T23:37:36Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-yk4","depends_on_id":"improvise-ihv","type":"blocks","created_at":"2026-04-08T23:37:34Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-yk4","depends_on_id":"improvise-odx","type":"blocks","created_at":"2026-04-08T23:37:35Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-yk4","depends_on_id":"improvise-pby","type":"blocks","created_at":"2026-04-08T23:37:36Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-l3hm","title":"[bug] i on CellId::Create is a silent black hole; Aggregate/Derived decline out loud","description":"## Summary\n`i` / `a` on a `CellId::Create` (records-mode `+` column) is a silent black hole (`KeyOutcome` upstream/none), while Aggregate and Derived decline out loud with `SetStatus`. Same doctrine (`an_upstream_key_is_a_black_hole`) that drove loud declines for the other non-editable ids.\n\n## Repro (reasoning / unit-shaped)\n1. Records mode with a creating column under the cursor (`CellId::Create`).\n2. Press `i`.\n3. Observe: no status, no mode change — key appears dead.\n\n## Expected\nA local `SetStatus` naming that creating/naming a column is not available in the browser yet (same family as Aggregate’s drill message and Derived’s formula message), per clients.md loud-decline doctrine.\n\n## Actual\n`edit_or_drill` match arm `CellId::Create(_) =\u003e None` in `crates/improvise-protocol/src/resolve.rs`, which becomes Upstream/Ignored rather than Local SetStatus. Comment cites improvise-poia (projection identity; closed) — the loud-decline gap remains.\n\n## Evidence\n- Code hunt by external Grok Bot agent for Ed Langley, 2026-09-21 PT.\n- Contrast: `i_on_an_aggregate_declines_out_loud` / `i_on_a_derived_cell_names_the_formula` in the same file.\n- Closed improvise-poia / improvise-gcvb covered identity/commit, not this decline wording.\n\n## Notes\nFound by an external Grok Bot agent smoke/code hunt for Ed Langley (not Claude). Not an upstream black-hole key that is documented as intentionally silent for host features like `[`/`]`/drill wire — this is the client’s own edit-or-drill door answering None.\n","status":"open","priority":3,"issue_type":"bug","created_at":"2026-09-21T20:32:39Z","created_by":"box","updated_at":"2026-09-21T20:32:39Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-hsoe","title":"check-evidence cannot see a duplicate heading, and three exist","description":"Found by the Wave I agent while merging PR #92, which had accumulated TWO entries headed `### a_window_narrows_the_cells_and_neither_the_totals_nor_the_widths` — the original, describing a fixture that no longer existed and repeating a claim the adversarial review had measured false, plus the superseding one. A reader hitting the first would have taken it as current.\n\n--check-evidence CANNOT SEE THIS. It verifies that a heading names a test that runs, and a duplicate heading names one just as well. Both copies pass.\n\n## Three already on main, verified 2026-09-21\n ### a_layout_change_moves_the_layout_version_and_no_record (test-evidence/command.md)\n ### the_two_front_ends_draw_the_same_column_header (test-evidence/tui.md)\n ### the_whole_frame_is_unchanged (test-evidence/tui.md)\n\nReproduce:\n grep -rhoE '^### [A-Za-z_:][A-Za-z_0-9:]*$' context/test-evidence/*.md | sort | uniq -d\n\n## Why it matters beyond tidiness\ncontext/test-evidence records WHY a test can fail. Two entries under one heading means one of them is stale by construction — they cannot both describe the current test — and nothing indicates which. That is the same class as an evidence entry whose mechanism measurement contradicts (doctrine an_evidence_entry_that_measurement_contradicts_is_worse_than_none): a reader gets a confident wrong answer rather than no answer.\n\n## Acceptance Criteria\n- --check-evidence refuses a duplicate heading, naming the file and both line numbers. It should refuse rather than warn, per the precedent set for ambiguous leaf names in improvise-rm4s.\n- Prove it by injection: add a second copy of an existing heading and watch the checker fail by name.\n- Resolve the three above. Each needs a read to decide which copy is current, or whether they describe genuinely different aspects and want distinct names.\n- Consider whether the same check should apply ACROSS files, not only within one: a heading is keyed to a test name, and test names are unique workspace-wide once module-qualified (improvise-rm4s), so two files claiming the same test is also a defect.\n","status":"closed","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T12:39:28Z","created_by":"Edward Langley","updated_at":"2026-09-21T18:04:26Z","closed_at":"2026-09-21T18:04:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-swre","title":"grid_of recomputes an unchanged grid every frame; memoise on the tiers that already exist","description":"Split out of improvise-jmjz on the adversarial review of PR #92, which closed jmjz on its own acceptance criterion (fuse the passes) and argued this is the separate, larger win.\n\ngrid_of recomputes the whole grid from scratch EVERY FRAME, even when nothing moved. After jmjz that is exactly one pass rather than three — 95ms at 18,000 cells, 1.32s at 90,000 — but the common case during scrolling, panel toggling and cursor movement is that the grid has not changed at all.\n\n## Why this is cheap to do here\nThe invalidation signal ALREADY EXISTS and is maintained for other reasons:\n- document_generation, layout_version, record_versions (gotcha 40, ADR 001's three tiers)\n- improvise-mcq4's view_shapes / view_frame observation\n\nA GridBody memo keyed on those takes an unchanged grid to roughly zero.\n\n## Why the remaining pass cannot be narrowed instead\nRuled out twice over, and the review measured it: totals sum EVERY row including ones a viewport scrolled past (gotcha 44), and widths are content-adaptive over every row. The benchmark's windowing ratio stayed 1.2-1.4x precisely because the surviving pass is the one a viewport cannot narrow. improvise-i5tk's decline stands.\n\n## Acceptance Criteria\n- A memo on the projected grid body keyed on the existing invalidation tiers; no NEW tier invented, because a second description of \"has the document changed\" is the disease (see the reasoning behind gotcha 39 and 45, where the trigger is the counter moving rather than a table of command variants).\n- Benchmarked: an unchanged grid across frames should approach zero, and the first frame must not regress.\n- The negative arm matters as much as the positive: a test that a grid which DID change is recomputed. A memo that never invalidates is fast and wrong, and it is the failure mode nothing will notice.\n\n## Second-order, much bigger, not this ticket\n~5us per cell at 18k is dominated by evaluate_aggregated doing a matching_values set intersection per cell (gotcha 18). An incremental aggregate index is the deeper fix. 90,000 cells at 1.32s remains unusable; that case is labelled pathological and memoisation helps it most.\n","status":"open","priority":3,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-21T11:30:15Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:30:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-4dpj","title":"The RecordsNormal-to-Normal arm of settle_normal_mode has zero coverage, and it is live","description":"Two gaps in settle_normal_mode's coverage, both live code with zero tests. The second was raised by ChatGPT Codex on PR #91 and is recorded here because the behaviour turned out CORRECT and only the guard is missing.\n\n## Gap 1 — the RecordsNormal-to-Normal arm (found by injection)\nReplacing settle_normal_mode's guard with a one-way version — matching only AppMode::Normal, dropping the RecordsNormal arm — leaves THE ENTIRE WORKSPACE SUITE PASSING.\n\nThe existing crossing test drives the LAYOUT both ways, but both branches end with `enter-mode normal`, so the mode handed to the settle is AppMode::Normal in BOTH directions. It discriminates \"settles at all\" and \"settles against the ending layout\", but not \"settles from either mode\".\n\nThe arm is live, proved reachable in production:\n\n records mode -\u003e apply_effects([SetAxis { \"_Index\", Axis::None }])\n layout.is_records_mode() == false, mode was RecordsNormal\n -\u003e settle yields AppMode::Normal\n\nOne extra case in the existing loop — seed the mode as RecordsNormal, cross OUT — closes it, and the one-way injection must then fail by name.\n\n## Gap 2 — the SwitchView path, raised by Codex and VERIFIED CORRECT\nCodex reviewed PR #91 at c186909 (round 1, the per-effect resolution) and pointed out that SwitchViewAtCursor emits [SwitchView(target), ChangeMode(Normal)]: open the view panel over a records view, select a pivot view, and round 1 would leave RecordsNormal over the pivot grid, with the inverse leaving Normal over records.\n\nRound 2 (a10e95e) moved the settle to ONE call after rebuild_layout at the end of apply_effects, so it resolves against the POST-batch layout. Verified on main at ce895ff with a throwaway probe driving a real `:` line that crosses records-ness within one batch:\n\n start_records=true -\u003e ended_records=false mode=Normal\n start_records=false -\u003e ended_records=true mode=RecordsNormal\n\nCorrect in both directions. So the behaviour Codex named is fixed.\n\nTHE GUARD IS STILL MISSING. Codex asked specifically for \"a regression that switches between records and pivot views through the view panel\", and no such test exists. The path it names is different from the one the crossing test drives (toggle-records-mode, not SwitchView), and records-ness is auto-detected from the axes (gotcha 1), so a view switch can change it.\n\n## Acceptance Criteria\n- The gap-1 case added to the existing loop; the one-way-guard injection fails by name.\n- A regression driving SwitchViewAtCursor over the view panel, records view to pivot view AND back, asserting the mode matches the grid the batch ends on.\n- Both must fail if the settle is moved back before rebuild_layout.\n","status":"closed","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:29:18Z","created_by":"Edward Langley","updated_at":"2026-09-21T18:04:27Z","closed_at":"2026-09-21T18:04:27Z","dependency_count":0,"dependent_count":0,"comment_count":0}