kb: a mitigation that names the wrong file is not a mitigation
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled

PR #91 deferred a wire-level gap because 'keys.rs emits no ChangeMode at
all'. True, and irrelevant: keys.rs holds only BrowserKey, and the live
browser resolver is improvise_protocol::resolve, which emits
ChangeMode(AppMode::Normal) at five sites. The browser front end kept the very
bug the PR fixed for the TUI, and the suite was green over a divergence the PR
created.

I copied that false clause straight into improvise-e5pf when I filed it.
Corrected, and raised to P2: it is live, not latent.

The rule: before writing 'X does not do Y' as the reason a gap may be
deferred, grep for Y rather than for X. The claim that matters is about the
behaviour, and the file you happen to have open is not the only place it lives.

Also records that improvise-sihr's prose defended its staleness with an
example that works for a different reason — toggle-records-mode overwrites
normal_mode()'s answer in both directions, so the staleness is neutralised
there rather than exercised, and the case that would have tested it regressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 04:15:16 -07:00
parent 27399b6a28
commit fe816c9734
2 changed files with 5 additions and 1 deletions
+3 -1
View File
@@ -40,6 +40,7 @@
{"_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-e5pf","title":"Command::ChangeMode(Normal) is on the wire and reduce_view folds it without the records resolution","description":"Found by the /code-review pass on PR #91 (improvise-sihr), and CORRECTED by the adversarial review of the same PR — the first version of this ticket, and the PR's own prose, both said it was latent. It is not.\n\nimprovise-sihr made AppMode::Normal resolve through App::normal_mode(), so a minibuffer opened over a records grid returns to RecordsNormal. That resolution lives on the App. Command::ChangeMode(AppMode::Normal) is also on the WIRE, and improvise-protocol/src/reduce.rs:195 folds it as `view.mode = mode.clone()` with no resolution.\n\n## It is LIVE, not latent\nThe original claim was \"keys.rs emits no ChangeMode\". True and irrelevant: keys.rs holds only BrowserKey. The live browser key resolver is improvise_protocol::resolve, re-exported as improvise_wasm_client::resolve (wasm-client/src/lib.rs:8), and it emits Command::ChangeMode(AppMode::Normal) at resolve.rs lines 369, 692, 972, 1461 and 1613. Verified independently.\n\nSo the BROWSER FRONT END STILL HAS THE SIHR BUG. Demonstrated with a lockstep-shaped probe (resolve + ClientState::fold against reduce_key on the host; enter records with R, open a panel, Esc):\n\n left: Normal (client)\n right: RecordsNormal (host)\n\nNothing in crates/improvise-wasm-client/tests/lockstep.rs covers it.\n\n## And it compounds\nOnce the client's mode reads plain Normal, its only records-ness fact — state.view.mode.is_records(), read at resolve.rs:686 and :708 — is false. A subsequent commit over a records grid then takes the non-records branch.\n\n## Why it cannot be closed the obvious way\nA client holds a GridCache, not a GridLayout, so \"is this grid records-shaped\" is not a fact it has — and by the time Esc arrives from CategoryPanel the mode no longer says records, so the fact is genuinely gone. Moving the resolution client-side would be a SECOND DERIVATION of records-ness, which is improvise-edp's disease.\n\n## Acceptance Criteria\n- The projection carries what the client needs to settle the mode correctly. Gotcha 47's shape: where a client cannot compose the answer, ENRICH THE PROJECTION rather than moving resolution back to the host.\n- Or: a written argument that ChangeMode(Normal) should not be on the wire at all, plus its removal.\n- A lockstep test covering the divergence above, so the suite stops being green over it.\n\nDo NOT close this by having the client re-derive records-ness from the axes.\n","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:04:35Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:15:00Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-9c9o","title":"A geometry effect right after a mid-batch SwitchView skips the cursor check entirely","description":"Introduced by PR #78 round 2. PASSES on pre-PR main (0947fd0) and on round-1 head (bcccfde); FAILS at d9cb16c.\n\nRound 1 compared frame hashes across a view change, which threw a cursor away spuriously. Round 2 added a 'was == self.view_name()' guard, which fixes that — but it also exempts the FIRST geometry effect after a mid-batch SwitchView from the check altogether, because there is no basis for the new view yet, so the code re-bases and skips.\n\n## Steps to Reproduce\n app.apply_effects(vec![SwitchView(\"Second\"), TransposeAxes]);\nBoth preconditions hold: the switch lands, and row_count() really changes. Yet cursor_epoch does not move and the cursor stays at (2,0) in a transposed grid.\n\n## Acceptance Criteria\n- The probe above lands as a guard.\n- The basis re-bases to the new view AT THE SWITCH rather than at the next geometry effect — i.e. the basis follows active_view changes, which SwitchView being ViewOnly currently prevents. Not a one-liner.\n\nWHY THE DIRECTION MATTERS: round 1 threw away a cursor that should have been kept; round 2 keeps one that should have been thrown away. Gotcha 35's asymmetry says under-reporting is the expensive side — a stale cursor has nothing in the stream to correct it, where an extra invalidation costs one redundant projection.\n\nReachability is the same narrow class as the bug it replaced: a . -chained : line, or any future Binding::Sequence composing the two. DrillIntoCell and EnterRecordsMode hit the pattern constantly but their target view is freshly created, so it is benign there. Merged on that basis rather than blocked.","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T05:15:07Z","created_by":"Edward Langley","updated_at":"2026-09-21T05:15:07Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-iot6","title":"render_csv builds GridLayout::new, so CSV export carries rows the view prunes","description":"FOURTH instance of the GridLayout::new versus with_frozen_records trap, and the first one in production that a user can see.\n\ncrates/improvise-io/src/persistence/export.rs:33 builds GridLayout::new(\u0026workbook.model, view), which does NOT apply prune_empty. So :export in the TUI and the CLI export path both write rows the grid does not draw.\n\n## Steps to Reproduce\nFixture: Region {North, Service, South}, Revenue set for North and South only, prune_empty = true. The view draws 2 rows (assert this as the precondition).\n\nrender_csv output:\n Region,Revenue\n North,1\n Service, \u003c- pruned on screen, exported anyway\n South,1\n\n## Acceptance Criteria\n- render_csv honours the view's prune_empty, via the same path grid_in now uses.\n- A test whose precondition asserts the view really prunes (a row that is actually empty, pruning left ON in the view). A fixture that turns pruning off, or has nothing to prune, is vacuous — that exact vacuity was demonstrated while writing the improvise-t6db property.\n- None of export.rs's three existing tests mentions pruning; that is why this shipped.\n\nPre-existing, not a regression from PR #78. Found by the adversarial review of #78 while enumerating every GridLayout::new call site: three are outside test modules, two of those are test helpers, and this is the third.","status":"closed","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T05:14:59Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:09:27Z","closed_at":"2026-09-21T11:09:27Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-zv8t","title":"fit_measure never tells preprocess which columns are categories, so a numeric- or date-valued category is unspellable","description":"fit_measure never tells preprocess which columns are categories, so a numeric- or date-valued category is unspellable\n\nFound reviewing improvise-wd6n's branch, and it was very nearly foreclosed in\nprose: the first draft of context/code/gp.md §11 called Artefact::NumericCategory\na permanent refusal that \"no vocabulary closes\". That is wrong, and this is the\nticket the prose would have prevented.\n\ncrates/improvise-gp/src/fit.rs passes `\u0026Overrides::default()` to `preprocess`,\nso it never tells preprocess which header columns are document categories --\nalthough the very next line does tell `Features::of`. `preprocess` then infers\nNumeric -\u003e Date -\u003e Categorical from the cell values, so:\n\n * a regular category whose items all parse as numbers (a `Year` category with\n items 2024/2025) becomes ColumnKind::Numeric, and its feature is refused as\n Artefact::NumericCategory\n * a regular category whose items are all ISO dates becomes ColumnKind::Date,\n and its features are refused as Artefact::DatePart\n\nIn both cases `Year IS 2024` is EXACTLY spellable, and the fit is refused\nanyway. Unlike the two families improvise-wd6n declines, this needs no\nformula-language change at all: `Overrides::force[col] = Forced::Categorical`\nalready exists (gp-tree/src/data.rs, consumed in preprocess's `match ov.force`)\nand is the knob. fit.rs knows which columns are categories -- it computes\n`category_columns` on the line above.\n\nThe refusal text for NumericCategory argues about `Expr::Ref` resolving to a\n_Measure item of the same name, which was never the right spelling for a\ncategory column. It also sits awkwardly beside this crate's own rule (gotcha\n37(d), gp.md §11: the check asks the document, never the name's shape) and\nbeside a_coordinate_is_spelled_as_one_even_when_an_item_shares_the_one_hots_name,\nwhich exists precisely to show the two spellings cannot collide.\n\nNeither NumericCategory nor AbsentCoordinate has a test today; they were\ninvented by the wd6n branch and only three of the five families are reached by\nits fixture.\n\n## Acceptance\n\nA fit over a view with a numeric-valued or date-valued regular category emits a\n`Cat IS Item` coordinate for it rather than being refused, demonstrated by a\ntest that fails before the fix. Whatever forcing is applied must not change how\na MEASURE column is inferred -- a numeric measure is still Numeric. If forcing\nturns out to be wrong for some reason not visible from here, say so in\ncontext/code/gp.md and correct the \"permanent refusal\" paragraph either way.\n","status":"closed","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T05:05:31Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:09:28Z","closed_at":"2026-09-21T11:09:28Z","dependency_count":0,"dependent_count":0,"comment_count":0}
@@ -194,7 +195,8 @@
{"_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-e5pf","title":"Command::ChangeMode(Normal) is on the wire and reduce_view folds it without the records resolution","description":"Found by the /code-review pass on PR #91 (improvise-sihr), and checked rather than assumed.\n\nimprovise-sihr made AppMode::Normal resolve through App::normal_mode(), so a minibuffer opened over a records grid returns to RecordsNormal. That resolution lives on the App. Command::ChangeMode(AppMode::Normal) is also on the WIRE, and reduce_view folds it into a client's ViewState WITHOUT the resolution, so a thin client would settle into plain Normal while its layout is still records.\n\n## Why it is latent rather than live\n- keys.rs emits no ChangeMode at all; the only wasm-client occurrences are its own tests.\n- apply_command's catch-all arm returns Dirty::NOTHING, so a view-half command never reaches the host's own slice (gotcha 36).\n\nBoth were verified, not assumed.\n\n## Why it cannot be closed the obvious way\nA client holds a GridCache, not a GridLayout, so \"is this grid records-shaped\" is not a fact it has. Moving the resolution client-side would be a SECOND DERIVATION of records-ness, which is improvise-edp's disease.\n\n## Acceptance Criteria\n- The projection carries what the client needs to settle the mode correctly. Gotcha 47's shape: where a client cannot compose the answer, ENRICH THE PROJECTION rather than moving resolution back to the host.\n- Or: a written argument that ChangeMode(Normal) should not be on the wire at all, plus its removal.\n\nDo NOT close this by having the client re-derive records-ness from the axes.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:04:35Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:04:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-26dm","title":"The fourth length-not-completeness list: cases.len() == 13 in ui/effect.rs","description":"improvise-5ubs is closed for the two CYCLIC all() functions, by reading serde's\nderive-generated variant list out of its \"unknown variant ... expected one of\"\nmessage and diffing it against the names the cycle reached. That closes the\nAppMode and Rejection halves and the mechanism generalises to any serde enum.\n\nIt does NOT reach the FOURTH instance 5ubs records in its notes:\nlayout_geometry_effects_actually_change_document_config in\ncrates/improvise-command/src/ui/effect.rs, whose assert_eq!(cases.len(), 13) is\nover a hand-built list of EFFECTS, not of enum variants. serde has nothing to\nsay about it: an effect is a struct implementing a trait, so there is no derived\nenumeration to diff against.\n\nThe general statement stands and is the reason this is worth its own ticket:\ndoctrine(a_list_checked_for_length_reads_like_a_list_checked_for_completeness).\nA count catches a case being DELETED from the list and never one being ABSENT\nfrom it, and the two are written identically as an assert_eq over a number. The\nadversarial review of PR #78 added a fourteenth pub struct declaring\nEffectKind::Model(ModelChange::LayoutGeometry), pointed at by nothing, and the\nwhole suite stayed green.\n\nWhat would close it: an enumeration of the effects the compiler maintains. The\ncandidates are a proc-macro over the impl blocks, or inventory-style\nregistration, or reading the Debug names off a derived list the way\nevery_model_command_names_the_effect_of_the_same_name already does for the\nModelCommand mapping -- that last one is the closest existing precedent in this\nrepo and is worth looking at first.\n\nNot fixed in wave I because ui/effect.rs was another wave's ground that round,\nand because a fourth hand-rolled attempt in a fourth place is the disease 5ubs\nnames.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:09:56Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:09:56Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-6nsd","title":"A prune-by-write renumbers the grid without moving layout_version -- ADR 001 middle tier","description":"A cell write that prunes a row renumbers the grid without moving layout_version.\n\nMeasured 2026-09-21 while answering improvise-9v0j, by driving each ModelCommand\nthrough apply_command and reading cursor_epoch() and layout_version().\n\nEvery command in reshaping_commands() moves both counters. A ClearCell that\nprunes a row out of a pruning view moves the cursor epoch and NOT layout_version:\n\n ClearCell(prunes a row) reframe=true reshape=false\n\nThat is not a defect in the observation. improvise-mcq4 designed it that way: a\ncell write changes DATA, not the view's CONFIGURATION, and view_shape hashes\nconfiguration. So 9v0j's candidate property test (\"is every reframe also a\nreshape?\") would FAIL and writing it would be wrong. That half is answered and\nrecorded in context/code/command.md.\n\nWHAT IS OPEN, and it is an ADR question rather than a test to land quietly.\nAfter such a write the grid has renumbered while layout_version has not moved,\nso a client still claiming the OLD layout tier is accepted. The record tier\ncovers a write to the PRUNED record -- that client's picture of Service is\nstale. It says nothing about a write to a SURVIVING record whose row index moved\nunderneath. Whether that is a hole in ADR 001's middle tier, or correctly\ndelegated to the record tier because the record surrogate is position-free\n(ADR 002), is the question.\n\nNote which way the asymmetry falls if it IS a hole: gotcha 35 says\nunder-reporting leaves a client wrong forever, and layout_version under-reports\nhere by construction.\n\nReachability: needs two clients on pruning views and a cell write that empties a\nrow. The cursor invalidation half already works (mcq4), so the user-visible\nsymptom would be a basis check accepting a batch composed against a grid shape\nthat no longer exists -- not a wrong cursor.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T11:09:49Z","created_by":"Edward Langley","updated_at":"2026-09-21T11:09:49Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-e8no","title":"A wide numeric category now refuses a fit that used to ignore it","description":"improvise-zv8t forces every document category to `Forced::Categorical`, which is\nright (a `Year` category with items 2024/2025 is a coordinate, not a number).\nThe cost: a WIDE numeric category one-hots past `MAX_FEATURES` and refuses the\nwhole fit, where before it encoded as one numeric feature and a fit that never\nsplit on it succeeded.\n\nMeasured on 100 records with a 100-item numeric `Year` beside a `Cost` that\npredicts `Score` on its own:\n- pre-zv8t: fit lands on `IF(Cost \u003c 48.015...)`; `Year` never appears.\n- post-zv8t: preflight refuses at 101 features.\n\nPinned by `a_wide_numeric_category_now_refuses_a_fit_that_used_to_ignore_it`\nin crates/improvise-gp/src/fit.rs, and argued in context/code/gp.md section 11.\n\nThe workaround the refusal names works: mark the category a Label and the\npre-zv8t feature set is restored exactly. What is lost is the case where the\nuser wants neither -- a wide numeric category left alone while the fit uses the\nmeasures.\n\n## Steps to Reproduce\n\nBuild a model with a Regular category `Year` holding 100 numeric items\n(1900..1999), a Label category for record identity, and two measures `Cost` and\n`Score` where Score = 4000 if Cost \u003e 50 else 1000. Put `Year` on Axis::Row and\n_Measure on Axis::Page. Run `fit_measure(.., \"Score\", ..)`.\n\nPre-zv8t it lands on `IF(Cost \u003c 48.015..)`. Post-zv8t it is refused with\n\"would encode 101 features from 100 records\". The fixture is\n`wide_category_beside_a_good_measure()` in crates/improvise-gp/src/fit.rs.\n\n## Acceptance Criteria\n\nThe honest fix is a budget that can decline A COLUMN rather than the whole\ntable: `encoded_width` (and the exact check beside it) would drop the widest\ncategory and report it as a `Dataset::warnings` notice -- gotcha 37(a), it comes\nback as DATA and nothing prints. That keeps the zv8t decision intact (a category\nis still never inferred numeric) and changes only what the budget does when it\ncannot afford one.\n\nDo NOT close this by making the forcing conditional on cardinality: that is\ninference by another name, and makes the ENCODING depend on a budget rather than\non what the document says.\n","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T10:55:32Z","created_by":"Edward Langley","updated_at":"2026-09-21T10:55:39Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-sxsf","title":"cargo test -- --list cannot name the binary, so two crates could share a full test path","description":"MEASURED 2026-09-21 (PR #88). Re-verified latent: 1444 names, zero duplicate full paths, zero duplicate (crate, path) pairs.\n\nTHE PROPOSED FIX IS DECLINED ON TWO INDEPENDENT GROUNDS, and the second is the interesting one:\n\n1. COST: 45.2s against 2.4s for the one workspace call (11 members, warm) — 18x. And --check-evidence needs the --ignored twin, so the honest figure is 22 cargo invocations.\n\n2. IT DOES NOT CLOSE THE CLASS IT NAMES. A package is not a binary. The improvise crate alone has FIVE integration test binaries beside its lib, and an integration test's top-level fn lists with NO MODULE PATH AT ALL — 60 of 1444 names are bare. So '-p improvise' prefixes all six identically and tests/a.rs::foo still cannot be told from tests/b.rs::foo.\n\nAny real fix is per-TARGET (--lib, --test \u003cname\u003e), not per-package. Whoever takes this should start there.\n\nWorth noting how this was found: the agent set out to time the per-package fix and discovered it does not work while reading the target list. The cost measurement was the excuse for reading it; the correctness finding was the payoff — an argument for measuring declines rather than asserting them.","status":"open","priority":3,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-21T04:49:16Z","created_by":"Edward Langley","updated_at":"2026-09-21T05:23:48Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-8shu","title":"Constrain the .improv grammar generator's strategy; improvise-io's lib binary is the run's outlier","description":"Split out of improvise-8ec3, whose second half (stop pinning one machine's number) is done. This is the first half, left open.\n\nimprovise-io's lib binary is an order of magnitude above every other binary in the run on every machine it has been measured on: 1.40-1.45s over three runs here (vs a ~0.06s median), 3.70-5.24s on the machine that filed 8ec3. The ratio is the portable signal; the seconds are not.\n\nIts slow half is the grammar-walking generator in persistence/parse.rs plus the .improv roundtrip proptests at the default 256 cases. A grammar walk with no size bound generates deeply nested documents whose cost is not uniform across cases, so a few cases dominate.\n\ndoctrine(proptest_cases) rules out the cheap fix: constrain the strategy, do NOT lower the count below 256.\n\nAcceptance: improvise-io's lib binary is no longer an order of magnitude above the run's median, with the case count still 256 and the properties still covering groups, formulas, views, hidden items and pipe-quoting edges. Report the before/after ratio, not seconds.","status":"closed","priority":3,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-21T04:14:01Z","created_by":"Edward Langley","updated_at":"2026-09-21T05:15:42Z","closed_at":"2026-09-21T05:15:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
+2
View File
@@ -274,6 +274,8 @@ doctrine(a_macro_effect_cannot_carry_a_derivation,'ChangeMode was a simple_effec
doctrine(a_ticket_that_pre_diagnoses_its_fix_as_a_protocol_change_deserves_checking,'improvise-9c9o was filed saying the honest fix needs the basis to follow active_view, which SwitchView being ViewOnly prevents -- i.e. a protocol change, not a one-liner. It was neither. The basis is App own bookkeeping and none of it is on the wire, so \"the basis follows active_view\" was achieved by moving WHERE the re-base happens -- into the loop, at the switch -- and SwitchView stayed ViewOnly. A pre-diagnosis in a ticket is a hypothesis wearing the authority of a decision, and the author of the ticket is usually the person with the least time to check it.').
doctrine(prose_that_calls_a_change_free_is_a_claim_and_is_checkable,'The sibling of prose_that_calls_something_permanent_forecloses_the_ticket, found one paragraph below it in the same section. gp.md section 11 said improvise-zv8t cost nothing -- \"no document that fitted before stops fitting\" -- reasoning that a tree splitting on a numeric category was refused as an artefact anyway. FALSE: the refusal is conditioned on used_features, so a fit that never split on the column SUCCEEDED, and the wide columns are exactly the ones a fit has least reason to split on. Measured with a throwaway ignore-probe on both sides: 100 records, a 100-item numeric Year beside a Cost that predicts Score alone -- pre-fix the fit lands on IF(Cost < 48.015..) with Year never touched, post-fix the preflight refuses at 101 features. The suite was green on either reading. A cost claim is as checkable as a permanence claim and is checked the same way: construct the document the claim says does not exist.').
doctrine(forcing_by_name_while_budgeting_by_index_defeats_the_budget,'improvise-zv8t made fit_measure force header[..category_columns] to Forced::Categorical keyed by NAME, while encoded_width decides category-ness by INDEX. A document may legally name a regular category and a raw measure the same thing (a_category_and_a_measure_of_the_same_name_are_refused ships that fixture), so the MEASURE column was forced categorical by name match while the preflight still counted it numeric by index: measured 41 against 80 at 40 records. no_ambiguous_column_names ran AFTER preprocess, so the expansion the preflight exists to prevent was allocated and only then refused -- guard_the_cost_not_the_result defeated by an inconsistency the fix itself introduced, NOT the bounded approximation a_budget_check_is_advisory_or_exact_and_must_say_which licenses. Repair is a data dependency, not a moved line: as_categories runs the check and returns Result<Overrides>, so the Overrides preprocess needs cannot exist until the header is accepted. Guard is the SIGNATURE -- reordering the two statements is error[E0425], verified. Deliberate deletion of the check from as_categories is still possible and is stated rather than implied.').
doctrine(a_mitigation_that_names_the_wrong_file_is_not_a_mitigation,'PR #91 deferred a wire-level gap on the ground that \"keys.rs emits no ChangeMode at all\". True, and irrelevant: keys.rs holds only BrowserKey, and the LIVE browser key resolver is improvise_protocol::resolve, re-exported as improvise_wasm_client::resolve -- which emits Command::ChangeMode(AppMode::Normal) at five sites. So the browser front end kept the very bug the PR fixed for the TUI, and the suite was green over a divergence the PR itself created. The sentence was in context/, which design-principles section 6 names as exactly the place a reason must not be wrong, because nothing can find it later. Before writing \"X does not do Y\" as the reason a gap may be deferred, GREP FOR Y rather than for X -- the claim that matters is about the behaviour, and the file you happen to have open is not the only place it can live. The orchestrator copied the same false clause into the bead it filed.').
doctrine(an_example_that_works_for_another_reason_defends_nothing,'improvise-sihr resolved AppMode::Normal against a pre-batch layout (gotcha 23) and defended the staleness in prose with the case of toggle-records-mode, which does work in both directions. It works for a DIFFERENT REASON: ToggleRecordsMode emits change_mode(RecordsNormal) explicitly on entry, and on exit ViewBack restores the mode SwitchView pushed, so normal_mode() answer is overwritten in both directions and the staleness is neutralised there rather than exercised. The case that would actually test it -- a batch that CROSSES records-ness and then sets Normal -- was untested, and regressed: from a records grid, \":toggle-records-mode . enter-mode normal\" landed in RecordsNormal where it was right before. An example chosen because it passes is not evidence; ask WHY it passes.').
work_done('improvise-8tk',bug_fix,'ZZ/:wq quit even when the save failed, silently discarding the model. Save now aborts the effect batch on failure; save_to_current_path/1 returns Result<String,String>; three tests in ui/effect.rs.').
work_done('improvise-907',bug_fix,'Import wizard step machine redesign: validation_error() gate at ReviewProposals (no-category and over-limit), history-stack previous() bound to Left on every step, and Done made a real confirmation step holding the built workbook. Superseded improvise-gfq, improvise-o1c, improvise-c6j.').
work_done('improvise-6sq',wont_do,'Merging EditOrDrill and EnterEditAtCursorCmd into one EnterEdit{mode} is not safe: they differ in WHEN the cell value is read, not in a mode parameter. Added ui/app.rs::tests::i_and_o_diverge_on_an_empty_records_grid as executable evidence.').