diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b967f839..25a045d9 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,8 @@ -{"_type":"issue","id":"improvise-h3y7","title":"The wire vocabulary must be complete against what needs the server, not merely correct","description":"USER RULING 2026-09-21: \"the wire protocol needs to cover the full range of commands that need the server side.\"\n\nThis is a COMPLETENESS requirement on the vocabulary, and it lands on a\nweakness the wave just demonstrated rather than on a suspicion.\n\nTHE PRECEDENT THAT MAKES IT URGENT. PR #115 found that gotcha 38's account of\nwhy ViewBack/ViewForward were off the wire was FALSE: it said they were\nsession-local residue like ApplyAndClearDrill, but view_back_stack is in\nViewState -- the portable slice -- so by the view half's own rule\n(decision(improvise-cqi): the ViewOnly effects targeting the portable slice)\nthey were always Commands and were simply MISSING. The derivation had been\nchecked for CORRECTNESS -- every listed variant belongs -- and never for\nCOMPLETENESS. The cost was a live divergence nobody could see: reduce_view's\nSwitchView arm set active_view and nothing else while effect::SwitchView pushes\nthe back stack, so NO CLIENT EVER HAD A BACK HISTORY.\n\nSo this bead is: enumerate the vocabulary AGAINST the rule, in both directions,\nand close what the enumeration finds.\n\nTHE KNOWN GAPS AT 523d571, to be treated as a starting list rather than the\nanswer:\n\n1. NO ModelCommand FOR A COORDINATE RENAME. ApplyAndClearDrill's\n RecordCol::Category(_) arm is apply_coordinate_rename -- add an item, then\n move EVERY measure cell in the row (gotcha 1) -- and grepping reduce.rs for\n Rename returns zero. A drill's category-column commit therefore has no wire\n form. This is a missing ModelCommand, NOT a missing addressed form, and is\n much smaller than it was assumed to be.\n\n2. SIX NAMES IN declined() (resolve.rs:232) that a browser cannot ask for at all:\n drill-into-cell, toggle-prune-empty, toggle-records-mode,\n add-record-row, delete-formula-at-cursor, view-back\n plus enter-mode via stranding (SavePrompt, ExportPrompt, CommandMode).\n view-back becomes removable when PR #115 merges -- the comment blaming the\n drill commit is wrong as of that PR. The others each need their own answer:\n some want a ModelCommand, some want a projection, and some may be genuinely\n host-only. SAY WHICH, PER NAME.\n\n3. EIGHT NAMES in the_command_names_that_still_need_the_host_are_these:\n hide-selected-row-item, toggle-group-under-cursor, search,\n search-navigate, search-or-category-add, enter-advance, save, force-quit\n (search-or-category-add leaves with improvise-koh6/PR #114.) Note gotcha 47:\n an UPSTREAM key is a black hole, not a slow path -- Host::handle answers\n ClientFrame::Key with an explicit empty vec -- so every name on this list\n does NOTHING in a browser rather than being merely slower.\n\n4. ClientFrame::Key ITSELF. It exists, is deployment-independent by gotcha 42's\n argument, and is answered with an empty vec. Either it should carry\n something or it should not exist; today it is a promise the protocol does\n not keep.\n\nWHAT THIS BEAD IS NOT: a licence to put everything on the wire. gotcha 38\nrecords two effects deliberately EXCLUDED -- ApplyAndClearDrill and\nAcceptWizardModel -- because their operands are session-local residue addressed\nto precisely one session, and an unaddressed form lets client B commit client\nA's typing (demonstrated against Sessions::apply before removal). The rule is\n\"everything that NEEDS the server\", and residue does not. The test for each\ncandidate is the one gotcha 38 already states: does the argument for excluding\nit name the thing it differs in?\n\nACCEPTANCE: a per-name verdict for every entry in (2) and (3) -- on the wire,\ndeliberately excluded with the argument, or blocked on a named prerequisite --\nplus the enumeration in (1) closed or filed. A list of what is missing is worth\nmore than a partial fix, and doctrine(a_stale_list_of_what_is_missing_is_worse_than_no_list)\nmeans the verdict list has to be maintained where it can be checked, not left\nin this bead.\n","status":"open","priority":1,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T02:51:02Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:51:02Z","labels":["architecture","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-fdk1","title":"A records coordinate rename has no wire form, and the surrogate is why","description":"Found by the improvise-h3y7 completeness audit, WHICH CORRECTED THE TICKET'S OWN\nDESCRIPTION OF THIS GAP -- and the correction changes the fix.\n\nimprovise-h3y7 said \"grepping reduce.rs for Rename returns zero\". True, and\nMISLEADING. Two paths do this:\n\n - effect::apply_coordinate_rename (ui/effect.rs:655), inside ApplyAndClearDrill,\n which is deliberately off the wire (gotcha 38).\n - commit.rs::rename_record_coordinate (cmd/commit.rs:366), the plain records path,\n which ALREADY decomposes into [AddItem, ClearCell(old), SetCell(new)] -- three\n commands that all have ModelCommand forms.\n\nSo it reads as though the plain path were already wire-expressible. IT IS NOT, for a\nreason the ticket does not state: ModelCommand::SetCell takes a RecordCell, whose\nRecordHash is an OPAQUE SURROGATE the server resolves against cells that ALREADY\nEXIST (App::resolve_record, ui/app.rs:516). The renamed record does not exist yet, so\na client cannot name it.\n\nTwo things that look like they solve this and do not. `will_be_created_by_the_frame`\n(ui/reduce.rs:284) exempts such a record from the BASIS CHECK -- it says nothing about\nhow to NAME one, and the two are easy to confuse. And RecordHash::of being pub is not\na licence: a client computing surrogates is precisely the invention ADR 002 exists to\nprevent.\n\nWHAT TO ADD: one position-free variant naming the existing record by surrogate and the\nnew coordinate by value --\n\n ModelCommand::RenameRecordCoordinate { record: RecordHash, category: String, item: String }\n\n-- mapped to a SINGLE effect factored out of the two copies above. Position-free, so\nimprovise-hkbc's rule still holds. Add it to records_named's match: it names one\nrecord, and creating the ITEM is internal to the effect, as both existing copies\nalready do.\n\nCONSOLIDATING THE TWO COPIES IS WORTH DOING REGARDLESS. They differ today only in how\nthey report the empty-value refusal, and gotcha 19 already warns that the synthetic-\nrecords branch and the plain branch drift.\n\nREMEMBER gotcha 1: renaming a category coordinate in wide records mode moves EVERY\nmeasure cell in the row, not just the one under the cursor. That is also why the basis\nunit is the record (gotcha 40) -- a cell-level unit is violated by this one legal edit.\n\nRELATED: improvise-o9uq is about the same code. Read it in full and say whether this\nsubsumes it before starting.","status":"open","priority":1,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:16Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:16Z","labels":["protocol"],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"improvise-2mqz","title":"SaveAs and ExportCsv have no wire form, which is why two prompts are stranded","description":"Found by the improvise-h3y7 completeness audit (direction A: effects -\u003e vocabulary).\nOf 32 ViewOnly effects, these are the only two that write storage and have NO wire\nform of any kind -- exactly the hole `Save` was in before improvise-368h.\n\neffect::SaveAs(path) and effect::ExportCsv(path) are unreachable from any client.\nresolve.rs::stranding() refuses SavePrompt and ExportPrompt for precisely this\nreason, so THE DECLINE IS A SYMPTOM: closing it means closing this, not editing the\ndecline.\n\nSHAPE: request FRAMES, not Commands -- improvise-368h's stated reason applies\nunchanged. A runtime-fallible write inside a ClientFrame::Commands batch breaks the\nall-or-nothing promise; [Save, SetCell] reported a failed write and then applied the\nedit. And neither mutates shared state or any client's slice, so reduce_view has\nnothing to fold and Checked has no basis to answer for.\n\n ClientFrame::SaveAs(PathBuf) / ClientFrame::ExportCsv(PathBuf)\n\nanswered ServerFrame::Rejected(..) on failure, to the asker alone, and SILENTLY on\nsuccess -- nothing moved, so no widget is dirty.\n\nBLOCKED ON the KeyOutcome shape gap (a resolver cannot emit a frame today). Land that\nfirst.\n\nTHE OPEN QUESTION THIS RAISES AND DOES NOT ANSWER -- flag it in the PR and get a\nruling before shipping: a browser client naming a path in the HOST's Storage is a\ndifferent act from a TUI user doing it (gotcha 2 -- the host picks the backend). The\nalternative is that a browser gets a download and never names a host path. Say which\nin context/code/protocol.md; different protocols follow from each.","notes":"=== USER RULING 2026-09-22 -- THE SHAPE ABOVE IS SUPERSEDED. READ THIS FIRST. ===\n\n'Save can save to OPFS, but exports should be downloads. There should also be a\nway to download the .improv file.'\n\nSo the description's proposed ClientFrame::SaveAs(PathBuf) / ExportCsv(PathBuf) is\nWRONG and must not be built. A browser client never names a path in the host's\nStorage. That also ANSWERS the open question the description flagged, in the safe\ndirection -- do not re-open it.\n\nTHE CORRECT SHAPE: a request frame that asks the host for BYTES, and a page that\nturns bytes into a download. Uniform across both deployments, names no path, and\ncarries no filesystem semantics across the wire at all.\n\n ClientFrame -\u003e a request for the rendered document or export\n ServerFrame -\u003e the bytes (or Rejected)\n web/app.js -\u003e Blob + an \u003ca download\u003e, which is the ONLY new browser-side code\n\nTWO ARTEFACTS ARE WANTED, not one: the CSV export, and THE .improv FILE ITSELF.\nThe second is new scope the original ticket did not have and is arguably the more\nimportant of the two -- it is how a browser user gets their document out of OPFS\nand into a file they own. persistence::format_md already renders it; nothing new\nis needed server-side beyond the frame.\n\nWHY BYTES AND NOT CLIENT-SIDE RENDERING, which is the tempting shortcut:\nimprovise-io IS in the browser bundle, so the worker deployment could render CSV\nlocally. The socket deployment could not -- a thin client holds no Model. Rendering\nlocally where possible and asking otherwise would be TWO derivations of one answer,\nwhich is the disease design-principles §5 names and improvise-edp is the worked\nexample of. Ask the host in both.\n\nNOTE render_csv must build its layout with with_frozen_records, NOT GridLayout::new\n(gotcha 1, improvise-iot6) -- export.rs already does; do not regress it.\n\nSAVE ITSELF NEEDS NOTHING NEW: the worker already persists to OPFS on the dirty\nflag (gotcha 45), and ClientFrame::Save exists for the socket deployment. What is\nmissing there is only the KEY that reaches it -- improvise-jz6w.\n\nSTILL BLOCKED ON improvise-jz6w for the same reason as before: a resolver cannot\nemit a frame today.","status":"open","priority":1,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:04Z","created_by":"Edward Langley","updated_at":"2026-09-22T05:13:41Z","labels":["protocol"],"dependencies":[{"issue_id":"improvise-2mqz","depends_on_id":"improvise-jz6w","type":"blocks","created_at":"2026-09-22T04:48:02Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-jz6w","title":"KeyOutcome cannot express 'send a frame', so Ctrl-S cannot reach the ClientFrame::Save that already exists","description":"Found by the improvise-h3y7 completeness audit. It is a SHAPE gap, not a vocabulary\ngap, which is why an audit that enumerates Command/ModelCommand against effects finds\nnothing here.\n\nClientFrame::Save landed with improvise-368h and Host::handle answers it (ui/host.rs:83).\nBut KeyOutcome is `Local(Vec\u003cCommand\u003e) | Upstream(KeyEvent) | Ignored`\n(improvise-protocol/src/resolve.rs:20), so a resolver has NO WAY to say \"send this\nframe\". Consequently `save` is still on the_command_names_that_still_need_the_host_are_these,\nresolves to Upstream, and Host::handle answers Upstream with an empty vec (gotcha 47:\nan upstream key is a BLACK HOLE, not a slow path).\n\nSo: an implemented server feature that no key can reach.\n\nWHAT TO ADD: a fourth KeyOutcome variant carrying a ClientFrame -- or a narrower\n`Request` enum, if a resolver handing out arbitrary frames is too wide. ARGUE THE\nCHOICE in the PR rather than picking silently; the narrow form is the one that keeps\na resolver from composing a Viewport or a Commands batch by accident.\n\nThen bind Ctrl-S through it and REMOVE `save` from the host list in the same commit --\ngotcha 47: closing a gap has to come to that list and say so.\n\nTHE GUARD: end to end, in the style of tests/browser_round_trip.rs -- Ctrl-S on a\nClientState yields the save request, and Host::handle on that request writes the\ndocument through a real App. Not two half-tests that never meet.\n\nTHIS UNBLOCKS the SaveAs/ExportCsv frames (filed separately), which need the same\nmechanism. Do this one first; it is small on its own.\n\nNOTE for whoever reads KeyOutcome afterwards: context/code/clients.md should say that\nKeyOutcome's variants are part of the protocol surface and belong in any completeness\nenumeration. That omission is why this gap was invisible to the audit's first pass.","status":"open","priority":1,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:03Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:03Z","labels":["browser","protocol"],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"improvise-m0sx","title":"Command::ClearBuffer is invented, and disagrees with the only implementation of its own name","description":"Found by the improvise-h3y7 completeness audit (direction B: vocabulary -\u003e effects),\nand VERIFIED INDEPENDENTLY by the orchestrator before filing.\n\nTHE DIVERGENCE. reduce_view's arm (improvise-protocol/src/reduce.rs:269) does\n`view.buffers.remove(name)` -- it REMOVES the key. The host's `clear-buffer`\ncommand (command/cmd/effect_cmds.rs:226) composes `core::write_buffer(name, String::new())`,\nwhich is `effect::SetBuffer` -- and SetBuffer INSERTS, leaving `buffers[name] == \"\"`.\nresolve.rs::write_buffer composes the same SetBuffer. So the one behaviour both\nends actually implement leaves an empty string, and the wire variant says otherwise.\n\nThere is NO `ClearBuffer` effect anywhere in the workspace. The variant is therefore\nnot derived from the effect set, violating decision(improvise-cqi,\ncommand_set_derived_not_invented). It is the exact INVERSE of the ViewBack defect\nthat improvise-h3y7 was opened over: ViewBack was an effect with no Command; this is\na Command with no effect.\n\nREACHABILITY. `grep -rn \"Command::ClearBuffer\" crates/ src/ web/` returns exactly\nthree hits, ALL inside reduce.rs: the definition at :39 and two of reduce.rs's own\ntests at :405 and :465. No production code composes it. (Note `ClearBufferCmd` in\neffect_cmds.rs and registry.rs is the command-layer NAME, not the wire variant --\nthey are different things that read the same, which is part of why this survived.)\n\nImpact if it were ever sent: a Snapshot seeded from a ViewState where the host left\n`buffers[\"edit\"] == \"\"` against an incrementally-folded client with no key -- two\nclients whose ViewStates are PartialEq-unequal for the \"same\" state.\n\nDECIDE ONE, AND SAY WHICH IN context/code/protocol.md:\n(a) delete the variant and its two tests -- free today, since nothing composes it; or\n(b) keep it as the derived form and change `clear-buffer`'s effect to remove the key,\n which makes ViewState equality agree between a folded client and a snapshot-seeded one.\n\nTHEN ADD THE GUARD that makes this whole class visible: an exhaustive test over\n`variants::declared::\u003cCommand\u003e()` asserting every variant is composed by some\nproduction resolver or relay. The machinery is already in\ncrates/improvise-protocol/src/variants.rs. Prove it discriminating by deleting one\nresolver arm and watching it fail -- a variant nothing composes is either dead or a\npromise, and both need naming.\n\nNOTE the two existing tests currently PIN the divergent behaviour (they assert\n`!view.buffers.contains_key(\"edit\")`), so whichever answer is taken, they change.","status":"open","priority":1,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T04:46:49Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:46:49Z","labels":["protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-h3y7","title":"The wire vocabulary must be complete against what needs the server, not merely correct","description":"USER RULING 2026-09-21: \"the wire protocol needs to cover the full range of commands that need the server side.\"\n\nThis is a COMPLETENESS requirement on the vocabulary, and it lands on a\nweakness the wave just demonstrated rather than on a suspicion.\n\nTHE PRECEDENT THAT MAKES IT URGENT. PR #115 found that gotcha 38's account of\nwhy ViewBack/ViewForward were off the wire was FALSE: it said they were\nsession-local residue like ApplyAndClearDrill, but view_back_stack is in\nViewState -- the portable slice -- so by the view half's own rule\n(decision(improvise-cqi): the ViewOnly effects targeting the portable slice)\nthey were always Commands and were simply MISSING. The derivation had been\nchecked for CORRECTNESS -- every listed variant belongs -- and never for\nCOMPLETENESS. The cost was a live divergence nobody could see: reduce_view's\nSwitchView arm set active_view and nothing else while effect::SwitchView pushes\nthe back stack, so NO CLIENT EVER HAD A BACK HISTORY.\n\nSo this bead is: enumerate the vocabulary AGAINST the rule, in both directions,\nand close what the enumeration finds.\n\nTHE KNOWN GAPS AT 523d571, to be treated as a starting list rather than the\nanswer:\n\n1. NO ModelCommand FOR A COORDINATE RENAME. ApplyAndClearDrill's\n RecordCol::Category(_) arm is apply_coordinate_rename -- add an item, then\n move EVERY measure cell in the row (gotcha 1) -- and grepping reduce.rs for\n Rename returns zero. A drill's category-column commit therefore has no wire\n form. This is a missing ModelCommand, NOT a missing addressed form, and is\n much smaller than it was assumed to be.\n\n2. SIX NAMES IN declined() (resolve.rs:232) that a browser cannot ask for at all:\n drill-into-cell, toggle-prune-empty, toggle-records-mode,\n add-record-row, delete-formula-at-cursor, view-back\n plus enter-mode via stranding (SavePrompt, ExportPrompt, CommandMode).\n view-back becomes removable when PR #115 merges -- the comment blaming the\n drill commit is wrong as of that PR. The others each need their own answer:\n some want a ModelCommand, some want a projection, and some may be genuinely\n host-only. SAY WHICH, PER NAME.\n\n3. EIGHT NAMES in the_command_names_that_still_need_the_host_are_these:\n hide-selected-row-item, toggle-group-under-cursor, search,\n search-navigate, search-or-category-add, enter-advance, save, force-quit\n (search-or-category-add leaves with improvise-koh6/PR #114.) Note gotcha 47:\n an UPSTREAM key is a black hole, not a slow path -- Host::handle answers\n ClientFrame::Key with an explicit empty vec -- so every name on this list\n does NOTHING in a browser rather than being merely slower.\n\n4. ClientFrame::Key ITSELF. It exists, is deployment-independent by gotcha 42's\n argument, and is answered with an empty vec. Either it should carry\n something or it should not exist; today it is a promise the protocol does\n not keep.\n\nWHAT THIS BEAD IS NOT: a licence to put everything on the wire. gotcha 38\nrecords two effects deliberately EXCLUDED -- ApplyAndClearDrill and\nAcceptWizardModel -- because their operands are session-local residue addressed\nto precisely one session, and an unaddressed form lets client B commit client\nA's typing (demonstrated against Sessions::apply before removal). The rule is\n\"everything that NEEDS the server\", and residue does not. The test for each\ncandidate is the one gotcha 38 already states: does the argument for excluding\nit name the thing it differs in?\n\nACCEPTANCE: a per-name verdict for every entry in (2) and (3) -- on the wire,\ndeliberately excluded with the argument, or blocked on a named prerequisite --\nplus the enumeration in (1) closed or filed. A list of what is missing is worth\nmore than a partial fix, and doctrine(a_stale_list_of_what_is_missing_is_worse_than_no_list)\nmeans the verdict list has to be maintained where it can be checked, not left\nin this bead.\n","notes":"AUDIT COMPLETE 2026-09-22 against 44b8819. Both derivations run.\n\nMECHANISM: brace-matched impl Effect blocks in ui/effect.rs =\u003e 26 EffectKind::Model(_),\nverified against gotcha 38's stated 26/24. NOTE THE MECHANISM UNDER-REPORTS BY 9: it\nmisses the 8 simple_effect! macro expansions and the one impl Effect in\ncommand/keymap.rs (SetTransientPrefix). Total effects = 58: 26 Model(_), 32 ViewOnly.\nAll 9 missed are ViewOnly, which is exactly the half the ViewBack bug lived in.\nGotcha 38 warns the naive awk OVER-reports by picking up HelpPageSet; the brace-matcher\nit recommends instead UNDER-reports, and nothing in context/ said so.\n\nDIRECTION A (effects -\u003e vocabulary): no gap in the model half -- all 26 accounted for.\nIn the view half, 21 of 32 have a Command; 9 of the remaining 11 are soundly excluded\nand TWO ARE A GAP: SaveAs and ExportCsv write storage and have no wire form at all\n(=\u003e improvise-2mqz).\n\nDIRECTION B (vocabulary -\u003e effects), NEVER RUN BEFORE, three finds:\n - Command::ClearBuffer is INVENTED and contradicts the only implementation of its\n name (=\u003e improvise-m0sx, P1, orchestrator-verified independently)\n - effect::HelpPageSet unclamped where its mirror clamps (=\u003e improvise-4n4i)\n - effect::SwitchView pushes the back stack before a guarded switch (=\u003e improvise-swzk)\n\nSTRUCTURAL PROPERTY FOUND AND NOT PREVIOUSLY WRITTEN DOWN: apply_command handles only\nSwitchView and Model; every other Command falls to _ =\u003e Dirty::NOTHING and is folded\ninto the asking session's mirror by reduce_view -- THE SAME FUNCTION both ends run. So\nmirror-vs-client drift is structurally impossible, and the whole direction-B risk\ncollapses to ~21 pairs of 'does reduce_view's arm do what its effect does'. That is an\nafternoon, and re-running it after any wave touching reduce_view or effect.rs is the\nonly thing standing between this repo and another ViewBack.\n\nTHE HEADLINE, which corrects this ticket's own framing: THE VOCABULARY IS FAR MORE\nCOMPLETE THAN ASSUMED. Of 13 gaps examined, EIGHT are closed in the vocabulary and open\nonly in the RESOLVER. Taken literally, this ticket would have produced new variants for\nthings already on the wire. Only three things need new wire surface: SaveAs/ExportCsv\n(frames), the coordinate rename (a ModelCommand), add-record-row (a new naming mode).\n\nGENERALISABLE FINDING: declined() is NOT evidence of a vocabulary gap and is NOT\nmaintained against the vocabulary. It is a sentence written when a name was hard, and\nnothing re-checks it when the thing that made it hard lands. view-back is the proven\ninstance (improvise-cfly is cleaning up after PR #115); toggle-prune-empty and\ntoggle-records-mode are two more sitting in that state now. The structural fix is that\nthe decline list and the host list must be DERIVED FROM or CHECKED AGAINST the\nvocabulary, not hand-kept beside it.\n\nALSO: a SHAPE gap can hide behind a vocabulary audit. 'save' looks like a missing\nCommand and is not -- ClientFrame::Save exists and is answered; the only missing thing\nis a KeyOutcome variant that can express sending a frame (=\u003e improvise-jz6w).\nKeyOutcome's variants are part of the protocol surface and belong in any completeness\nenumeration; context/code/clients.md should say so.\n\nAND: the surrogate is the binding constraint on the whole CREATION class. RecordHash is\nresolved server-side against cells that exist, so a client cannot name a record it is\nabout to create. Invisible from the ModelCommand list. will_be_created_by_the_frame\nexempts such a record from the BASIS CHECK and says nothing about NAMING one -- the two\nare easy to confuse. Deserves a line in ADR 002.\n\nCHILDREN FILED: improvise-m0sx (P1), improvise-jz6w (P1), improvise-2mqz (P1, blocked\nby jz6w), improvise-fdk1 (P1), improvise-fo0w (P2), improvise-1ohv (P2, blocked by\nfdk1), improvise-4n4i (P3), improvise-swzk (P3).\nALREADY FILED, not duplicated: improvise-7xyx (raised P3-\u003eP2: it is a black-hole key,\nnot just a dead field), improvise-hs0u (prefixes), improvise-gd28 (drill),\nimprovise-bd9o (records mode), improvise-uln5 (advertised-vs-resolvable).\nFOUR PRODUCT RULINGS OWED -- see the orchestrator's hand-off.","status":"open","priority":1,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T02:51:02Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:48:13Z","labels":["architecture","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-koh6","title":"[bug] Following the help overlay's N to add a category overwrites a data cell","description":"DEMONSTRATED end to end under Chromium at 523d571, on web/demo.improv, with\nthree controls (C, P, T) in the same loop that DID move -- otherwise a blind\nprobe and a black hole look identical.\n\n cell (0,0) before: \"18,400\"\n keys: N R e g i o n Enter\n cell (0,0) after: \"18400on\" \u003c- Espresso/Revenue, overwritten with text\n categories: UNCHANGED -- no category was created\n status: \"...records mode is not available in the browser yet\"\n\nTHE SEQUENCE, measured one key at a time:\n\n - `N` resolves Upstream. Host::handle answers a Key frame with an empty vec, so\n the FULL DOM snapshot -- mode, hint, status, grid, tiles, minibuffer -- is\n BYTE-IDENTICAL before and after. Completely silent.\n - The user then types the category name into NORMAL, where every character is a\n live Normal binding:\n R -\u003e status \"records mode is not available in the browser yet\"\n e, g -\u003e nothing\n i -\u003e INSERT on the grid cell under the cursor, PRE-FILLED with its value\n o, n -\u003e appended to that edit buffer\n - Enter commits.\n\n`N` is advertised by the browser's OWN help overlay, page 4:\n\"N -- Quick-add a new category (from anywhere)\".\n\nWHY THIS IS NOT THE MINIBUFFER BUG IT WAS REPORTED AS: the minibuffer works\nperfectly. Measured: C then n gives mode NEW CATEGORY, minibuffer \"new category:\"\nin yellow, 1280x28.6 at y=691, and each typed character appears live; Enter\ngives 'Added category \"Reg\"'. Verified in two more modes (F,n -\u003e \"formula:\" cyan;\nC,a -\u003e \"add item to Product:\" green). The user's \"typing blind\" was LITERAL -- a\ndead key followed by characters landing as commands.\n\nTHE CATEGORY THIS ADDS TO THE DOCTRINE: clients.md enumerates black holes as\nkeys that do nothing. A black hole that leaves the keyboard in a MUTATING mode\nis strictly worse and is not currently distinguished. The cost here is a\ncorrupted value, not a confused user.\n\nSECOND, COMPOUNDING PROBLEM: the decline messages actively mislead. Pressing N\nthen R answers \"records mode is not available in the browser yet\" -- confident,\ntrue, and about a different subject entirely, arriving adjacent in time to an\nunanswered gesture. clients.md already names the failure (\"a reader who is told\nsomething plausible stops looking\"); this is a new route to it.\n\nIS THERE A ONE-LINE FIX? NO, and the investigating agent resisted calling it one.\nSearchOrCategoryAdd::execute (command/cmd/search.rs:181) is:\n\n if !ctx.search_query.is_empty() { SearchNavigate(false).execute(ctx) }\n else { vec![SetPanelOpen{Category,true}, change_mode(AppMode::category_add())] }\n\nThe else-branch is exactly two commands the browser already composes elsewhere,\nand search_query is on ViewState, so the client HAS everything it needs. But a\nconditional arm makes N's outcome STATE-DEPENDENT, and\nenter_in_an_empty_view_panel_resolves_locally_rather_than_upstream\n(resolve.rs:1364) exists precisely to forbid making the host-needed list\nstate-dependent. So it is a decision, not a line.\n\nCHEAP INTERIM that stops the data loss without deciding that: one arm in\ndeclined() giving search-or-category-add its own true subject. That ends the\nsilence but NOT the sequel, because the user's next keystrokes still land in\nNORMAL where `i` opens an editor.\n","status":"open","priority":1,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T00:22:11Z","created_by":"Edward Langley","updated_at":"2026-09-22T00:22:11Z","labels":["browser","data-loss","ux"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-1wft","title":"Browser boot failures have no diagnosis channel, so five different causes all read as silence","description":"Five open beads, filed independently by three different reviewers, are one\nfailure mode: the browser's boot can fail for several reasons and the page\neither says nothing specific or nothing at all.\n\n improvise-i0v9 OPFS handle held by another context (or a prior session)\n improvise-86nw start() rejects for a non-OPFS reason; onFrame then drops\n every frame while `server` is null\n improvise-9cma invalid ?doc= -- generic watchdog text only\n improvise-iobc bad or missing ?seed= -- silent empty document, CORS/404\n visible only in the console\n improvise-vep8 the watchdog arms AFTER `await transport.ready`, so a\n connection hang never arms it at all\n\nTHE SHARED ROOT: there is no channel carrying a boot failure's REASON from the\nworker to the page. gotcha 46 already records the mechanism -- a worker that\nrejects in start() fires NO `error` on the main thread, because worker.js\ncatches its own -- so the only survivor is a generic first-frame watchdog, and\nimprovise-vep8 shows that watchdog does not even arm in one case.\n\nThe result is the worst shape a failure can take, measured on i0v9: `#status`\nreads \"in-page worker - \u003cdoc\u003e\" at 13ms, which looks like SUCCESS, and 8 seconds\nlater is replaced by \"this browser may not allow it\" -- blaming the browser for\na condition the code knows precisely, since it is holding the exact\nNoModificationAllowedError naming the exact document.\n\nWHAT THIS TICKET IS: the diagnosis channel, not the five triggers. The worker\nmust be able to report a structured boot failure across postMessage, and the\npage must render it instead of a timeout. Each of the five then becomes a\nmatter of producing the right reason rather than of plumbing.\n\nTwo things to decide while building it, both raised by the triggers above:\n - a boot failure is a STATE, not a rejected promise (i0v9 needs the page to\n open and paint read-only, which means boot must succeed without a handle)\n - the watchdog stays, but as a LAST resort that is armed before anything can\n hang (vep8), and is disarmed rather than pre-empted by a precise failure\n (gotcha 48 already states that rule and PR #21 implemented half of it)\n\nNot a new investigation: every one of the five is already demonstrated. This\nexists so the five are not fixed five different ways.\n","status":"closed","priority":1,"issue_type":"feature","owner":"el-github@elangley.org","created_at":"2026-09-21T21:26:34Z","created_by":"Edward Langley","updated_at":"2026-09-21T21:58:54Z","closed_at":"2026-09-21T21:58:54Z","close_reason":"Fixed in the 2026-09-21 wave, integrated into main at 1574 passed / 0 failed / 7 ignored. Playwright 42 passed / 0 failed / 4 skipped (WebKit, no OPFS) -- up from 37, so the count MOVED. All gates green.\n\nTHE CHANNEL: a protocol frame crosses the port as a STRING, a shell notice as an OBJECT. Total discriminator, zero protocol change, since ServerFrame JSON is a string by construction of on_message.\n\nA boot failure is now a STATE, not a rejected promise: a document another context holds the handle to opens READ-ONLY over vfs::ReadOnly rather than dying. Only BootFailed is fatal. The watchdog is armed BEFORE transport.ready and every notice routes through shell.fail, so gotcha 48's disarm half holds.\n\nMoving the watchdog forced its message to change, and that is load-bearing: armed before ready, it can no longer claim 'accepted the connection but sent no document' -- the page does not know that yet. The existing spec asserted the OLD wording and failed, which is the right way round and is how it was found.","labels":["architecture","browser","ux"],"dependency_count":0,"dependent_count":6,"comment_count":0} {"_type":"issue","id":"improvise-55l5","title":"dispatch swallows interactive() errors silently","description":"## Summary\n\n`dispatch` uses `.ok()?` on `interactive()`, swallowing `Err` (argument parse failures, mode mismatch for `commit-buffer item`, etc.). The user presses a key and nothing happens — no status message. Violates `doctrine(an_upstream_key_is_a_black_hole)` for failures that *are* resolvable as user-visible errors.\n\n## Expected / Actual\n\n- **Expected:** Construction/resolve errors surface as status (or equivalent loud feedback).\n- **Actual:** `.ok()?` turns Err into silent no-op.\n\n## Evidence / verify\n\nBind a key to `commit-buffer item` in Normal mode (not ItemAdd). Press the key — command fails to construct with no status message.\n\n## Doctrine\n\n`doctrine(an_upstream_key_is_a_black_hole)` — silent no-ops that look like black holes when the failure is knowable.\n\n## Location\n\n`crates/improvise-command/src/command/keymap.rs` ~L37\n\n## PR / Attribution\n\nPR #100 F05. external Grok Bot from cloud adversarial review PR #100 for Ed Langley\n","status":"open","priority":1,"issue_type":"bug","owner":"box@local","created_at":"2026-09-21T20:48:19Z","created_by":"external-grok-bot","updated_at":"2026-09-21T20:48:19Z","external_ref":"gh-pr-100-F05","labels":["adversarial-review","pr-100"],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -63,6 +67,8 @@ {"_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-1ohv","title":"add-record-row cannot be named on the wire: a client cannot create a record","description":"Found by the improvise-h3y7 completeness audit.\n\nAddRecordRow (cmd/grid.rs:492) builds a CellKey from the active view's\nfiltering_cats() page selections (gotcha 27: Page union Filter, NOT\ncategories_on(Axis::Page)) and writes SetCell(key, Number(0.0)).\n\nA client cannot compose it, for the same reason the coordinate rename cannot be\ncomposed: ModelCommand::SetCell takes a RecordCell whose RecordHash is an opaque\nsurrogate the server resolves against cells that ALREADY EXIST (App::resolve_record,\nui/app.rs:516). The record this command creates does not exist, so there is nothing\nto name it with.\n\nThe client DOES have the operands -- GridCache::page_coords carries the page\nselections (gotcha 33), and page_coords is framed by nothing and survives a reshape.\nWhat it lacks is a variant that TAKES them.\n\nWHY THIS IS NOT A ONE-LINE ADDITION. A ModelCommand that names coordinates rather\nthan a surrogate is a NEW NAMING MODE in the vocabulary. ADR 002 contemplated two:\n\"names its target by name\" (a category, an item, a view) and \"names a record that\nalready exists by a position-free surrogate\". A third -- \"names a record that does\nnot exist yet, by coordinates\" -- wants the ADR conversation improvise-hkbc's guard\nis designed to force. DO NOT SLIP IT IN AS A VARIANT.\n\nCHEAPEST HONEST INTERIM: leave add-record-row declined and land the coordinate-rename\nticket first, which answers most of the same question and may settle the naming mode\non a case where the record's identity is at least partly known.\n\nTHE CLASS THIS BELONGS TO, worth writing into ADR 002 or context/code/protocol.md\nbecause it will be rediscovered otherwise: a client cannot name a record it is about\nto create. That is ADR 002 working as designed, and it is INVISIBLE from the\nModelCommand list, because every existing variant either names things by name or\nnames a record that already exists. `will_be_created_by_the_frame` (ui/reduce.rs:284)\nexempts such a record from the basis check, which reads as though the naming problem\nwere solved. It is not, and the two are easy to confuse.","status":"open","priority":2,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:53Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:53Z","labels":["protocol"],"dependencies":[{"issue_id":"improvise-1ohv","depends_on_id":"improvise-fdk1","type":"blocks","created_at":"2026-09-22T04:48:02Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-fo0w","title":"Three black-hole keys are resolvable today over vocabulary that already exists","description":"Found by the improvise-h3y7 completeness audit. THE HEADLINE OF THAT AUDIT is that the\nvocabulary is far more complete than assumed: of thirteen gaps examined, EIGHT are\nclosed in the vocabulary and open only in the resolver. These three need NO new wire\nsurface at all.\n\nEach sits on the_command_names_that_still_need_the_host_are_these (resolve.rs:2114)\nand is therefore a BLACK HOLE -- gotcha 47: Host::handle answers ClientFrame::Key with\nan explicit empty vec, so these keys do nothing at all in a browser rather than being\nmerely slower.\n\n1. `search` -- EnterSearchMode::execute (cmd/mode.rs:386) emits exactly\n [SetSearchMode(true), SetSearchQuery(\"\")]. Both are Commands. Composable in one\n line; the cheapest single win in the audit.\n\n BUT IT MUST NOT SHIP ALONE. search-navigate scans the model for matching cells and\n the client has only a viewport-windowed GridCache, so it is NOT composable --\n shipping `/` without `n` gives a browser user a search box that finds nothing,\n which is a WORSE failure than the current silence. Either land both (search-navigate\n needs a results projection or a host request frame -- file separately once scoped)\n or decline `/` out loud in the meantime.\n\n2. `enter-advance` -- EnterAdvance::execute (cmd/navigation.rs:144) is pure cursor\n arithmetic returning viewport_effects(...), i.e. SetSelected/SetRowOffset/\n SetColOffset, all on the wire. Its row_count/col_count are GridCache::extent, which\n improvise-bpq2 added for precisely this. The bottom-right AbortChain becomes\n KeyOutcome::Ignored. NOTE improvise-bd9o measures this as the black hole that\n swallows a records-mode commit.\n\n3. `hide-selected-row-item` -- ModelCommand::HideItem { view, category, item } exists.\n The operands are AxisFrame::cats.first() and the selected row's label tuple [0],\n both on GridCache.row_frame (gotcha 44).\n\nRULES THAT APPLY TO ALL THREE:\n- Each arm mirrors its Cmd LINE FOR LINE, including where siblings disagree\n (doctrine(mirror_the_cmd_line_for_line), gotcha 47). Tidying them into one rule is\n what makes the client a second derivation.\n- Each must come OFF the host list in the same commit. That list is where closing a\n gap has to come and say so.\n- tests/lockstep.rs is where the evidence goes.\n\nNOT IN THIS TICKET: force-quit, which is in the same file and is a product question\n(what does Ctrl-Q mean in a browser tab?). It should stop being an upstream black hole\neither way, but which way is a ruling.\n\nCAVEAT FROM THE AUDIT, stated so nobody treats the sizes as measured: the auditor read\neach Cmd::execute and confirmed every effect it emits has a Command, but did not write\nthe arms. Operand availability in GridCache is read off the types, not exercised.","status":"open","priority":2,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:16Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:16Z","labels":["browser","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-cfly","title":"The wave ships view history with no key that reaches it: declined() still refuses view-back","description":"Found by the wave's integration agent at preview 30560e0f, and it is a WAVE-LEVEL\nresidue rather than a defect in any branch -- which is why nobody could see it.\n\nPR #115 makes the view history genuinely work for a connected client:\nViewBack/ViewForward join the view half of Command, reduce_view folds all three\nview effects line for line, each client's own view_back_stack is maintained, and\na deleted view is pruned from every client's history via Command::ForgetView.\n\nBUT resolve.rs's composed() has NO view-back or view-forward arm, and declined()\nstill answers \"stepping back to the previous view is not available in the browser\nyet\" -- byte-identical to base.\n\nSo the merged wave contains the CAPABILITY and not the KEY. A browser user\npresses `\u003c` and is told the feature does not exist, over machinery that now\nfully supports it.\n\nWHY NEITHER AGENT COULD SEE IT: #115 was told not to touch resolve.rs (another\nagent owned it) and correctly reported the exact lines instead. #114 owned\nresolve.rs and had no reason to look at declined() at all. Reading #115 alone you\nwould conclude browser view-history works; reading #114 alone you would never\nlook.\n\nTHE EXACT LINES, from #115's report and confirmed by the integration agent as\nstill byte-identical to base:\n\n crates/improvise-protocol/src/resolve.rs\n 239: // NOT \"not built\": it commits a drill first, so composing it would\n 240: // commit another client's typing. Needs addressed commands -- clients.md.\n 241: \"view-back\" =\u003e \"stepping back to the previous view\",\n\nDELETE THE COMMENT WITH THE ARM. Its stated reason is WRONG as of #115: composing\nview-back does not commit another client's typing, because a browser session has\nno drill state to commit. Leaving a false reason behind is worse than leaving the\ndecline.\n\nWHAT TO ADD: a composed() arm returning vec![Command::ViewBack], and the forward\nsibling. Note gotcha 47's rule -- each resolver mirrors its Cmd LINE FOR LINE,\nincluding where siblings disagree -- so check ViewNavigate's own execute() rather\nthan assuming the two directions are symmetric.\n\nPRECONDITION: #115 merged. Nothing else; #115 named this explicitly.\n\nNOT IN SCOPE: line 234, \"drill-into-cell\" =\u003e \"drill\". That one still needs the\n_Drill view session-local and the staging design, and the user has since ruled\nthat browser staging must be identical to the TUI's. See improvise-gd28.\n\nALSO: make sure the wave's PR body does not claim browser view-history works.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T04:17:26Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:17:26Z","labels":["browser","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-en4s","title":"No guard that a client's formula panel loses a formula its target item's deletion removed","description":"Found while reviewing improvise-g3no; the reasoning is in\ncontext/code/command.md beside the other Dirty notes.\n\nDirty::of(ModelChange::CellData) reports formulas: true. That was written as\ndeliberate over-reporting in the cheap direction -- a cell write cannot change\na formula, so the entry cost one redundant Projection and bought nothing.\n\nIt is now load-bearing. improvise-g3no made Model::remove_item drop the\nformulas targeting the item, and effect::RemoveItem is CellData, so a delete\nreally can take a formula out of the document. Tightening that arm would leave\nevery connected client's formula panel showing a formula the document no longer\nhas, forever, with nothing in the stream to correct it -- gotcha 35's expensive\ndirection, and exactly the failure that put `formulas` on the LayoutGeometry arm\nin the first place.\n\nNothing guards it. removing_a_category_empties_the_clients_formula_panel covers\nthe LayoutGeometry twin only. The missing test is its item-level sibling: a\nsession whose formula panel names a _Measure formula target, a\nModelCommand::RemoveItem for that target, and an assertion that the client's\nFormulaCache no longer names it. Inject the fault by setting formulas: false on\nthe CellData arm of Dirty::of.\n\nLives in ui/project.rs, which is why it is filed rather than written.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T02:27:48Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:27:48Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-xolb","title":"The wave brief's gate list omitted the node-test jobs, the only gate that sees an unpainted RenderModel field","description":"Found by the wave's integration agent diffing my wave brief against .github/workflows/ci.yml.\n\nMY GATE LIST OMITTED A REAL CI JOB:\n\n ./scripts/node-test.sh 'web/tests/*.test.js' ci.yml:239 \"Browser JS unit tests (node --test)\"\n web/tests/bundle/*.test.js ci.yml:371 (needs a built bundle)\n\nAt base: 36 tests over 5 files, 0 fail. Neither my brief nor context/repo-map.md's \"Testing -- the short version\" mentions either.\n\nWHY IT MATTERS, and it is not a formality: web/tests/render_surface.test.js is the MECHANICAL guard at the RenderModel -\u003e DOM boundary. It parses the pub fields off crates/improvise-wasm-client/src/render.rs and requires model.\u003cfield\u003e to appear in web/app.js + web/shell.js.\n\nPROVEN BY INJECTION: adding `pub injected_unpainted_field: u64` to RenderModel fails that gate with actual: ['injected_unpainted_field'], expected: []. cargo test --workspace is COMPLETELY INDIFFERENT to it.\n\nSo an agent working from a gate list that omits it can expose a projected field, never paint it, pass every gate it was told to run, and ship the defect. That is precisely the cross-branch shape a wave produces: one agent adds the field, another owns the painting.\n\nTWO BLIND SPOTS THE GUARD STATES ABOUT ITSELF, confirmed by reading, so it should not be over-trusted either:\n - it is a SUBSTRING search (page.includes(\"model.\" + field)), so a field read only inside a function nothing calls reads as painted\n - its field regex requires exactly four-space indentation and a lowercase name\n\nTHE GENERALISABLE PART: a gate list carried in a brief is a COPY of CI, and a copy drifts. The brief is written from memory and the repo-map, and both were stale here. Checking the brief against ci.yml is a once-per-wave action, not a once-per-agent one -- no single agent would find this, because q and s never touch RenderModel and r and t each individually pass cargo test.\n\nWHAT TO DO: add both node jobs to the repo-map's testing section and to the wave brief template, so the next brief inherits CI rather than memory.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T00:49:15Z","created_by":"Edward Langley","updated_at":"2026-09-22T00:49:15Z","labels":["docs","testing","tooling"],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -97,6 +103,7 @@ {"_type":"issue","id":"improvise-jkxr","title":"parse_duplicate_categories is vacuous: cannot fail for a merge that drops half the items","description":"## Summary\n\n`persistence::parse::parser_edge_cases::parse_duplicate_categories` only asserts `!item_names.is_empty()` after parsing two `## Category: Type` sections (`- A` then `- B`). That cannot fail for the merge defect the test name and placement imply.\n\n## Evidence / fault injection\n\nProduction merge (`ParsedFile::push_category`) concatenates items on duplicate section names (`invariant(duplicate_category_sections_merge_in_pass_one)`). The nearby test `two_sections_naming_one_category_round_trip_to_a_stable_group_order` does assert both halves survive for groups.\n\nInjected fault mental model: change `push_category` to **replace** the first section with the second (keep only `B`), or keep only the first (`A`). Assert `!item_names.is_empty()` still passes. Only a total wipe of items fails.\n\nExpected assertion shape: both `A` and `B` present (order/content), matching the merge invariant — same standard as the group round-trip neighbour.\n\n## Doctrine\n\n- `doctrine(check_the_checker)` — a check nobody has seen fail for the named defect is indistinguishable from one that always passes\n- `invariant(duplicate_category_sections_merge_in_pass_one)`\n- design-principles §7 testing doctrine\n\n## Attribution\n\nexternal Grok Bot adversarial code review for Ed Langley\n\n## Location\n\n`crates/improvise-io/src/persistence/parse.rs` — `parse_duplicate_categories` (~L1096)","notes":"## Steps to Reproduce\n1. Open crates/improvise-io/src/persistence/parse.rs::parse_duplicate_categories.\n2. Mentally change ParsedFile::push_category to replace (not extend) on duplicate name.\n3. Re-run the test — it still passes.\n\n## Acceptance Criteria\n- Test asserts both items A and B are present after parsing the two Type sections.\n- Injecting a replace-not-extend merge makes the test fail.","status":"closed","priority":2,"issue_type":"bug","owner":"box@local","created_at":"2026-09-21T20:43:15Z","created_by":"box","updated_at":"2026-09-21T21:25:59Z","closed_at":"2026-09-21T21:25:59Z","close_reason":"Fixed in 2b086b2 (formatting follow-up e4adf86) on main.\n\nThe finding was verified by injection rather than taken on report: changing\npush_category's 'existing.items.extend(items)' to 'existing.items = items' --\nso a second section REPLACES the first -- left the old assertion PASSING. The\nstrengthened assertion fails with left: [\"B\"], right: [\"A\", \"B\"].\n\nThe test now asserts assert_eq!(item_names, [\"A\", \"B\"]), pinning order as well\nas membership, since insertion order is load-bearing here (gotcha 18). That is\nthe standard its neighbour two_sections_naming_one_category_round_trip_to_a_stable_group_order\nalready held groups to; this was the item-list half of the same invariant held\nto a weaker one.\n\nEvidence recorded in context/test-evidence/formula-and-io.md.","labels":["adversarial-review","testing"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-12ha","title":"Multi-tab browser editing: a SharedWorker cannot host OpfsFs (measured), so pick a shape","description":"CONTEXT: improvise-i0v9 is the bug (a second tab is dead and silent). This is the\ncapability ticket. The user ruled that read-only second tabs are acceptable \"as\nlong as that's clear\", then asked whether the worker should be shared between\ntabs. The answer was measured rather than recalled.\n\nTHE DECISIVE MEASUREMENT, all three engines:\n\n chromium SharedWorkerGlobalScope, isWorkerGlobalScope true, getDirectory ok,\n getFileHandle ok, hasCreateSyncAccessHandle FALSE\n \"TypeError: fh.createSyncAccessHandle is not a function\"\n firefox identical\n webkit getDirectory itself fails (no OPFS at all -- the gap opfs.spec.js\n already skips on)\n\ncreateSyncAccessHandle is spec'd [Exposed=DedicatedWorker] and both engines\nimplement that faithfully. NOT a flag, NOT transient, NOT permissions. The real\nOpfsFs was then run unmodified inside a module SharedWorker and failed at exactly\nthat call.\n\nSO: a SharedWorker CANNOT host OpfsFs as written. Gotcha 28's premise -- the\nhandle's read/write/truncate/flush ARE blocking, which is what let Storage stay\nsynchronous -- is a DEDICATED-WORKER-ONLY premise.\n\nTwo things cut the other way and are worth keeping:\n - SharedWorker itself is in fine shape: present in all three, MODULE shared\n workers work in all three, and two tabs demonstrably share ONE instance (same\n instance id, connections: 2, both served).\n - WebKit's SharedWorker is healthy; its OPFS is what is missing. WebKit is no\n worse off under a shared-worker design than it is today.\n\nTHREE WAYS OUT, ALL PROBED:\n\nA. SharedWorker host with an ASYNC storage seam. createWritable() IS\n [Exposed=Window,Worker] and works in a shared worker (measured, chromium and\n firefox); reads need no handle at all (getFile().text()). But this is\n precisely what gotcha 28 exists to avoid: Storage::read/write are sync,\n App::storage is a Box\u003cdyn Storage\u003e, effect::Save calls it synchronously\n inside Effect::apply inside App::apply_effects -- the one mutation site. An\n async fn in Effect::apply is a DIFFERENT TRAIT, and it reaches the native TUI\n too. Load-bearing, not plumbing.\n NARROWER VARIANT worth costing first: keep Storage sync over an IN-MEMORY\n document, flushed asynchronously from the message loop -- the shape\n commit_deletes() already has. Storage never becomes async; a fifth awaited\n method persist() joins the four. UNCOSTED: what holds a document in memory\n across a LoadModel, and what a crashed tab loses that a sync handle would not.\n\nB. Nested dedicated worker under a SharedWorker. DEAD. Firefox allows it and the\n nested worker gets a real working sync handle with OpfsFs unchanged; Chromium\n and WebKit do not implement nested workers in shared workers at all\n (\"ReferenceError: Worker is not defined\"). The one shape that would have cost\n nothing works in exactly one engine.\n\nC. Keep the dedicated worker, add a RELAY owning no storage -- a SharedWorker or\n BroadcastChannel moving ClientFrame/ServerFrame JSON between follower tabs and\n the one tab whose dedicated worker holds the handle.\n improvise_protocol::frame is already THE one pair for all deployments\n (improvise-xks6), and the argument recorded there is explicitly\n transport-agnostic. A third transport is the case that consolidation was made\n for.\n - probably free: Worker::on_frame's \"one client by construction\". Gotcha 45\n already says the Addressed filter is honoured against a future SharedWorker.\n Session ids, fan-out and per-session mirrors need nothing new.\n - LOAD-BEARING: leader election and failover. The handle IS released when the\n leading tab closes, but bindings.rs::boot has NO RETRY PATH, so a follower\n cannot be promoted without a reload -- the same \"dead until you reload\"\n through a different door.\n - LOAD-BEARING: the relay must serialize frames onto one tail promise.\n web/worker.js had this exact race and web/serial.js exists because of it\n (gotcha 48). A relay forwarding async reintroduces it ACROSS TABS, where\n gotcha 33 answers an overtaken update by DROPPING it.\n - REVISIT: persist-on-dirty. Storage is whole-document (gotcha 41), so N\n editing tabs is N whole-file rewrites through one handle.\n\nRECOMMENDED ORDER (agent's read, not a ruling): C first, A's narrow variant\nsecond, read-only as the fallback it now is. C's real price -- leader failover --\nis THE SAME MISSING RETRY PATH the read-only fallback needs anyway, so that work\nis not wasted either way.\n\nDO NOT treat this as small because Sessions is ready. That readiness is real and\nis the good news; the no is entirely on the storage side.\n\nUNTESTED: Safari proper. Playwright's WebKit has no OPFS in any context so it is\nunaskable here. Safari 17+ ships OPFS; whether createSyncAccessHandle is\ndedicated-only there too is an EXPECTATION, not a measurement.\nUNPROBED: whether BroadcastChannel beats a SharedWorker relay for C. It is in all\nthree engines and needs no new worker, possibly making C cheaper -- but it fans\nout to everyone, so the Addressed filter would be doing real work rather than\nbeing honoured defensively. One probe settles it.\n","notes":"NEXT CHEAPEST PROBE: whether BroadcastChannel beats a SharedWorker relay for option C. It is in all three engines and needs no new worker. The catch is that it fans out to everyone, so the Addressed filter would be doing real work rather than being honoured defensively (gotcha 45).","status":"open","priority":2,"issue_type":"feature","owner":"el-github@elangley.org","created_at":"2026-09-21T20:40:31Z","created_by":"Edward Langley","updated_at":"2026-09-21T20:42:20Z","labels":["architecture","browser","opfs"],"dependencies":[{"issue_id":"improvise-12ha","depends_on_id":"improvise-i0v9","type":"discovered-from","created_at":"2026-09-21T21:31:54Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-7gip","title":"[bug] OpfsFs::open's dedicated-worker guard checks WorkerGlobalScope, which a SharedWorker satisfies","description":"MEASURED in all three engines at 1f28868.\n\nOpfsFs::open() guards with:\n\n .dyn_into::\u003cweb_sys::WorkerGlobalScope\u003e()\n .map_err(|_| anyhow!(\"OPFS sync access handles require a dedicated Web Worker\"))?\n\n`self instanceof WorkerGlobalScope` is TRUE in a SharedWorkerGlobalScope. So a\nshared worker sails past this check and fails much later as a raw TypeError deep\nin the wasm glue:\n\n [firefox] workerError \"TypeError: arg0.createSyncAccessHandle is not a\n function\" at opfs/pkg/opfs_contract.js:93\n\nThe message names the right requirement; the type cannot check it.\ndyn_into::\u003cweb_sys::DedicatedWorkerGlobalScope\u003e() would.\n\nThis is doctrine(an_invariant_is_made_unrepresentable_not_validated) HALF DONE:\na guard was reached for, and the type chosen is one level too wide, so the\ninvariant is validated-and-wrong rather than unrepresentable.\n\nopfs.rs is wasm32-only with no Rust tests (0t in the inventory), so there is no\ncargo test that can fail first. The instrument that CAN fail first is harness/ --\nthe probe above is exactly the reproduction, and harness/opfs-contract already\nboots the real bundle in a real worker.\n","status":"closed","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T20:40:31Z","created_by":"Edward Langley","updated_at":"2026-09-21T21:58:57Z","closed_at":"2026-09-21T21:58:57Z","close_reason":"Fixed in the 2026-09-21 wave, integrated into main at 1574 passed / 0 failed / 7 ignored. Playwright 42 passed / 0 failed / 4 skipped (WebKit, no OPFS) -- up from 37, so the count MOVED. All gates green.\n\nGuard is now DedicatedWorkerGlobalScope.\n\nTHE ONE-LINER DOES NOT COMPILE: the type is web-sys feature-gated, so improvise-io/Cargo.toml needed a line too. That is why it looked free and was not.\n\nNOT VERIFIED: that a SharedWorker is actually refused -- the harness has no SharedWorker page. What is asserted is only that a DEDICATED worker still satisfies the narrow type, i.e. the fix did not break the shipped path. Stated explicitly in the evidence rather than implied. improvise-12ha owns the SharedWorker question.","labels":["browser","bug","opfs"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-7xyx","title":"FrameGroup::cat is dead in both front ends and its doc comment promises an echo nobody composes","description":"FrameGroup::cat is read by NEITHER front end -- dead in both. Its doc comment\nsays it is \"echoed back in ToggleGroup\", and resolve.rs has NO ToggleGroup arm\nat all, so the echo it promises cannot happen from a browser.\n\nBy design-principles.md section 6 this is the expensive class: a reason that\nlives only in a comment, where nothing checks it against the code. The comment\nis a promise about a client that does not exist.\n\nTwo outcomes are acceptable: compose ToggleGroup in resolve.rs and make the\ncomment true (z is on the silent list anyway, so this would close part of that),\nor delete the field and the comment together. What is not acceptable is leaving\na doc comment describing a protocol interaction no code performs.\n\nAlso noticed in the same pass: AxisFrame::cats is PARTIAL on both ends -- the\ncount is used, the category NAMES are drawn by neither, so the grid's corner\ncell is blank in both surfaces. Possibly intended; worth a decision either way.\n","status":"open","priority":2,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:58Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:48:03Z","labels":["docs","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-hs0u","title":"[bug] gg is implemented in the resolver and unreachable from any browser key","description":"DEMONSTRATED at 1f28868.\n\n PROBE 'g': browser = Upstream (silent)\n PROBE 'G': browser = Local[SetSelected { row: 1, col: 0 }, SetRowOffset(1)]\n\ngrid_move's GridMove::ToStart(true) arm EXISTS and is CORRECT. jump-first-row is\nbound only behind the g prefix (keymap.rs:639), and prefixes are unconditionally\nUpstream in resolve(). So a browser user can reach the last row and cannot get\nback to the first.\n\n0 and Home (jump-first-col) both work, so the asymmetry is ROW-ONLY and reads as\na bug rather than as an absence. The resolver arm is dead code in the browser\ntoday.\n\nSame root cause as the paste ticket: every prefix is Upstream, and\nHost::handle answers ClientFrame::Key with an explicit empty vec, so an upstream\nkey is a black hole rather than a slow path (gotcha 47). Fixing prefixes\nresolvably closes gg, yy and ZZ together.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:56Z","created_by":"Edward Langley","updated_at":"2026-09-21T20:39:56Z","labels":["browser","bug","parity"],"dependencies":[{"issue_id":"improvise-hs0u","depends_on_id":"blocked-by:improvise-lut7","type":"blocks","created_at":"2026-09-21T20:40:37Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-hs0u","depends_on_id":"improvise-lut7","type":"related","created_at":"2026-09-21T21:31:57Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-0ygd","title":"[bug] Tab on the +measure column silently discards the commit that Enter accepts","description":"DEMONSTRATED at 1f28868.\n\n PROBE +measure at (0,4), extent GridExtent { rows: 1, cols: 5 }\n PROBE Tab on +measure, the LAST row:\n browser = Upstream (silent)\n tui = doc_changed true, status \"Added new record row\"\n\na_locally_committed_creating_column_agrees_with_the_host passes for\nCreateKind::Measure -- it presses ENTER. Press TAB on the very same cell and:\n\n - resolve.rs::advanced() opens with\n if matches!(advance, Advance::Right) \u0026\u0026 state.grid.records\n \u0026\u0026 row \u003e= row_max \u0026\u0026 col \u003e= col_max { return None; }\n - None propagates out of create_column -\u003e commit -\u003e composed -\u003e Upstream, so\n the WHOLE COMMIT is discarded, not just the advance. The typed measure name\n is lost silently.\n - CommitAndAdvance::execute (cmd/commit.rs) takes the\n at_bottom_right \u0026\u0026 ctx.is_records_mode() branch instead: runs AddRecordRow,\n moves to (row+1, 0) and re-enters the editor. That is the bulk-entry Tab\n contract (see the records-mode Tab memory: Tab always advances to the next\n useful place).\n\n+measure is ALWAYS the last column, so col \u003e= col_max holds for it by\nconstruction. This fires on every single-row records grid and on the last row of\nany records grid.\n\nThis sits inside the already-filed \"records mode is silent in the browser\" gap,\nand closing that gap subsumes it -- but carry the specific observation into that\nticket, because \"records mode is silent\" does not predict \"the same cell answers\nEnter and drops Tab\".\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:55Z","created_by":"Edward Langley","updated_at":"2026-09-21T20:39:55Z","labels":["browser","bug","parity"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-wk18","title":"[bug] the advertised-key guard cannot see help.rs literal() lines, so 17 keys are unchecked","description":"DEMONSTRATED at 1f28868.\n\nevery_advertised_command_answers_in_its_own_mode iterates help::advertised(\u0026mode),\nwhich yields (command, args, caption) triples. help.rs ALSO emits 17 literal(..)\nlines naming a KEY STRING and no command, so the guard cannot see them.\nweb/app.js paintHelp renders them verbatim.\n\nOf the 17:\n gg, yy, ZZ -- all three are prefix keys, all three silent in the browser\n :wq :q :q! :import :add-cat :add-item :formula :add-view :set :help and the\n rest -- the whole ex-command reference page; the browser declines at the door,\n so it says something, but it renders a full page of commands it cannot run.\n\nSo the browser paints a help overlay in which, conservatively, an entire page\nplus three scattered lines are inert, and the test that exists to stop exactly\nthat cannot see any of it.\n\nThis is the cheapest of the parity findings and it PROTECTS THE OTHERS: it is\nthe mechanism this repo uses to stop black holes, and it looks away by\nconstruction from the two lines that the gg and paste tickets are about. Without\nit the next gg lands the same way and nothing notices.\n\nFix: either give literal() a command name so the guard can reach it, or add a\nsecond guard over the help pages.\n","notes":"BLOCKED AND RE-SCOPED 2026-09-21 by the keymap agent (PR #112), which took improvise-636d and deliberately did NOT half-do this one. Its reasoning and its proposed design, which is worth more than the ticket had:\n\nWHY IT COULD NOT BE DONE FROM THE KEYMAP GROUND:\n - the guard needs resolve() AND the `furnished` fixture, both private to resolve.rs's mod tests. Duplicating the fixture elsewhere is the disease this repo names repeatedly.\n - the HelpLine reshape both tickets call for breaks the struct patterns at improvise-wasm-client/src/render.rs:346 and src/ui/help.rs:72 -- NEITHER uses `..`, so the reshape is a compile error in a file another agent owns.\n\nTHE PROPOSED SHAPE, designed but not written: keep HelpLine FLAT and PUBLIC, and introduce an internal\n\n HelpRow {\n Keys { mode, command, args, description },\n Prefix { mode, prefix, then, .. },\n Ex { line, .. },\n Prose (HelpLine),\n }\n\nthat pages() RENDERS into HelpLine. One source, no renderer edit, and the guard walks the rows rather than the rendered lines.\n\nThat satisfies uln5's stated blocking cost -- \"carry command/mode/args onto the variant\" -- WITHOUT touching render.rs, which is what made it look expensive.\n\nIT ALSO SPLITS wk18's 17 literal() lines honestly rather than treating them as one lump:\n - gg / yy / ZZ are checkable via Keymaps::prefix_keymap\n - the 14 ex-commands are checkable against the registry from improvise-command's tests\n\nSo the two beads are one change to HelpRow plus two different guards, not one guard over a uniform list.\n\nNOTE THE GROUND THIS NEEDS: resolve.rs's test module, help.rs, and a test that can reach the registry. That is a different partition from the one this wave used, so schedule it with whoever owns resolve.rs rather than with the keymap.","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:19Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:27:15Z","labels":["bug","parity","testing"],"dependencies":[{"issue_id":"improvise-wk18","depends_on_id":"improvise-uln5","type":"related","created_at":"2026-09-22T02:27:15Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -283,6 +290,9 @@ {"_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-dsn2","title":"Browser back/forward buttons and real URLs: deferred, not foreclosed","description":"USER RULING 2026-09-22: \"I want to ignore browser history for now, without\nforeclosing the question: GitHub Pages is going to make actual client-side\nnavigation annoying.\" Clarified on the same day: \"Only actual browser history,\n\u003c/\u003e is a core feature but I don't much care about how it interacts with the\nback/forward buttons at the moment.\"\n\nSO THE SPLIT IS EXPLICIT, and the two halves must not be confused again:\n\n IN SCOPE AND SHIPPING -- the IN-APP view history. `\u003c` and `\u003e` over\n ViewState::view_back_stack, a CORE FEATURE per the user. PR #115 landed the\n mechanism (Command::ViewBack/ViewForward/ForgetView, per-client stacks,\n ForgetView pruning a deleted view from every client's history); improvise-cfly\n / PR #117 gives the browser a key that reaches it. Nothing here is deferred.\n\n DEFERRED, NOT FORECLOSED -- binding any of that to the BROWSER's own history:\n history.pushState, real URLs per document or per view, and the back/forward\n buttons doing anything at all.\n\nWHY DEFERRED, in the user's own reasoning: GitHub Pages is the deployment target\nand has NO server-side rewrite. A client-side router therefore needs either hash\nrouting or the 404.html-copies-index.html trick, and a deep link to a document or\na view is awkward under both. The cost lands on the deployment, not on the code,\nwhich is exactly the kind of cost that is cheap to defer and expensive to\ndiscover late.\n\nWHAT IT WOULD TOUCH IF TAKEN UP, recorded so the shape is not re-derived:\nweb/app.js (the only file that knows about the page), web/assemble.sh and the\npages-deploy job in .github/workflows/ci.yml. It does NOT touch the protocol:\nCommand::SwitchView, ViewBack and ViewForward already exist, so this is a\nquestion about the SHELL, not the vocabulary -- which is the main reason it is\nsafe to leave open.\n\nONE THING TO CHECK BEFORE BUILDING IT, because it decides the design: a URL that\nnames a view is a URL that names ONE CLIENT'S UI state, and gotcha 29 says the\nmodel is shared and the UI is not. A shared link that moves the recipient's view\nis fine; one that moves the SENDER's is the bug. Decide which a URL means before\nwriting a router.\n\nDO NOT close this as done when \u003c/\u003e ships. They are different features and the\ntracker should not let the second stand in for the first.","status":"blocked","priority":3,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-22T05:12:00Z","created_by":"Edward Langley","updated_at":"2026-09-22T05:12:04Z","labels":["browser"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-swzk","title":"A failed :view corrupts the TUI's own view history","description":"Found by the improvise-h3y7 completeness audit.\n\neffect::SwitchView (ui/effect.rs:336) pushes the current view onto view_back_stack\nBEFORE calling App::switch_to_view, which is guarded (ui/app.rs:565) and writes\nactive_view only if the view exists.\n\nSo `:view NoSuchView` leaves the user where they were WITH THEIR OWN CURRENT VIEW\nPUSHED ONTO THEIR OWN BACK STACK. `\u003c` then \"goes back\" to where they already are, and\none real history frame is buried under it.\n\nTHE WIRE PATH IS ALREADY CORRECT and this is not a regression of it: reduce_view's\n`switched` has no guard (the client holds no Workbook, so it cannot check), but\nSessions::apply_switch detects the refusal and relays the exact inverse\n([ViewBack, ForgetView], improvise-6c6q). Only the HOST'S OWN KEY PATH is unguarded.\nTUI-only, small, real.\n\nFIX: move the push after the switch, or make it conditional on the switch having\nhappened. Prefer whichever makes the ordering unrepresentable-otherwise rather than\nconventional.\n\nDEMONSTRATE WITH A FAILING TEST FIRST (AGENTS.md): switch to a real view, then to a\nnonexistent one, then `\u003c`, and assert you land on the real one. Note the test must use\na real second view -- a bystander sitting on the view it would land on anyway cannot\nsee this, which is the vacuity review demonstrated twice on PR #48.","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:55Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:55Z","labels":["tui"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"improvise-4n4i","title":"effect::HelpPageSet is unclamped where its wire mirror clamps","description":"Found by the improvise-h3y7 completeness audit (direction B: vocabulary -\u003e effects).\n\neffect::HelpPageSet(n) (ui/effect.rs:1174) writes `help_page = self.0` with NO bound.\nreduce_view's arm clamps: `HelpPage::Set(n) =\u003e (*n).min(HELP_PAGE_COUNT - 1)`.\n\nreduce.rs's own comment above that arm says \"these mirror HelpPageNext/HelpPagePrev,\nwhich saturate. The two used to disagree\" -- Next/Prev were fixed and Set WAS LEFT\nBEHIND, so the comment is now false of the arm it sits above. A stale correction: the\nsame failure mode as gotcha 38's prose, and the reason this is filed rather than\nquietly patched.\n\nLATENT, NOT LIVE, and the ticket should say so rather than overstate it:\neffect::help_page_set has exactly one call site (cmd/effect_cmds.rs:404) passing the\nliteral 0, and no client composes HelpPage::Set (resolve.rs has only Next/Prev arms).\nSo it is an asymmetry waiting for its first real caller.\n\nFIX: clamp the effect, and correct the comment in the same commit -- the comment is\nthe thing that made this survive.\n\nTHE REGRESSION GUARD: a test that the two agree for n \u003e HELP_PAGE_COUNT. Nothing tests\nthis in EITHER direction today, which is why a one-sided fix would look complete.","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T04:47:54Z","created_by":"Edward Langley","updated_at":"2026-09-22T04:47:54Z","labels":["protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-t3oh","title":"The cache-parity guard's whole-cache escape hatch proves consumption, not coverage","description":"Stated as a residual by the agent that WROTE the guard, in PR #113's evidence,\nrather than found later by someone else.\n\n`web/tests/render_surface.test.js` gained a second check one level up: every\n`pub` field of every widget cache reaches a front end, or is named as debt. It\nhas an escape hatch for the case where a whole cache is handed to a shared\nderivation, e.g. `flatten_cat_tree(\u0026state.categories, ..)` -- in which case\nevery field of that cache counts as reached.\n\nTHE HOLE: a second field added to `CategoryTreeCache` and ignored by\n`flatten_cat_tree` passes. The hand-off proves the cache is CONSUMED, never that\nevery field of it is.\n\nClosing it needs the CONSUMER checked rather than the call site -- i.e. asking\nwhat `flatten_cat_tree` actually reads, not that it was handed the struct.\n\nWHY THIS BEAD EXISTS RATHER THAN A TODO: the same guard's two OTHER escape\nhatches were both vacuous on first writing and both were found only by\ninjection -- `\u0026state.grid` is a substring of `\u0026state.grid.row_frame`, so every\nGridCache field read as covered (vacuous for precisely the cache p7lh was\nabout), and rustfmt splits `state.grid.totals` over three lines so a substring\nsearch calls a read field unread. Two of three hatches were wrong. The third\nbeing merely incomplete rather than wrong is not a reason to trust it.\n\nSee doctrine(an_escape_hatch_is_where_a_new_check_goes_vacuous).\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T02:28:50Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:28:50Z","labels":["browser","testing"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-ozxr","title":"The browser's page bar is display-only; clicking an entry cannot cycle it","description":"PR #113 (improvise-p7lh) made the page bar VISIBLE. It is display-only.\n\nIn the TUI, `[` and `]` cycle the page selection and the page bar is where a\npaged category is both shown and navigated. In the browser the bar now paints,\nbut clicking an entry does nothing -- cycling needs a `click_page` on the\nwasm-bindgen surface (`improvise-wasm-client/src/lib.rs`), which was outside\nthat agent's ground.\n\nWORTH KNOWING BEFORE SCOPING IT: `[` and `]` ALREADY WORKED in the browser\nbefore #113. The feature was never missing -- the FEEDBACK was. That is a worse\nfailure than a dead key, because the user sees the document change and cannot\nattribute it. So this bead is the mouse affordance for something the keyboard\nalready does, not new capability.\n\nPattern to follow: the other five surfaces got delegated handlers in\nimprovise-b8ot, with operands carried on the node as data attributes and read\nback by a pure `cellAction`/`indexAction` in `web/shell.js`. A page entry should\ndo the same -- `data-index` on the entry, a pure `pageAction`, one handler on\nthe `#pages` root. Do NOT derive the index from DOM position.\n\nNote gotcha 27: Page is the NAVIGABLE axis and Filter is not, and `PageAxis::cycle`\nis where that question is already answered -- ask it rather than respelling it.\n\nRelated: improvise-z336 (a page selection is never cleared, so cycling a\ncategory's axis hides a filter and cycling back restores it) is the other half\nof the page-axis story and is core-side, not browser-side.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T02:28:49Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:28:49Z","labels":["browser","ux"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-nwgk","title":"Model::remove_item returns unit, so a wire RemoveItem naming nothing still reports success","description":"Left open by improvise-g3no, deliberately, with the reasoning in\ncontext/code/core.md section 9.\n\ndoctrine(a_status_line_composed_beside_a_mutation_lies_when_the_mutation_no_ops)\ngeneralises to: a mutation whose failure is reportable must RETURN whether it\nacted, because a unit-returning mutation makes the lie unavoidable rather than\naccidental. g3no removed the REACHABLE lie by restoring the formula sweep to\nModel::remove_item -- from either panel the item name comes off the tree, so it\nexists and the delete now really happens. What is left is narrow: a wire\nModelCommand::RemoveItem naming an item the document does not have is still\nanswered with an unconditional past-tense \"Deleted item ...\".\n\nWhy it was not done in that PR: closing it means threading an answer through\nWorkbook::remove_item and effect::RemoveItem, and ui/effect.rs was another\nagent's ground in that wave. It is a two-file change, not a design question.\n\nThe browser half is separately blocked and is the more interesting part:\nSetStatus is ViewOnly and never crosses the wire, so a host-side refusal cannot\nreach a client at all, and nothing in the projection stream says which _Measure\nrows are derived (FormulaCache is Vec\u003cString\u003e of Formula::raw, and recovering a\ntarget from that would be a second home for the unquoting gotcha 34 says has\nexactly one). A browser that is to tell the truth about a refused delete needs\neither a ServerFrame::Rejected arm or an enriched projection.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-22T02:27:40Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:27:40Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -299,7 +309,6 @@ {"_type":"issue","id":"improvise-nvq4","title":"Hidden/collapsed items with slash in name corrupt on persistence round-trip","description":"## Summary\n\n`hidden:` persistence line format is `hidden: Cat/Item`. If Item contains `/`, the parser splits wrongly; hidden state is lost or corrupted on round-trip. A test allegedly documents this.\n\n## Steps to Reproduce\n\n1. Create item named `A/B`, hide it, save, reload.\n2. Hidden state lost or corrupted.\n\n## Acceptance Criteria\n\n- Round-trip preserves hidden/collapsed for names containing `/` (escape or alternate encoding).\n\n## Location\n\n`crates/improvise-io/src/persistence/parse.rs` (hidden: line)\n\n## PR / Attribution\n\nPR #100 P3 F23. external Grok Bot from cloud adversarial review PR #100 for Ed Langley\n","status":"closed","priority":3,"issue_type":"bug","owner":"box@local","created_at":"2026-09-21T20:49:07Z","created_by":"external-grok-bot","updated_at":"2026-09-21T21:58:17Z","closed_at":"2026-09-21T21:58:17Z","close_reason":"DOES NOT REPRODUCE -- closing invalid, on evidence.\n\nhidden_line reads two names; bare_name excludes '/', so quote_name already writes hidden: C/|A/B|. The grammar already forbade the hazard this bead describes.\n\nKept as a PASSING CONTROL rather than deleted: slash_in_hidden_name_survives_roundtrip now asserts it, so the bead cannot be re-filed without someone watching the property pass.\n\nSecond external-bot report in this area that did not reproduce (the first is the one AGENTS.md records at PR #21). Worth weighting bot-filed P3s accordingly.","external_ref":"gh-pr-100-P3-hidden","labels":["adversarial-review","pr-100"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-y8ym","title":"DeleteView effect discards error result","description":"## Summary\n\n`DeleteView` uses `let _ = app.model_state.workbook.delete_view(...)`, discarding `Result`. Failure (e.g. last view) produces no user error; code below assumes success.\n\n## Steps to Reproduce\n\n1. Attempt to delete the only view in a workbook.\n2. No error message; view may still exist while UI assumed deletion.\n\n## Acceptance Criteria\n\n- delete_view Err reaches status_msg (and abort if appropriate); success path only runs after Ok.\n\n## Location\n\n`crates/improvise-command/src/ui/effect.rs` ~L321\n\n## PR / Attribution\n\nPR #100 P3 F18. external Grok Bot from cloud adversarial review PR #100 for Ed Langley\n","status":"open","priority":3,"issue_type":"bug","owner":"box@local","created_at":"2026-09-21T20:49:07Z","created_by":"external-grok-bot","updated_at":"2026-09-21T20:49:07Z","external_ref":"gh-pr-100-P3-deleteview","labels":["adversarial-review","pr-100"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-v4hg","title":"AddItem/AddFormula errors don't abort the effect batch","description":"## Summary\n\n`AddItem` / `AddItemInGroup` / `AddFormula` (parse-error branch) set `status_msg` on failure but do not set `abort_effects = true`, unlike `AddCategory`. Downstream effects in the batch still run (e.g. SetCell after failed AddItem).\n\n## Steps to Reproduce\n\n1. Run a Sequence: AddItem to non-existent category, then an effect that assumes the item exists.\n2. Status shows error but second effect still applies.\n\n## Acceptance Criteria\n\n- Item/formula construction failures abort the batch consistently with AddCategory (or document intentional asymmetry).\n\n## Doctrine context\n\n`invariant(an_abort_stops_the_chain_not_the_bookkeeping)`\n\n## Location\n\n`crates/improvise-command/src/ui/effect.rs` ~L102-110 (contrast AddCategory ~L87)\n\n## PR / Attribution\n\nPR #100 P3 F17. external Grok Bot from cloud adversarial review PR #100 for Ed Langley\n","status":"open","priority":3,"issue_type":"bug","owner":"box@local","created_at":"2026-09-21T20:49:06Z","created_by":"external-grok-bot","updated_at":"2026-09-21T20:49:06Z","external_ref":"gh-pr-100-P3-additem","labels":["adversarial-review","pr-100"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"improvise-7xyx","title":"FrameGroup::cat is dead in both front ends and its doc comment promises an echo nobody composes","description":"FrameGroup::cat is read by NEITHER front end -- dead in both. Its doc comment\nsays it is \"echoed back in ToggleGroup\", and resolve.rs has NO ToggleGroup arm\nat all, so the echo it promises cannot happen from a browser.\n\nBy design-principles.md section 6 this is the expensive class: a reason that\nlives only in a comment, where nothing checks it against the code. The comment\nis a promise about a client that does not exist.\n\nTwo outcomes are acceptable: compose ToggleGroup in resolve.rs and make the\ncomment true (z is on the silent list anyway, so this would close part of that),\nor delete the field and the comment together. What is not acceptable is leaving\na doc comment describing a protocol interaction no code performs.\n\nAlso noticed in the same pass: AxisFrame::cats is PARTIAL on both ends -- the\ncount is used, the category NAMES are drawn by neither, so the grid's corner\ncell is blank in both surfaces. Possibly intended; worth a decision either way.\n","status":"open","priority":3,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:58Z","created_by":"Edward Langley","updated_at":"2026-09-21T20:39:58Z","labels":["docs","protocol"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-pcje","title":"Render parity: the browser drops col_widths, column ancestry, every AxisColor and the tile overflow","description":"DERIVED at 1f28868 by a mechanical grep of every cache field across src/ui/*.rs,\nsrc/draw.rs, wasm-client/src/render.rs, web/app.js and web/shell.js.\n\nGOOD NEWS FIRST: exposed-but-unpainted is EMPTY. All 22 RenderModel fields and\nevery nested field are read by app.js or shell.js. The four-widget gap that\ndesign-principles.md section 7 records as its motivating case is CLOSED. No\norphan DOM ids either.\n\nWhat differs, none of it silent-wrong, all of it cosmetic or structural:\n\n GridCache::col_widths TUI yes / browser NO -- never reaches RenderModel;\n the browser lets CSS table layout size columns\n GridCache::totals TUI yes / browser NO -- not on RenderModel at all\n GridCache::col_frame browser has no shares_ancestry equivalent, so the TUI's\n selected-column ancestry underline has no counterpart\n PageAxis::{cat,selected} TUI yes / browser NO\n CellDisplay::numeric TUI NO (it right-aligns every cell unconditionally,\n grid.rs:349) / browser yes\n CellDisplay::error read by NOBODY in src/, despite the field's doc saying\n \"rendered distinctly\"\n CellDisplay::id MIRROR-IMAGE PARTIALS: the TUI reads only Aggregate -\u003e\n italic; the browser collapses it to\n editable = matches!(id, Some(Record(_))) and so cannot\n tell Aggregate from Derived from None\n CategoryRow::axis TUI to_string() + axis_fg(axis.color());\n browser axis.initial(), NO COLOUR\n TileEntry::axis TUI short_label() + colour; browser label only\n TileBarCache::tiles TUI horizontal scroll with left/right overflow arrows;\n browser no scroll or overflow handling\n\nThe colour ones are one question, not four: AxisColor has no CSS counterpart at\neither site, unlike MinibufColor which crosses as a semantic enum. Answer it once\n(gotcha 26's shape: the decision is a semantic enum in the shared layer and only\nthe last translation is local) rather than per widget.\n\nCellDisplay::id's mirror-image partials are the one with teeth -- see the\nseparate ADR 002 / editable ticket.\n","notes":"GROOM 2026-09-21: SCOPE NARROWED -- this bead OVERLAPS improvise-p7lh and p7lh owns the intersection.\n\np7lh is the demonstrated one (probed against a real Host) and covers the two gaps that matter most: GridCache::totals (the column-totals strip) and page_coords (the page bar), both of which the server already projects and the browser's cache already holds correctly -- they are simply never painted. p7lh is also a blocker on improvise-bck, because the SHIPPED DEMO has a page axis and no page bar.\n\nWHAT REMAINS THIS BEAD'S, after removing totals and page_coords:\n - col_widths never reaches RenderModel; the browser lets CSS table layout size columns\n - col_frame has no shares_ancestry equivalent, so the TUI's selected-column\n ancestry underline has no browser counterpart\n - CategoryRow::axis and TileEntry::axis cross WITHOUT COLOUR -- AxisColor has\n no CSS counterpart at either site, unlike MinibufColor which does\n - TileBarCache::tiles has no scroll or overflow handling in the browser\n - CellDisplay::error is read by NOBODY in src/, despite the field's doc saying\n 'rendered distinctly'\n - CellDisplay::id: mirror-image partials -- the TUI reads only Aggregate, the\n browser collapses to editable = matches!(id, Some(Record(_)))\n\nThe colour items are ONE question, not four: answer it the way gotcha 26 already answers it for MinibufColor -- the decision stays a semantic enum in the shared layer and only the last translation is local -- rather than per widget.\n\nAlso worth carrying: the sweep found exposed-but-unpainted is EMPTY. All 22 RenderModel fields are read by app.js or shell.js, so the four-widget gap design-principles.md section 7 records as its motivating case is CLOSED.\nSCOPE NOTE 2026-09-21, and a correction to this bead's own framing.\n\nThis bead opens \"GOOD NEWS FIRST: exposed-but-unpainted is EMPTY. All 22 RenderModel fields ... are read\". That was TRUE and it was ALSO the wrong question, which PR #113 demonstrated rather than argued.\n\nThe sweep behind this bead was CLOSED UNDER RenderModel -- it asked whether every field RenderModel exposes is painted. GridCache::totals and page_coords were projected, folded by the browser's cache, and painted by nothing, for the entire period between improvise-n10 and improvise-p7lh, and the sweep truthfully reported an empty set the whole time. A completeness check closed over the artefact that can OMIT a thing cannot see the omission.\n\nPR #113 closed that by adding the question one level up to web/tests/render_surface.test.js: every pub field of every WIDGET CACHE reaches a front end, or is named as debt.\n\nWHAT THAT MEANS FOR THIS BEAD: `grid.col_widths` is now the single named debt entry in that guard, and it points here. So this bead is no longer \"a list someone derived by grepping\"; it is the one item a running check reports. The rest of its original list -- column ancestry, AxisColor, tile overflow -- should be re-derived against the guard rather than trusted from the grep, because that grep had the same blind spot.\n\nThe guard's own residual hole is improvise-t3oh (a whole-cache hand-off proves the cache is consumed, never that every field of it is).","status":"open","priority":3,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-21T20:39:57Z","created_by":"Edward Langley","updated_at":"2026-09-22T02:29:06Z","labels":["browser","parity","ui"],"dependencies":[{"issue_id":"improvise-pcje","depends_on_id":"improvise-p7lh","type":"discovered-from","created_at":"2026-09-21T21:31:56Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"improvise-yae4","title":"Column width overflows before clamping a 65535-column text value","description":"Report-only hunt at 70025e2. No implementation changes.\n\nLocation: crates/improvise-command/src/ui/viewport.rs:66,85\n\nA stored 65535-character ASCII text value causes compute_col_widths/grid_body to panic at w+1 in debug builds. Width is narrowed to u16 before adding the cell gap and clamping to MAX_COL_WIDTH. In release the sum wraps and chooses a minimum-width column instead of the configured maximum. Longer widths can truncate during the earlier as u16 cast even when addition does not overflow. Clamp while still usize or use bounded/saturating conversion before arithmetic. Test widths around 65535 and 65536 as well as ordinary text. Duplicate checks: width, overflow, full existing descriptions.\n\n## Steps to Reproduce\nStandalone Rust 2024 package with improvise and improvise-io path dependencies:\n```rust\nuse improvise_io::workbook::Workbook;\nfn revenue_book()-\u003eWorkbook {\n use improvise_io::model::cell::{CellKey,CellValue};\n let mut w=Workbook::new(\"T\");w.add_category(\"Region\").unwrap();w.add_item(\"Region\",\"East\").unwrap();w.add_item(\"_Measure\",\"Revenue\").unwrap();\n w.model.set_cell(CellKey::new(vec![(\"Region\".into(),\"East\".into()),(\"_Measure\".into(),\"Revenue\".into())]),CellValue::Number(10.0));w\n}\n#[test] fn wide_cell_clamps_without_overflow() {\n use improvise::{ui::app::App,model::cell::{CellKey,CellValue}};\n let mut w=revenue_book();\n w.model.set_cell(CellKey::new(vec![(\"Region\".into(),\"East\".into()),(\"_Measure\".into(),\"Revenue\".into())]),CellValue::Text(\"x\".repeat(65535)));\n let app=App::new(w,None);\n let widths=improvise::ui::viewport::compute_col_widths(\u0026app.model_state.workbook.model,\u0026app.layout,false,0);\n assert!(widths.iter().all(|w| *w\u003c=32));\n}\n```\nRun cargo test wide_cell_clamps_without_overflow -- --nocapture. Current source failed this test.\n\n## Acceptance Criteria\nThe focused regression passes without weakening the expected behavior; ordinary cases still agree, with both debug and release arithmetic considered.\n","status":"closed","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T20:37:41Z","created_by":"Edward Langley","updated_at":"2026-09-21T21:58:44Z","closed_at":"2026-09-21T21:58:44Z","close_reason":"Fixed in the 2026-09-21 wave, integrated into main at 1574 passed / 0 failed / 7 ignored, all gates green, every guard fault-injected.\n\nCapped while still usize in the one place every measurement enters (widen). Behaviour identical for every non-overflowing input.\n\nTHE SECOND INSTANCE, which the bead did not name and which was the more dangerous: improvise_protocol::header_level_widths had byte-for-byte the same defect, on the axis gotcha 44 calls the one home for the width rule -- and it is in the PROTOCOL, so it is shared by both front ends. Fixing only the site the bead named would have left the row header and the column widths with two different answers to the same arithmetic.\n\nThe release symptom is the one to lead with, not the panic: the WIDEST value in the document silently rendering at the NARROWEST column width, with no failing test anywhere. Faults injected: debug -\u003e 'attempt to add with overflow'; release -\u003e left [5], right [32], the wrap reported as the minimum width.\n\n'as u16 followed by + 1 and .clamp' is a greppable shape and the grep finds both.","dependency_count":0,"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}