kb: the wave-3 batch, and three corrections to clauses that had gone false
Applied through the MCP cycle (loadSession, four loadProgram calls, saveSession),
not by editing the file. Counts verified against the live file immediately before
writing, because the first read of them came from a working tree six merges behind
and would have deleted three clauses while reporting a larger total.
doctrine 56 -> 95 (+39)
invariant 51 -> 64 (+13, one of them a rename)
decision 48 -> 64 (+16)
work_done 20 -> 23 (+3)
Verified: kb_consistent passes (exit 0, the exact CI invocation); unique names
equal totals for all three keyed predicates; a name-by-name diff against
01ad91a reports exactly one pre-existing name absent -- the deliberate rename --
and no decision lost.
Three clauses were corrected rather than reproduced verbatim, each verified
against the tree first:
- invariant(shipped_demo_cells_are_records) was FALSE and is renamed
shipped_demo_cells_are_settable_or_derived. Both its guards said "every cell
is a Record" and passed while that was false for a third of the landing-page
grid (improvise-ir7l).
- decision(improvise-bck, demo_built_against_an_assertion) carried the same
stale wording.
- doctrine(a_fact_stated_twice_is_changed_by_grep) gained the observation that
the commit correcting one copy is the one most likely to miss the others.
One further amendment, disclosed rather than silent: decision(improvise-9x6,
worker_persists_on_its_own_behalf) asserted MarkDirty still exists and that the
worker triggers on the command variant. improvise-gv9r removed MarkDirty and
made the trigger the flag, so the clause contradicted
invariant(the_persist_trigger_is_an_observation) in the same file. It now says
so and points at the invariant.
The MCP re-added its eight host_rpc shims on save; stripped. Note AGENTS.md
enumerates seven -- js_eval_json is missing from that list, so a grep built from
it is one case short.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
+80
-9
@@ -93,7 +93,7 @@ invariant(grid_extent_is_the_whole_grid,'project::grid_of reads GridExtent from
|
||||
invariant(client_frame_commands_is_a_batch,'One keypress resolved client-side is SetSelected plus the offsets viewport_effects returns; a host that folds one and not the other projects the wrong window. Since improvise-955n a frame is answered WHOLE by CheckedBatch::against before anything is applied, so Applied::Rejected is unreachable through Host::commands. Clause 4 (all-or-nothing on an apply-time failure) is NOT delivered: App::apply_effects aborts without undoing.').
|
||||
invariant(opfs_index_holds_documents_not_entries,'OpfsFs::list joins dir like the other two backends, so storage_contract takes dir as a parameter and the three agree at EVERY directory (improvise-vhcz), not only where joining is the identity. The index holds DOCUMENTS: attach indexes nothing (what it creates is zero bytes), write indexes the name and un-queues a pending delete, refresh rebuilds from OPFS skipping zero-byte entries but counting an unmeasurable one as a document, since the only cause is another tab holding the exclusive handle (improvise-vvl3).').
|
||||
invariant(async_seam_does_not_survive_boxing,'OpfsFs::open/attach/refresh/commit_deletes are on the struct, not on Storage, and App::storage is a Box<dyn Storage>: handing the backend to the App puts the seam permanently out of reach. The host holds the concrete backend and gives the App a shared handle, which is impl<T: Storage + ?Sized> Storage for Rc<T> in improvise-io (improvise-hmws) -- there because the trait is local there, blanket because nothing about it is OPFS-shaped, and Rc not Arc because wasm32 has one thread.').
|
||||
invariant(shipped_demo_cells_are_records,'web/demo.improv projects only CellId::Record; guarded twice, in tests/web_demo_model.rs against grid_of and in post_message.rs against the real seed-VFS-open-connect-Snapshot chain.').
|
||||
invariant(shipped_demo_cells_are_settable_or_derived,'web/demo.improv projects CellId::Record for its two raw measure columns and CellId::Derived for Profit, which a formula owns -- never an Aggregate. Guarded twice and two-sidedly, in tests/web_demo_model.rs against grid_of (the two arms are CellId::Record(_) with editable false and CellId::Derived{target} with target == \"Profit\") and in post_message.rs against the real seed-VFS-open-connect-Snapshot chain. Before improvise-ir7l both guards said \"every cell is a Record\" and PASSED while that was false for a third of the landing-page grid.').
|
||||
invariant(matching_cells_yields_insertion_order,'DataStore::matching_cells/matching_values return the primary IndexMap order. Measured at 18,000 cells: +20% worst case on matching_values, matching_cells 7% FASTER because sorted positions resolve CellKeys with better locality.').
|
||||
invariant(a_stored_fit_names_only_what_the_document_holds,'fitted_formula refuses a tree splitting on a feature Model::find_item_category cannot resolve. It asks the DOCUMENT, not the name shape: a measure literally named Region=North is legal and indistinguishable from a one-hot as a string.').
|
||||
invariant(the_frame_is_answered_once_not_the_command,'Since improvise-955n, CheckedBatch::against answers a whole ClientFrame::Commands before anything is applied. The two DOCUMENT tiers (document_generation, layout_version) hoist to the frame; the RECORD tier does not, because which records are at stake is a property of the command (records_named). Hoisting all three was a lost update: [SetCell(North), SetCell(South)] overwrote a South another client had moved, silently, invisible to the other two tiers because a SetCell is CellData. Every mutation answers for its own records against the FRAMEs basis -- never its own field, or a later command could claim a fresher picture than the frame was composed under.').
|
||||
@@ -101,11 +101,24 @@ invariant(the_create_exemption_is_a_property_not_a_list,'A record that NEITHER r
|
||||
invariant(a_cats_change_reidentifies_every_position,'A cells identity is AxisFrame::cats zipped with its label tuple, so GridCache::invalidate_relabelled blanks the WHOLE axis when old.cats != new.cats -- not the positions whose tuples differ -- and blanks the totals on a recategorization of EITHER axis, since a column total sums over the rows. It is applys job and not the producers, for the reason the whole fold works that way: an update must be correct with no follow-up behind it (gotcha 33b).').
|
||||
invariant(the_header_width_rule_has_one_home,'improvise_protocol::header_level_widths: per level, the widest label plus a gap, clamped (improvise-1125). It lived twice -- inline in src/ui/grid.rs::render_grid and again in ui::viewport::compute_row_header_width -- and a browser sizing its own row header would have been a third. It takes a borrowed iterator rather than an AxisFrame because its other caller holds a GridLayout on the keypress path and building a frame there would clone every row tuple on every key.').
|
||||
invariant(saving_is_a_frame_not_a_command,'ClientFrame::Save (improvise-368h) carries no path -- a client cannot see the hosts storage -- and is a FRAME because as a Command it put a runtime-fallible operation inside a batch ClientFrame::Commands promises is whole-or-nothing, so [Save, SetCell] reported a failed write and applied the edit anyway. It also fails the test every Command passes: it mutates no shared state and no clients slice, so reduce_view has nothing to fold and Checked has no basis to answer for. Refused as NoDocumentPath or SaveFailed to the asker alone; success is silent.').
|
||||
invariant(the_persist_trigger_is_an_observation,'improvise-worker-server persists on model_state.dirty, not on a table of Command variants (touches_document is deleted) -- the same choice as watching cursor_epoch rather than matching CycleAxis, and document_generation rather than matching LoadModel. It is strictly tighter: a refused batch raises nothing, so the "was anything rejected" guard went with it.').
|
||||
invariant(the_persist_trigger_is_an_observation,'improvise-worker-server persists on model_state.dirty, not on a table of Command variants (touches_document is deleted) -- the same choice as watching cursor_epoch rather than matching CycleAxis, and document_generation rather than matching LoadModel. It is strictly tighter: a refused batch raises nothing, so the \"was anything rejected\" guard went with it.').
|
||||
invariant(the_harness_runs_the_worker_chain_under_chromium_only,'harness/ (improvise-vvl3/xf1) executes improvise_io::storage::contract::storage_contract against a real OpfsFs in a real dedicated Web Worker under Chromium and Firefox, gated by the browser job in CI; two bugs fell out of the first run. Playwrights WebKit provides no OPFS at all (getDirectory rejects with UnknownError), so xf1s three-engine criterion is met for two of three and the project is kept so it starts the day that build gains OPFS. harness/tests/shell.spec.js runs web/app.js under CHROMIUM ONLY -- engine-independent logic, and routeWebSocket is slower across three. It also runs BINDINGS.RS: its no-?ws test loads the app, which spawns web/worker.js, which imports the glue, which calls bindings.rs start() and OpfsFs::open(), and the test then asserts a grid draws. This invariant was named opfs_is_run_but_bindings_is_not and asserted the opposite in FIVE places for months (here, the crate/3 fact above, two context/repo-map.md lines and improvise-bck) -- every copy written before the harness landed and none re-read after. A never-run claim in prose is unfalsifiable by anything in this repo: the CI job checks this KB for CONSISTENCY, not truth, and the error UNDERSTATED coverage so nothing failed and nobody looked. What is genuinely unrun, BY ENGINE: Safari/WebKit (no OPFS in Playwrights build) and any non-Chromium engine for the shell spec. That is deliberately scoped to engines rather than written as a closed list of everything unreached, because a closed list is how the previous version of this invariant went wrong in the other direction: two wasm_bindgen exports on WorkerServer, documents() and refresh(), have NO caller in web/*.js or harness/ and so are run by nothing (the .refresh( hits in harness/opfs-contract are OpfsFs::refresh, a different method). The lesson the rename is about applies to its own replacement: a coverage claim in prose is unfalsifiable by anything in this repo, so state what IS covered and scope what is not, rather than enumerating an absence.').
|
||||
invariant(a_different_permission_is_a_different_cellid,'ADR 002 clause 1 applied rather than departed from: CellId has four variants, and each names a permission. Record is SetCell, Aggregate is Drill, Derived is a cell a formula owns (no surrogate, so no write is constructible -- improvise-ir7l), Create is a records-mode creating column whose operand is a NAME (improvise-poia). None means only \"no projected cell\". cell_id asks derived BEFORE the record branch, since a formula target is a real stored coordinate, and AFTER the aggregate branch, since a formula cell that aggregates is still drillable (gotcha 10) -- reversing the two turns a drillable cell into a dead one.').
|
||||
invariant(the_page_filter_split_lives_in_the_type,'GridLayout::page_coords is Vec<PageAxis>, and PageAxis::items is Some exactly for Axis::Page, so a Filter axis carries no list and no caller can compose a cycle for one (gotcha 27). page_cat_data and the browsers page_cycle both READ it; neither spells the rule again. The list is effective_item_names -- the same call that chooses the default selection, so the selection and the list it indexes into agree by construction -- and is NOT filtered to visible items, because the TUI cycles hidden ones.').
|
||||
invariant(the_title_bar_cannot_be_gated_by_an_effectkind,'dirty is raised by any EffectKind::Model(_) and cleared by effect::Save, which is ViewOnly; SaveAs moves file_path and is also ViewOnly. So Dirty::of reports NOTHING for exactly the effects that change this widget most visibly, and gating it would leave every client showing [+] for a saved document forever. ClientSession::document_update OBSERVES instead -- build the desired cache, diff against the mirror -- the same answer record_versions got. stored_as is a NAME, never a path: a socket server projecting file_path would hand a remote client the hosts directory layout, and None carries the fact ClientFrame::Save fails on.').
|
||||
invariant(a_front_end_owns_an_event_map_and_painting,'improvise-0yq8 moved ClientState and resolve/step into improvise-protocol: both depend only on ViewState, the widget caches and the announced window, all already protocol types, so the move cost no dependency. What a new front end still writes is its own event -> KeyEvent mapping and its painting. improvise-wasm-client/src/keys.rs is BrowserKey and nothing else, 1273 lines -> 188. tests/lockstep.rs STAYS: it holds resolve against improvise_command::ui::reduce::reduce_key, the host Cmd/Effect path -- two derivations in two crates, one unreachable from a browser -- so moving a mirror between crates does not stop it being one.').
|
||||
invariant(a_ui_string_is_keyed_by_a_command_never_by_a_key,'improvise-protocol::help owns mode_label, hint_line, which_key and pages; AppMode::label/hint_text are gone (improvise-k099) so the enum that crosses the wire carries no English. Hint{keys,command,caption}: keys looked up in Keymaps::default_keymaps(), command the semantic value a localized client reads, caption the default English -- semantic_palette_at_the_core_boundary applied to text. Three rendering rules, each with a silently-wrong alternative: hint_line drops chorded keys, runs single-character keys together and slashes the rest, and drops a hint that compacts to nothing (a chord-only command rendered a bare \":caption\"). A help row whose command resolves to no keys is DROPPED -- keys() returns Option so that is unrepresentable rather than validated. Not derivable, and said so rather than faked: the import wizard (one Any binding into HandleWizardKey, which decides per step inside the command), gg/yy/ZZ and the ex-commands (HelpLine::Literal), and the captions, which no test can judge.').
|
||||
invariant(fit_is_denormalized_before_it_is_written,'gp_tree fits a standardized target, so a champion written down untouched predicts standard deviations. fit_csv prints the affine map beside the tree; a document cannot, so improvise_gp::fit_measure calls gp_tree::fit::denormalize first. Leaves only -- standardize rescales the target, so a split threshold is already in its feature units').
|
||||
invariant(a_fit_is_a_new_measure,'a fitted rule is stored as <target> (fit), never as the target: _Measure items are explicit items union formula targets (gotcha 7), so a formula on the target would make the two one item and the cells the fit was learned from would stop being visible').
|
||||
invariant(measure_axis_never_filters_a_fit,'_Measure is in View::filtering_cats() (Page union Filter) and Workbook::new puts it on Page, but it is the coordinate a feature matrix spreads its COLUMNS across -- so improvise_gp::dataset::filter_coords skips it. Pinning it is not \"which records am I looking at\", it is \"throw away every column\"').
|
||||
invariant(a_fit_is_bounded_in_width_not_only_in_search,'FitConfig bounds the GA; nothing bounded the encoded feature count, which preprocess can drop to zero (a panic: random_tree draws below(0)=0 and sample_thresh indexes ranges[0]) or multiply without limit (one column per distinct categorical value, and permutation importance is repeats*rows*features). improvise_gp::fit_measure refuses both, and MAX_FEATURES names Label as the fix').
|
||||
invariant(dataset_columns_are_not_header_aligned,'gp_tree::data::preprocess continues past an entirely-missing column WITHOUT pushing a ColumnSpec, so Dataset::columns is not index-aligned with the header it was built from. Ask a column role by NAME, or off the header feature_table built -- never by position into columns').
|
||||
invariant(no_input_line_is_dropped,'The Markdown renderer covers exactly the subset the corpus uses; anything it does not recognise falls through to paragraph text. The failure mode is an ugly paragraph, never a missing one.').
|
||||
invariant(a_table_cell_boundary_is_an_unescaped_pipe_outside_a_code_span,'splitRow walks the row rather than split(\"|\"), because this corpus documents a file format whose own quoting syntax is |Time Period| and a cell containing one would be shredded into extra columns -- a row that still parses, so no column count could detect it. Latent when fixed, which is the point: the trigger is one ordinary sentence away in a document whose subject is pipe quoting.').
|
||||
invariant(code_spans_stand_aside_for_emphasis,'Inline rendering replaces code spans with placeholders before matching emphasis, because this corpus routinely opens ** on one side of a code span and closes it on the other; matching per segment silently drops that bold with no visible symptom. Strong is matched before emphasis with a body that may contain a single * pair, because [^*]+ cannot match **a *b* c** and the leftover * then pairs across the bold -- which turned one sentence of gotcha 33 into three italic paragraphs.').
|
||||
doctrine(bug_first,'write a failing test demonstrating the bug before fixing it').
|
||||
doctrine(unit_over_e2e,'prefer small unit tests over integration/e2e').
|
||||
doctrine(suite_speed,'Two numbers, because the ambiguity between them is what let this drift: NO SINGLE TEST BINARY may exceed 2 seconds, and the WORKSPACE WALL CLOCK is the sum of them. Measured at 22d43b2: 1309 tests over 33 binaries, ~2.2s wall, longest single binary 1.42s -- so the workspace is already past 2s and the rule that bites is the per-binary one. Re-MEASURE rather than re-assert: cargo test --workspace prints a "finished in" per binary. Excludes compilation, and excludes harness/ (Playwright, minutes, its own CI job).').
|
||||
doctrine(suite_speed,'Two numbers, because the ambiguity between them is what let this drift: NO SINGLE TEST BINARY may exceed 2 seconds, and the WORKSPACE WALL CLOCK is the sum of them. Measured at 22d43b2: 1309 tests over 33 binaries, ~2.2s wall, longest single binary 1.42s -- so the workspace is already past 2s and the rule that bites is the per-binary one. Re-MEASURE rather than re-assert: cargo test --workspace prints a \"finished in\" per binary. Excludes compilation, and excludes harness/ (Playwright, minutes, its own CI job).').
|
||||
doctrine(coverage,'aim ~80% line/branch on logic; skip ratatui render paths').
|
||||
doctrine(test_contract,'test the observable contract, not the internal storage').
|
||||
doctrine(proptest_cases,'keep proptest at default 256 cases; constrain strategies instead of brute force').
|
||||
@@ -140,7 +153,7 @@ doctrine(a_documented_door_with_no_caller_is_the_dangerous_kind,'reduce_full was
|
||||
doctrine(the_audience_follows_the_optimistic_fold,'a relay is addressed by whether the asker folded the command optimistically and by whose UI the change reaches. A Command::Model is never folded optimistically, so every relay out of apply_model is ToAll. A view switch IS folded optimistically and reaches nobody else UI, so an accepted one relays NOTHING and a refused one is To(asker) alone -- improvise-6c6q, where ToOthers told every client but the one that was wrong. Two traps, both proven by injection on PR #48. A test written through a what-do-the-others-hear helper looks away from the asker by construction, which is how the sibling bug in apply_model shipped green. And a test whose bystander is already on the value being relayed cannot see a wrongly WIDENED audience either, because the wrong relay folds to a no-op -- so assert the Broadcast ITSELF, since the audience is the message, and seat the bystander somewhere it can be yanked off.').
|
||||
doctrine(half_a_lockstep_fix_is_worse_than_none,'resetting the server mirrors without telling the clients makes the DIFF believe they are already current, so it sends nothing: stale FOREVER rather than until the next scroll. When a fix has a server half and a client half, shipping one is not partial progress, it is a worse bug than the original.').
|
||||
doctrine(force_is_a_recomposition_not_a_bypass,'force never applies a batch whose basis is stale; it re-composes the same INTENT against the current state and submits a fresh batch with a current basis. A mechanism whose only purpose is to violate the invariant is not a feature, it is the invariant deleted with extra steps.').
|
||||
doctrine(a_simulated_fix_must_be_the_fix_you_named,'The sibling of an_injected_fault_must_be_the_fault_you_named, one level up, and it fails in the opposite direction: an injected FAULT that is not the one named proves a check that does not fire; a simulated FIX that is not the one named proves a BUG THAT DOES NOT EXIST. Worked example: the harness agent measured the 8s watchdog overwrite honestly, on a tree predating PR #21, then reported it "survives improvise-taxx, confirmed by simulating the opened flag". PR #21 is TWO changes -- the flag AND routing both close arms through setShellFailure, which clears the timer. Simulating half of it reproduced the bug and read exactly like a confirmation. improvise-xps8 was filed on that report, propagated into repo-map gotcha 48 and a spec comment, and was closed INVALID. The repair is not a deletion: the property is now executed by a test that outlasts HANDSHAKE_TIMEOUT_MS, proved discriminating by removing setShellFailures clearTimeout and watching "sent no document" appear.').
|
||||
doctrine(a_simulated_fix_must_be_the_fix_you_named,'The sibling of an_injected_fault_must_be_the_fault_you_named, one level up, and it fails in the opposite direction: an injected FAULT that is not the one named proves a check that does not fire; a simulated FIX that is not the one named proves a BUG THAT DOES NOT EXIST. Worked example: the harness agent measured the 8s watchdog overwrite honestly, on a tree predating PR #21, then reported it \"survives improvise-taxx, confirmed by simulating the opened flag\". PR #21 is TWO changes -- the flag AND routing both close arms through setShellFailure, which clears the timer. Simulating half of it reproduced the bug and read exactly like a confirmation. improvise-xps8 was filed on that report, propagated into repo-map gotcha 48 and a spec comment, and was closed INVALID. The repair is not a deletion: the property is now executed by a test that outlasts HANDSHAKE_TIMEOUT_MS, proved discriminating by removing setShellFailures clearTimeout and watching \"sent no document\" appear.').
|
||||
doctrine(an_order_test_needs_two_histories,'a single-process test cannot tell \"sorted correctly\" from \"hashed that way today, on this machine\". Compare against the insertion-ordered structure itself, or build the same logical data by two DIFFERENT insertion histories and require each to list its own. Corollary of check_the_checker.').
|
||||
doctrine(a_helper_that_builds_the_world_the_code_wants_tests_nothing,'improvise_eval FABRICATED a _Measure item per feature name, so it proved the emitter and gp_tree::eval agree against a model that could not exist. The fix is not to delete such a test -- it checks real things -- but to state its scope and assert the PRECONDITION separately against a real fixture. Pair a refusal test with an accepting one, or the refusal can pass by refusing everything.').
|
||||
doctrine(a_session_local_widget_gets_a_cache_and_no_projection,'a Projection is addressed to a widget on EVERY client, which is right exactly when the change is addressed to NO ONE session. A half-open dialogue belongs to precisely the session that opened it, so WizardCache is built and drawn locally with no Projection. Same argument that removed AcceptWizardModel from ModelCommand, applied to rendering rather than to mutation.').
|
||||
@@ -153,12 +166,51 @@ doctrine(a_watchdog_names_the_condition_not_the_peer,'a worker that catches its
|
||||
doctrine(a_demo_is_checked_against_the_type_not_the_axes,'ask grid_of what a cell IS -- a CellId variant, per ADR 002 -- never re-derive the rule from the view config: a second derivation can agree with itself while disagreeing with the client. The assertion is written BEFORE the model, and proved discriminating against the model that fails it.').
|
||||
doctrine(being_the_second_consumer_audits_the_abstraction,'improvise-9x6 framed the worker as a wasm-flavoured analogue of the socket server, and the transport work was nearly free. The four real obstacles were Host in the wrong crate, Save unreachable from the wire, dirty never set by a wire edit, and an async seam unreachable through a Box -- and THREE OF THE FOUR were gaps in the SHARED layer that the socket server also had and had never noticed. A second real consumer is what audits an abstraction; that is a stronger argument for having something to test than any specific check.').
|
||||
doctrine(a_load_bearing_order_needs_an_order_test,'an ordering documented as load-bearing and held up only by the order of two statements is a check nobody has seen fail. Projection::Basis is emitted after the grid updates whose counters it states; that was asserted in three doc comments and tested nowhere until a test was added and proved by injecting the swap.').
|
||||
doctrine(a_fact_stated_twice_is_changed_by_grep,'When a fact is stated in more than one place, changing it is a grep and not an edit -- and the obligation attaches to the FACT, not to the edit: writing a NEW statement of an existing fact obliges you to find the places that already state it. A new paragraph restating an existing fact is a second derivation, in prose, where the compiler cannot see it. Evidence at 22d43b2: gotcha 44 was rewritten in design-principles.md and in src/ui/grid.rs and left stale in context/repo-map.md; "there is no browser here" survives in seven places after harness/ made it false, including in the spec file that falsifies it.').
|
||||
doctrine(a_fact_stated_twice_is_changed_by_grep,'When a fact is stated in more than one place, changing it is a grep and not an edit -- and the obligation attaches to the FACT, not to the edit: writing a NEW statement of an existing fact obliges you to find the places that already state it. A new paragraph restating an existing fact is a second derivation, in prose, where the compiler cannot see it. Evidence at 22d43b2: gotcha 44 was rewritten in design-principles.md and in src/ui/grid.rs and left stale in context/repo-map.md; \"there is no browser here\" survives in seven places after harness/ made it false, including in the spec file that falsifies it. And this is how it RECURS: the same commit that corrects one copy is the one most likely to miss the others, because its author believes the fact is now fixed. Demonstrated twice in one day -- improvise-s17o left three statements standing including one 200 lines below the paragraph it corrected, and PR #45 created a false SIBLING note while fixing the fact it described.').
|
||||
doctrine(a_reviewed_paragraph_is_not_evidence,'A recipe in a doc is code and must be run in the head. The AGENTS.md dead-ID audit scanned only context/repo-map.md and context/adr/*.md, so it found a dead id in the map and missed both copies in harness/tests/shell.spec.js; its [a-z0-9]{3,4} class could not see a commit SHA wearing the improvise- prefix; and its exclusion list named only core and wasm. A verified SPELLING is not a verified BEHAVIOUR: playwright.config.js lists three projects, and exactly one spec runs on more than one engine while a third engine has no OPFS at all.').
|
||||
doctrine(a_fast_per_position_accessor_invites_the_loop_it_replaces,'AxisFrame::shows_level materializes every item on every call and is deliberately NOT made cheaper, because a cheap per-position form is an invitation to the quadratic draw loop header_labels exists to replace. The per-position accessors survive for one-off lookups and say so in their own doc.').
|
||||
doctrine(a_derivation_correct_per_element_can_be_wrong_for_a_set,'improvise-kmgs derived ModelCommand from the model effects ONE AT A TIME, which was right per effect and put SetCell on the wire without its MarkDirty companion -- so "save if dirty" was a trigger that never fired and a wire edit was lost silently (improvise-gv9r). The repair was not a second table but raising the fact where it cannot be omitted: App::apply_effects reads dirtiness off the same EffectKind the Dirty and layout_version are read off.').
|
||||
doctrine(a_derivation_correct_per_element_can_be_wrong_for_a_set,'improvise-kmgs derived ModelCommand from the model effects ONE AT A TIME, which was right per effect and put SetCell on the wire without its MarkDirty companion -- so \"save if dirty\" was a trigger that never fired and a wire edit was lost silently (improvise-gv9r). The repair was not a second table but raising the fact where it cannot be omitted: App::apply_effects reads dirtiness off the same EffectKind the Dirty and layout_version are read off.').
|
||||
doctrine(reachability_is_a_reason_not_to_rush_not_a_reason_not_to_notice,'CheckedBatch documents clause 4s hole -- a chain whose later command fails at apply time leaves its predecessors applied, and silently -- rather than closing it, because no client in the workspace can compose it. The lost update the same commit FIXED had exactly that reachability argument and none of the documentation, which is how it shipped.').
|
||||
doctrine(a_ticket_id_and_a_commit_sha_must_not_share_a_prefix,'crates/improvise-core/src/workbook.rs cited a git commit sha wearing the improvise- ticket prefix. A reader runs bd show, gets nothing, and concludes a follow-up was lost. A dead ID is worse than no ID; an ID-shaped non-ID is worse again, because the audit loop reports it identically.').
|
||||
doctrine(redundant_earns_a_deletion_unused_earns_a_note,'improvise-n89z had three wire variants with no producer and gave them two answers. GridUpdate::Invalidated was DELETED: its meaning had migrated into running code (invalidate_relabelled blanks exactly what a reframing invalidates), and there is no state in which a server knows contents are stale and not WHICH -- grid_updates diffs and seed compacts -- so it was REDUNDANT. CategoryTreeUpdate::Items/Axes STAY with the argument written down: they are derived from the effects by the recipe that built ModelCommand, so they are merely UNUSED, and emitting them needs the tree observed and diffed for a widget costing a few hundred bytes. Three documents describing a variant as load-bearing while nothing produces it is worse than silence.').
|
||||
doctrine(a_client_side_test_cannot_say_what_a_producer_emits,'A test that hand-builds the projection it then folds is testing the fold, and says nothing about the producer -- it fabricates the very data the code under test is supposed to make. Found 2026-09-20 on improvise-p1ym self-review: a_filter_axis_is_projected_but_not_cycled builds its own PageAxis with items: None, so it would have passed with grid_of emitting items for a Filter axis, which is the gotcha-27 trap the ticket exists to close. Every client-side fold test over a new projection needs a producer-side sibling asserting what is actually emitted.').
|
||||
doctrine(a_lockstep_test_must_drive_two_documents,'Rebuilding the clients cache from the host after each step and then comparing the client against THAT HOST is two readings of one value, and it is vacuous. Proven 2026-09-20: a_locally_resolved_page_turn_lands_where_the_host_would_put_it stayed green with the odometers significance reversed. The fix drives two independent documents -- a Host fed the commands the client composed, an App fed the same keypress through reduce_key -- and compares View::page_selection, the DOCUMENT, never a projection. Pair it with an assert_ne! against a fresh fixture, or a resolver that emits nothing passes by moving neither.').
|
||||
doctrine(a_derived_string_deletes_the_test_that_policed_it,'every_hint_key_resolves_in_its_mode and its 60-line hint tokenizer existed to hold a hand-written AppMode::hint_text honest against the keymaps. Deriving the keys from default_keymaps() makes that direction true by construction, so the test and Keymap::binding_hints were DELETED -- a_consolidation_that_keeps_its_compatibility_test_is_unfinished, with the deletion as the measure. What replaced it checks the direction that was never checkable before: a mode advertising a command nothing binds. Note what neither version reaches, which is the honest residue: the CAPTION. \"i:edit\" where i enters records mode passes both.').
|
||||
doctrine(a_fixture_that_sits_on_one_arm_tests_one_arm,'The one-arm rule biting in a fixture rather than in a match. Proven 2026-09-20 on improvise-e24v: four faults were injected into the new client resolvers and three failed a named test immediately, but corrupting the status sentence in DeleteCategoryAtCursor CATEGORY arm did NOT, because the lockstep fixture had its panel cursor on an item row throughout and never entered that arm. The fixture now visits a header row as well. Generalise: when a Cmd branches on WHERE A CURSOR IS, the fixture must visit each place, and the injection pass is what reveals that it does not -- reading the test does not.').
|
||||
doctrine(no_evidence_is_not_a_verdict,'a metric computed over an empty sample is not the same statement as a bad metric. cv_regress pools nothing below two folds and regression_quality of an empty pool is R2 exactly 0.0, which reads as \"this rule is worth nothing\" -- the loudest thing a fit can say. Quality is an enum with Measured and Unmeasured so a caller must name the case where nobody checked; an Option would have collapsed them').
|
||||
doctrine(a_fixture_that_avoids_the_default_is_a_fixture_that_hides_a_bug,'every test in improvise-gp/dataset.rs built its View with View::new + on_category_added, under which an underscore category lands on Axis::None. A real Workbook::new puts _Measure on Page, and the whole feature matrix collapsed there. When the code under test takes a value a constructor elsewhere produces, at least one test must use THAT constructor').
|
||||
doctrine(assert_contents_not_shape,'the first version of the _Measure-on-Page regression pinned the header and the row count, and passed with the bug present: the target is _Measure first item in that fixture, so every row still had a target and only the feature columns came back blank. The shape survived while the data did not').
|
||||
doctrine(a_user_facing_string_is_code,'a rustfmt pass over the multi-line literal spelling improvise-gp refusal left runs of ten spaces mid-sentence -- invisible in the source, invisible to every test that only asked whether a substring was present, and visible to precisely the user the message exists for. Assert the shape of a message read off a status bar: one line, no double spaces').
|
||||
doctrine(a_printed_value_must_be_a_parseable_one,'FitOutcome::status_line spells the seed {:#x} and tells the user that reproduces the fit; FitMeasure::parse took decimal only, so the one instruction the status line gives failed on the value it had just shown. Aim the test at the round trip THROUGH the renderer, never at a hand-written literal of the rendered form -- a literal still passes when the printing changes, which is the half that breaks').
|
||||
doctrine(a_guard_test_must_outgrow_the_older_guard,'the MAX_FEATURES test at twelve records passed with the guard deleted, because that document is refused anyway by fitted_formula noticing a one-hot. When a new check shares a code path with an older one, the fixture has to be past the point where the older one fires, or the test is measuring the wrong refusal').
|
||||
doctrine(verify_the_mechanism_a_fixture_leans_on,'an improvise-gp fixture kept a category single-valued and claimed simplify would prune the constant one-hot, making the success path deterministic. data.rs::build_categorical pushes (0.0, 1.0) as the range of EVERY one-hot whatever the column holds, so simplify cannot know it is constant -- the tests passed on seed luck and the claim was repeated in five places before anyone read build_categorical. A fixture whose determinism rests on a mechanism elsewhere in the tree must cite the line, not the belief').
|
||||
doctrine(resolving_a_name_is_not_meaning_a_column,'fitted_formula asks whether the DOCUMENT can resolve a feature name, which is right for \"can this be spelled\" and insufficient alone: a numeric category named Cost encodes to a feature named Cost, and a _Measure item of the same name makes Expr::Ref resolve to the wrong column. The role check in improvise_gp::fit asks preprocess own plan instead -- a used feature is accepted only where the Dataset column it came from is a raw Numeric MEASURE column -- so one-hots, .missing flags, date parts and category coordinates are refused by ROLE rather than by spelling. Anchored by a_category_and_a_measure_of_the_same_name_are_refused and an_empty_category_before_a_measure_does_not_break_the_role_check. No function name is given here on purpose: the symbol the original proposal cited is in no file under crates/, and a fabricated symbol is diagnosed exactly like a rename.').
|
||||
doctrine(guard_the_cost_not_the_result,'the MAX_FEATURES check ran after preprocess had already materialised the rows x rows one-hot matrix it existed to prevent, so it refused the width only once the memory and the O(rows^2) work were spent. encoded_width counts what the expansion WOULD produce, from the raw table. A budget check has to run on the cheap side of the thing it is budgeting').
|
||||
doctrine(a_budget_check_is_advisory_or_exact_and_must_say_which,'improvise_gp::fit has two width checks: encoded_width is a cheap preflight that approximates preprocess type inference so a rows x rows one-hot is refused before it is allocated, and Dataset::feature_names.len() is the exact one that actually enforces MAX_FEATURES. Stating which is which is what makes the approximation safe -- a drift in either direction is bounded by the exact check behind it').
|
||||
doctrine(a_fixture_must_assert_it_built_the_case_it_names,'a collision test added a category named Cost and never put a Cost coordinate on any record, so the column was empty, preprocess dropped it, and the duplicate name the test existed for never occurred -- it passed on an unrelated bug. The fixture now asserts the header really contains two columns named Cost BEFORE fitting. When a fixture constructs a rare shape, assert the shape, not only the outcome').
|
||||
doctrine(characterize_the_negative_when_the_positive_is_incidental,'the aggregated-cell test could not assert what the cell IS -- on that document it is Error(\"ref:Cost\") rather than a wrongly-routed number, which is a property of the shape and not a rule anything states. It asserts what the cell is NOT: the sum of the per-record predictions. That covers both outcomes and fails on the day the behaviour is deliberately changed, which is when the characterization should be replaced').
|
||||
doctrine(a_link_is_a_promise,'Generated documentation must never emit a link it cannot resolve. An improvise-xxxx id whose title is not in the .beads/issues.jsonl export renders as PLAIN TEXT, and a cited source path links to GitHub only when the file exists on disk at generation time. A dead link is worse than no link for the same reason a dead ticket id is: it reads as a promise that the target exists. The generator therefore degrades to text rather than guessing a URL scheme.').
|
||||
doctrine(the_link_check_is_the_build,'scripts/build-doc-site.mjs resolves every anchor it emits against every internal href it writes and exits non-zero on a break; web/assemble.sh runs under set -eu, so a dead cross-reference fails the assembly rather than shipping. It earned this on its first two runs, catching a fence-close regex that compared a three-character marker to a one-character string (every document swallowed from its first code block onward) and a list parser that ended the gotcha list at the first blank line (1 of 48 anchors emitted while the jump strip linked to 48). Both were invisible in a rendered page that looked fine, which is the general lesson: for generated documents, \"it renders\" is not evidence.').
|
||||
doctrine(publish_by_manifest_not_by_glob,'A document reaches the public site only by being named in the SECTIONS manifest. A glob over context/** would publish the next working file dropped there, and what a stranger should read is not a decision to make by accident. The corollary is that every omission must be argued rather than merely happening -- context/code/doc-site.md carries the argument and is itself published.').
|
||||
doctrine(add_the_check_that_could_have_seen_it,'When a review finds a defect a check missed, fix the defect AND add a check that could have seen it, then prove the new one by reverting the fix. The worked example is a nested <a> in the generated doc site: the link check, a whole-site link check and a screenshot pass all missed it because every href resolved -- the defect was outside the question that check asks. A reviewer found it by clicking a link. Reverting the fix makes the new check report 66 and fail the build. The generalisation: a green check is evidence about its own question only, so name the question before trusting it.').
|
||||
doctrine(escape_tracker_text_as_an_attribute,'esc() escaping & < > but not the quote it is interpolated into title=\"...\" with let five shipped bead titles break out of the attribute, and a browser parsed the remains as junk attributes. Tracker titles are written by whoever files a bug, so a friendly corpus today is not a reason to interpolate untrusted-shaped text unescaped. Escape the delimiter you are interpolating into, not merely the characters that are unsafe as text.').
|
||||
doctrine(a_destructive_default_warns_after_the_write,'bd update --notes REPLACES a ticket\'s notes and prints its warning only after the write has happened. On 2026-09-20 it destroyed 5352 characters on improvise-xf1 -- including a user-supplied correction that contradicted the note being added. Recovered from the previous .beads/issues.jsonl export in git, which is the only backup and is overwritten by the next auto-export. General form: when a flag\'s failure mode is \"already gone\", the warning is not a safeguard. Prefer --append-notes; read before overwrite.').
|
||||
doctrine(parallel_actors_need_distinct_names,'bd\'s --actor defaults to $BEADS_ACTOR, then git user.name, then $USER, so a wave of agents sharing a machine shares an actor and the claim guards cannot tell them apart -- two agents claim one bead and neither is told. Exit 13 means another actor won the race; that is the mechanism working, not a failure to retry around. User instruction, 2026-09-20.').
|
||||
doctrine(a_stale_gotcha_asserts_the_negation,'repo-map gotcha 17 said float equality was \"currently mixed\" and named the div-by-zero guard as the exact-comparison half, long after both had been unified on FLOAT_EQ_EPSILON. A doc that goes stale does not merely omit -- it tells the reader not to assume the thing that is now true. This is why the orchestrator doc pass VERIFIES each claim against the tree rather than applying a queued edit.').
|
||||
doctrine(an_advisory_gate_is_not_a_gate,'scripts/check-comment-runs.py was written, fixed seven times, and quoted by seven agents as their definition of done while being invoked by NO workflow. A rule enforced only by the agents who agreed to it is a convention. The gate landed last, deliberately, because a gate that goes green before the work is done is the same class of lie the checker itself carried.').
|
||||
doctrine(a_review_names_the_sha_it_reviewed,'A review of a superseded commit is worse than no review: it reads as a pass. So the reviewer names the exact SHA, the author does not rewrite history while a review is in flight and announces any push, and a re-review is of the delta. This matters more now that adversarial agents are the ONLY gate -- Copilot\'s quota is exhausted for the rest of 2026-09 and its request is a SILENT no-op (the mutation returns success and registers no reviewer), which is itself an instance of doctrine(an_advisory_gate_is_not_a_gate).').
|
||||
doctrine(review_is_requested_not_inherited,'No PR is reviewed because it exists. Requesting review is a step in the author\'s own definition of done, ranked beside \"tests pass\", not a courtesy notification afterwards. The failure mode it prevents is the quiet one: an agent finishes, opens a PR, reports success and waits on a review that was never going to come -- indistinguishable, from the outside, from a PR waiting its turn. Note the shape repeats: an ABSENT gate and a SILENTLY no-op request both read as \"in progress\", which is an_advisory_gate_is_not_a_gate seen from the requester\'s side. The user had turned Copilot AUTO-review off, which is the actual explanation for PRs #37/#41/#42 merging unreviewed; the orchestrator had attributed that to quota, and the two are independent facts.').
|
||||
doctrine(construction_is_not_loading,'A lazily-loading embedded player proves nothing by existing. asciinema-player with autoPlay:false does not FETCH its cast until the viewer clicks, so a page whose every cast path 404s constructs the full set of players and throws no page error. The agent\'s own check passed on a page broken exactly that way (./casts/ -> ./cats/); pressing play returned 0 rendered lines and four 404s. Generalises: any check on a lazily-initialised embed must trigger the load it is about.').
|
||||
doctrine(a_deployed_path_is_a_grep_across_the_tests,'Moving where something is served is not a build-script change, it is a grep across the TEST surface. The landing-page inversion (user ruling on improvise-kh8) cost one line in web/assemble.sh and six navigations in harness/tests/shell.spec.js, a CI-gating job the agent did not own. Nothing about \"serve the landing page at the root\" says \"a browser test job goes red\". In this repo the harness is the surface LEAST likely to be in the mental model, and for a structural reason: it is not a workspace member and cargo test never touches it, so every habit the repo has built is blind to it. Cross-reference: the harness serves the DEPLOYED arrangement on purpose (improvise-vvl3), which is what makes it trustworthy and also what makes a layout change a test change.').
|
||||
doctrine(a_guard_that_matched_is_not_a_guard_that_fired,'Watching a guard MATCH proves the happy path. A guard written to catch a SILENT failure is only evidence once the specific failure it names has been injected and it has complained BY NAME. web/assemble.sh\'s four stamped checks exist because one grep over four files succeeds when any one matches (found by Copilot on PR #24), so injecting a generic failure would not test them -- the fault to inject is one broken substitution, and the evidence is that the right guard names itself. Same shape as the vacuous raw-string fixture in scripts/check-comment-runs.py: the orchestrator injected a fault, the self-test stayed GREEN, and the fixture turned out to prove nothing because a plain-string fallback reached the same verdict.').
|
||||
doctrine(an_injection_tests_the_prose_as_well_as_the_check,'A guard that carries an explanatory comment has TWO things that can be wrong, and injecting its fault tests both. web/assemble.sh\'s stamped comment says each reference \"names itself when it is the one missing\". Injecting one broken substitution confirmed exactly that. Injecting TWO showed stamped exits on the first, so only one is ever named -- a sentence that is true as written and misleading as read (improvise-x4wv, filed rather than fixed: it fails safe, and the guard\'s shape came out of a Copilot finding on PR #24). The agent\'s own framing is the part to keep: both times my instinct was to verify the happy path and call it evidence -- the players constructed, the guards matched; both times the injection was what turned it into a check. Across wave 2 the happy path was checked and called evidence THREE times by three different actors and all three were caught by injection and by nothing else, which is why force_the_failure has to be a standing rule rather than a reminder.').
|
||||
doctrine(a_reasoned_claim_feels_like_a_measured_one,'Self-reported unprompted by the WU-N agent on PR #49, and kept because the failure is structural rather than careless: at the moment of writing, a claim you reasoned and a claim you ran feel the same from the inside. Three in one PR -- an ungated AddItem reset caught by reading its own diff rather than by a test (1312 green throughout); a benchmark share quoted from a single run that a second run contradicted (64% vs 45%); and a verification command handed to the reviewer as printing nothing, which printed 94 lines, discovered only because the agent ran it AFTER sending. force_the_failure covers the test side; this is the reporting side. Run the command before quoting its output; quote a measurement as a range over runs and say which digits do not reproduce; and label an argued claim as argued so a reviewer can weight it.').
|
||||
doctrine(a_specialised_fast_path_can_be_the_only_path,'eval_agg had a fast path for Expr::Ref and a fallback that silently ignored its argument: for ANY inner expression that is not a bare Expr::Ref it narrowed nothing, summed the raw cells matching the partial key and DISCARDED THE EXPRESSION, so SUM(Revenue - Cost) summed Revenue AND Cost together. Every formula anyone had written used the fast path, so the fallback was never exercised and never wrong-looking, and no test anywhere covered it -- improvise-4zl7\'s own framing described a more careful bug than the one present. When a function special-cases the common shape, ask what the general branch actually DOES with the input it was given, not whether it returns something plausible.').
|
||||
doctrine(a_negative_assertion_must_show_its_event_reachable,'A test denying an event must demonstrate that event is reachable on that exact code path, not a sibling path and not the rule\'s unit test. Prefer a guard that makes the vacuous case fail by name. Ask whether the event was EVER possible on this path, not whether it is pending now: a correct implementation may already have cancelled it, which is usually the property under test.').
|
||||
doctrine(a_premise_guard_is_validated_in_both_directions,'A guard that closes a vacuity must be shown to fail on the vacuous path AND not to fail on the passing path. Only the first is obvious and only the first is what a reviewer asks for. Worked example: a watchdog premise-guard spelled armed() (armed now) passes the vacuity test and breaks the correct case, because a correct page has already disarmed by the time the check runs; timesArmed() > 0 (ever armed) is right. The distinction appears only when you build the guard and watch a green test go red.').
|
||||
doctrine(measure_the_property_that_actually_moved,'A measurement that cannot distinguish the two states is not a measurement, and a measurement of the wrong property is not one either, however precise. This repo\'s signature defect is a test that passes while the thing it names is broken, and it arrives through the INSTRUMENT at least as often as through the assertion. Instances. getBoundingClientRect().height reports a height for content-visibility-hidden children, so a nav-collapse probe read 26 visible links in BOTH the collapsed and the expanded state and would have passed the build whose DESKTOP sidebar was collapsed and unusable; checkVisibility({contentVisibilityAuto:true,visibilityProperty:true}) discriminates. Heading text overflows its box without widening it, so getBoundingClientRect reported every element inside the viewport while seven doc-site pages overflowed at phone width -- only per-element scrollWidth names the culprit. A width measured on a display:block label could not see an inline glyph and accused working code. A wall clock absorbed a contaminated tree without a trace where an arithmetic identity would not have. A CI watcher concluded on \"0 checks pending\", which reads identically for \"all finished\" and \"none registered yet\", and so reported a fresh push settled instantly -- a terminal state needs a non-zero total AND the expected head. And the sharpest case is the one where the instrument is sharp and still aimed wrong: a checkbox driving the doc-site\'s collapsed phone nav carried hidden (= display:none), which removes it from the tab order AND the accessibility tree, so at <=900px the navigation was mouse-only, and the probe that \"verified\" that fix counted VISIBLE nav links -- complete for a mouse, silent on whether any non-mouse route exists. Ask what the defect would DO to a user, then pick the instrument that observes that: here tab order, .focus() and the accessibility snapshot, measured on BOTH sides of the media query the regression lives in. The check is the same one applied to a test -- injure the thing being measured and confirm the number moves.').
|
||||
doctrine(an_aged_fact_is_cured_only_by_re_asking,'An instrument failure and an aged fact have no cure in common. Picking a better instrument fixes the first permanently; a fact verified once and believed later can only be re-asked, every time. Worked examples from 2026-09-20: a mergeable status verified in review round 1 and still believed in round 6, through two rounds of CI silence, while the branch sat conflicting; and a knowledge-base clause count taken from a working tree six merges behind, which would have been reported as evidence for a write that deleted three clauses.').
|
||||
doctrine(an_out_of_band_write_desynchronises_the_beads_export,'AGENTS.md says never hand-edit or git checkout .beads/issues.jsonl and attributes the damage to the post-checkout hook. That attribution is INCOMPLETE: resolving a merge conflict on that file with git restore, which fires no hook at all, desynchronises export-state.json identically. The mechanism is ANY out-of-band write, not the hook. The failure is silent in the worst direction -- bd list succeeds and re-exports nothing, git status reads clean, and issues exist in Dolt and nowhere else; three tickets were in that state until an explicit bd export -o reconciled it. The recovery is bd export -o, never bd list. Standing rule: any git operation touching that file is followed by bd export -o.').
|
||||
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.').
|
||||
@@ -179,6 +231,9 @@ work_done('improvise-i34',feature,'Storage gained list/delete, implemented for P
|
||||
work_done('improvise-cqi',feature,'crates/improvise-protocol created. ViewState, ViewCursor, ViewFrame, AppMode, Panel, MinibufferConfig, MinibufColor, HELP_PAGE_COUNT and the key types moved there; improvise-command re-exports them so no caller path changed. Command + reduce_view + RenderCache/CellDisplay/Snapshot added with 15 tests. Added to the CI wasm job.').
|
||||
work_done('improvise-sef',maintenance,'kb.pl audited and repaired (a406b52): may_depend_on/2 and before_fixing/2 were unsound, and sixteen lines of captured MCP runtime predicates were removed. kb_report/0 and kb_consistent/0 added (3a58d3c) over four error classes -- domain integrity, layering, rule soundness, functional keys -- and wired into CI; every class verified by fault injection. Backlog-grooming method recorded (3697171).').
|
||||
work_done('backlog-groom-2026-09-19',groom,'ran the five 2026-08-30 checks over 62 open/deferred issues. Cycles: none. Priority inversion: one (g0f P3 blocking cqq P2), raised. Unrecorded deps: 11 prose mentions, 1 real (lae blocks git, by the ticket own words), the rest analogues or siblings. Stale citations: 2 broken paths (nr5 doubled crate prefix, three gp-tree tickets pointing at examples/fit_csv.rs instead of crates/gp-tree/examples/), 9 line-number drifts corrected to symbol references. Dissolved premises: ede and toc closed, ete demoted to P4, 2 of 5 items struck from 7yx.').
|
||||
work_done('improvise-0yq8',refactor,'ClientState -> improvise-protocol/src/client.rs and keys.rs::{resolve,step,KeyOutcome} -> improvise-protocol/src/resolve.rs, with socket.rs::{typed,typed_cell_command} following step. BrowserKey stays in the wasm client: it is the browser half of the two things a front end owns. render_model became a free function, ClientState being foreign. 1311 tests before and after -- no behaviour change.').
|
||||
work_done('improvise-e24v',feature,'Fifteen command names resolve client-side: cycle-panel-focus, the category panel five, the view panel three, both tile-axis names, both help-page steps, commit-buffer in three kinds, clear-cell and paste. New lockstep harness applies what the client composed to a SECOND authoritative host and compares documents as .improv text (Debug prints the symbol table HashMaps, so it differs per process). Four injected faults, each failing a named test; one needed the fixture widened first. Found and fixed a wire/effect divergence in Command::HelpPage.').
|
||||
work_done('improvise-x7sw',refactor,'Closed with improvise-ktv1 and improvise-k099 as one question. improvise-protocol/src/help.rs is the one home for a UI string; AppMode::label/hint_text and Keymap::binding_hints deleted, along with every_hint_key_resolves_in_its_mode and its 60-line tokenizer. The TUI help overlay kept its ratatui styles and lost ~310 lines of content; the browser gained one (RenderModel::help, empty means closed), asserted by a new Chromium test in harness/tests/shell.spec.js proved discriminating by two injected faults.').
|
||||
work_followup('improvise-8tk','improvise-lae','no-file-path UX: prompt for a path rather than only erroring. ExportPrompt is CSV-only, so a new SavePrompt TextEntry mode committing to SaveAs is the likely shape.').
|
||||
work_followup('improvise-6sq','improvise-ct5','i on an empty records grid reports \'cannot drill\', leaking a pivot-mode concept into records mode').
|
||||
work_followup('improvise-h0b','improvise-a2x','ViewState field types (AppMode, MinibufferConfig, ViewFrame, KeyCode, KeyModifiers) still derive no Serialize, and improvise-command depends only on serde_json. Deferred: nothing serializes ViewState until improvise-cqi builds a Snapshot, so the derives would have no consumer.').
|
||||
@@ -219,18 +274,34 @@ decision('improvise-ondj',basis_check_is_a_type_not_a_visibility,'apply_command/
|
||||
decision('improvise-fkfg',structural_and_data_staleness_are_different_answers,'RecordGone: nothing resolves, nothing to write to, nothing coherent to force -- force never creates structure. RecordMoved: resolves at another version, force is one keystroke. This is why ADR 002 put a MAINTAINED RecordHash index in DataStore rather than regenerating from the layout, which would have conflated scrolled-out-of-view with no-longer-exists.').
|
||||
decision('improvise-gvnb',geometry_invalidates_every_cursor_but_moves_none,'App::cursor_epoch is bumped by reset_cursor and watched by Sessions::adopt_cursor_epoch. The trigger is the COUNTER MOVING, not the command variant -- which effects renumber the grid is a per-effect decision, so a variant table would be a second description free to disagree. What is broadcast is an INVALIDATION, never a position.').
|
||||
decision('improvise-9x6',host_moved_not_mirrored,'Host contains no socket but lived in improvise-ws-server, which is tokio and native-only. The worker needs the same session loop and cannot depend on it. Moved to improvise_command::ui::host beside Sessions; ws-server re-exports. Re-implementing would have been the mirror improvise-xks6 had just deleted for the frames: a duplicated decision layer drifts invisibly. registry stayed on the server because a worker hosts one document.').
|
||||
decision('improvise-9x6',worker_persists_on_its_own_behalf,'effect::Save is ViewOnly so no ModelCommand names it, and effect::SetCell does not set model_state.dirty (a separate MarkDirty effect does, only on the key path). So save-if-dirty never fires and no client on any transport can ask to save. The worker triggers on the command VARIANT instead, an over-approximation costing one redundant whole-document write.').
|
||||
decision('improvise-9x6',worker_persists_on_its_own_behalf,'effect::Save is ViewOnly so no ModelCommand names it, and effect::SetCell does not set model_state.dirty (a separate MarkDirty effect does, only on the key path). So save-if-dirty never fires and no client on any transport can ask to save. The worker triggers on the command VARIANT instead, an over-approximation costing one redundant whole-document write. SUPERSEDED by improvise-gv9r: MarkDirty is gone, App::apply_effects raises the flag for any EffectKind::Model(_), and the trigger is now the flag -- see invariant(the_persist_trigger_is_an_observation).').
|
||||
decision('improvise-9x6',document_named_out_of_band_not_in_a_frame,'the worker takes its document from its own URL query, the analogue of the socket naming it in the ws:// path. No fourth ClientFrame: a document is a resource, and the envelope pair stays shared with the socket deployment.').
|
||||
decision('improvise-9x6',seeding_is_a_write_not_a_bypass,'a fresh origin has an empty OPFS, so SEEDING is the new problem, not loading. A seed goes in by Storage::write and is read back like any other document, so a fetched demo, an upload and a saved file share one parse path and a seed never clobbers an edit. User ruling 2026-09-20: there should be a VFS mechanism for this, and there already is one.').
|
||||
decision('improvise-djm',websocket_path_kept,'the page spawns a worker by default and keeps ?ws= for improvise-ws-server, because the ws-server is the only way a TUI-hosted document is shared. Two transports cost little: they differ only in where strings come from, and the presence of the parameter is the whole selection mechanism.').
|
||||
decision('improvise-bpq2',keymap_is_client_only,'keys become commands in the browser; the server must not know the keymap. User ruling, following from the-model-is-shared-the-UI-is-not. Buys two clients different bindings over one document. Where a client cannot compose a Command for a binding, ENRICH THE PROJECTION rather than resolving the key on the host; GridExtent is the first instance.').
|
||||
decision('improvise-bck',demo_built_against_an_assertion,'web/demo.improv was written to satisfy every-projected-CellId-is-Record (ADR 002) before it was written to look good; the assertion was proved discriminating against examples/demo.improv, where it names the aggregate at (0,0).').
|
||||
decision('improvise-bck',demo_built_against_an_assertion,'web/demo.improv was written to satisfy an ADR 002 CellId assertion before it was written to look good; the assertion was proved discriminating against examples/demo.improv, where it names the aggregate at (0,0). Amended by improvise-ir7l: the original wording was every-projected-CellId-is-Record, which is now false and PASSED while false for the Profit column. The assertion is two-sided -- a formula-owned cell projects CellId::Derived -- and what the decision records is that the demo was built against the TYPE rather than against the axes, which stands.').
|
||||
decision('improvise-shyt',index_narrows_primary_orders,'the alternatives both MOVE the cost rather than removing it: an ordered index value type pays on every set, and scanning the primary map is O(whole store) per query on a path all aggregation routes through. The hash walk was not cosmetic -- the drill snapshot froze its record list in that order while pending_edits are keyed by ROW INDEX, so the same drill in a second process addressed different records.').
|
||||
decision('improvise-8xhy',refuse_at_emit_interim,'remedy 1 of 3, explicitly interim. Remedy 3 needs a scalar coordinate predicate the formula language lacks -- Filter gates a formula or an aggregate, it is not an Expr -- closes only the categorical third, and must first answer what the predicate means where context.get(cat) is None.').
|
||||
decision('improvise-764',wizard_cache_has_no_projection,'a Projection is addressed to a widget on EVERY client, which is right exactly when the change is addressed to NO ONE session. A half-open dialogue belongs to precisely the session that opened it, so WizardCache is built and drawn locally with no Projection::Wizard. Broadcasting it would re-create improvise-tom1 on the RENDERING side.').
|
||||
decision('improvise-n10',grid_widget_holds_no_model,'column totals MUST be projected rather than client-derived, because the sum runs over every row INCLUDING THE ONES A VIEWPORT SCROLLED PAST. The italic-aggregate rule reads CellId::Aggregate, which already carried the fact; the widget had been walking none_cats x kind.is_regular(), a second hand-rolled copy of project::cell_id.').
|
||||
decision('improvise-d31',pipeline_before_artifact,'the Pages delivery pipeline was built against what exists rather than waiting on improvise-9x6, so the worker swap is a change to WHAT IS BUNDLED, not to how it is built and shipped -- two lines in build.sh and one cp in the assemble step.').
|
||||
decision('improvise-cr3',render_decisions_are_made_in_rust,'the DOM half of the browser client is untested JavaScript, so every question with a SILENT wrong answer is answered in Rust under test and crosses as decided data. editable is a fact about the CellId variant -- character-for-character record_cell_at success condition -- so editable and composable-write hold by construction rather than by agreement. None is no-projected-cell, never aggregate (ADR 002).').
|
||||
decision('improvise-0yq8',lockstep_survives_a_crate_move,'The acceptance criterion invited deleting tests/lockstep.rs if the move made it trivially true by type identity. It does not: lockstep drives reduce_key (Cmd/Effect, improvise-command) against resolve (improvise-protocol). Those are two derivations of one answer in two crates, one of which a browser cannot depend on. The move changed which crate resolve is spelled in, not that it mirrors something. Kept, with the reason written down, so the next reader does not re-open it.').
|
||||
decision('improvise-e24v',three_listed_bindings_need_enrichment_after_all,'The ticket listed these as needing nothing added. Each is short exactly one fact and each wants a different answer (improvise-4bwa). toggle-prune-empty words its status off View::prune_empty, which is on the View inside the Workbook and in no cache -- a projection field would close it. delete-formula-at-cursor needs (target,target_category); FormulaCache carries the formula RAW TEXT, from which neither is recoverable without reimplementing parse_formula on the client. view-back emits ApplyAndClearDrill BEFORE ViewBack, and that effect is deliberately off the wire (improvise-3x3u) because its operand is another session uncommitted drill edits -- so it is not an enrichment at all, it is blocked on ADR 001 layer 2, and mirroring it by rebuilding the effect client-side would re-open exactly what 3x3u closed.').
|
||||
decision('improvise-k099',the_wire_changed_to_match_the_effect,'effect::HelpPageNext clamps at the last page and HelpPagePrev saturates at zero; reduce_view Command::HelpPage arm WRAPPED. Two derivations of one fact, disagreeing, invisible because the help overlay h/l were upstream keys and no client had ever sent the command -- improvise-e24v made them a client and the lockstep test failed on the first press past the end (client 0, host 4). The WIRE was changed to match the EFFECT rather than the other way round: the effect is what ships in the TUI, and changing that is a product decision nobody asked for. Whether both should wrap instead is improvise-trd3, which requires changing both together.').
|
||||
decision('improvise-vx3',fit_runs_in_the_cmd_not_the_effect,'a fit is decision logic -- a pure function of (Model, View, target, seed, FitConfig) mutating nothing -- so it runs in Cmd::execute and the effects carry its RESULT. Putting an expensive stochastic search inside Effect::apply would mix it into the one place the app mutates. That buys purity, not asynchrony: AC 4 is met on its \"bounded\" arm, measured at 6ms/12ms/105ms over 12/200/2000 records in release, and the fit stays a pure function a worker could be handed later').
|
||||
decision('improvise-vx3',quality_arithmetic_lives_in_gp_tree,'R2 against the predict-the-mean baseline and the permutation-importance ranking moved out of fit_csv printing into gp_tree::fit as regression_quality/importance_ranking, and the CLI prints from them. A second consumer recomputing the number a user decides whether to believe a rule by would be the derivation-done-twice defect. Verified by the CLI output staying byte-identical on both the classification and regression paths, with an injected baseline fault proving the diff discriminating').
|
||||
decision('improvise-vx3',the_outcome_carries_the_whole_request,'gotcha 37b says a fit takes the seed AND the config, and that pair is what gp_tree::data::Model records -- sufficient there because its other knobs are outside the loop. In fit_measure they are not: cross-validation draws from the same generator fit then draws from, so cv_folds moves the champion rather than only the evidence. FitOutcome carries the whole FitRequest; recording two of four fields documented the contract without meeting it').
|
||||
decision('improvise-4zl7',the_aggregation_question_is_not_about_categories,'improvise-4zl7 asked what a CATEGORICAL split means where its coordinate is aggregated away. Copilot on PR #25 showed the question has nothing to do with the split being categorical: evaluate_aggregated aggregates each Expr::Ref before evaluating the IF, so aggregate-then-route differs from route-then-aggregate for ANY tree while a linear formula is unaffected. The ruling needed is \"what does a fitted rule mean in an aggregated cell\", for the feature as a whole').
|
||||
decision('improvise-djoo',publish_the_context_prose_at_slash_docs,'The context/ prose is published at /docs/ on GitHub Pages, assembled by the same web/assemble.sh the pages-build job and harness/build.sh both run. Published: design-notes, design-principles, repo-map, the three ADRs, the seven context/code/ arguments, and the 501 test-evidence entries as their own last section. NOT published: context/plan.md, which is an agent prompt about a launch rather than a description of the program and cites a deleted context/SPEC.md, and context/cloud-agents.md, which is addressed to nobody who visits a website. Neither is referenced by a published document outside a code fence, so leaving them out dangles nothing -- checked, not assumed.').
|
||||
decision('improvise-djoo',test_evidence_is_published_as_its_own_section,'The brief called 501 entries about individual tests questionable for a public site. Published anyway, because every one of the seven context/code/*.md documents names its evidence file -- omitting it would break the published prose in exactly the way the site is otherwise careful not to -- and because a written record of the injected fault behind each load-bearing test is not a thing most projects can show. Mitigated by being its own section, last, described as a contributor reference rather than a tour.').
|
||||
decision('improvise-djoo',ticket_ids_resolve_inward,'Beads keeps its issues in a Dolt database on a git ref, so there is no public issue URL and linking ~190 citations anywhere external would produce ~190 404s. tickets.html is generated from .beads/issues.jsonl (already committed, already public) carrying id, status and title only -- descriptions are internal working notes. Because that export is passive and lags the database, an id it does not carry is emitted as plain text and listed as \"not exported\": the site never links to an anchor it did not itself generate.').
|
||||
decision('improvise-djoo',the_ticket_index_publishes_open_bugs,'tickets.html publishes all 180 cited ids with status and title, open bugs included -- a user ruling, and a decision rather than a default. The data is already public in .beads/issues.jsonl in a repo anyone can clone; the page only makes it legible, and filtering it while the page lede links the raw JSONL would be theatre. Second-order reason to keep it unfiltered: a citation whose row was suppressed would be a link into a page that does not answer it, which is the failure the id-resolution design exists to avoid.').
|
||||
decision('improvise-djoo',one_node_script_not_a_site_generator,'The landing page is hand-written HTML with no build step and one pinned CDN script; the prose site matches that posture with one zero-dependency node script. A static site generator would add a dependency tree, a lockfile and a toolchain in exchange for features this corpus does not use -- it has no front matter, no shortcodes and not a single markdown link. Every cross-reference in it is prose naming a path or an id, which is why the id and path resolution are the interesting parts and Markdown rendering is the boring one.').
|
||||
decision('improvise-djoo',the_nav_disclosure_is_a_checkbox_not_details,'The doc-site sidebar collapses on a phone through a hidden checkbox plus a label, not <details>. <details> collapses correctly on a phone, but the sidebar layout must force it OPEN and a closed <details> hides its content through ::details-content, which \`display\` cannot override -- measured in Chromium, where the desktop sidebar came out collapsed to one link with its toggle hidden. With a checkbox the hiding is ours, inside the media query, so overriding it is ordinary cascade. General form: when a layout must force a disclosure open, own the hiding rather than borrowing the UA\'s.').
|
||||
decision('improvise-864a',skip_a_superseded_pages_deploy,'Three merges within 20s queued three deploys on one Pages environment; only the newest kept its github-pages artifact and the other two runs went red on an empty fetch with a GREEN upload step. Chose to SKIP (compare GITHUB_SHA against the tip of main) rather than cancel: the concurrency block\'s own comment records that a deploy-pages cancelled midway can leave the deployment half-applied. Unresolved and flagged: why GitHub drops the losing runs\' artifacts -- the fix does not depend on the answer.').
|
||||
decision('improvise-9c8c',gate_the_comment_cap_last,'The CI step enforcing the two-line comment cap and the test-evidence keys was held back until every unit of the comment wave had merged, so that the gate\'s first green run was evidence rather than vacuous.').
|
||||
decision('improvise-kh8',landing_page_at_the_root,'USER RULING 2026-09-20: the landing page should be served at the root of the repository and link to the in-browser demo. Supersedes the agent\'s proposed landing_page_beside_the_app, which argued for the app at the root with the landing page at /landing/ on the grounds that the live app is the better front door; that argument is recorded as the reason the alternative was considered, and it was overruled. web/assemble.sh puts docs/index.html at the site root and moves the app bundle (index.html, app.js, style.css, worker.js, demo.improv, pkg/, worker-pkg/) under /app/, moving the two bundle dirs WITH the app so every relative path in the generated glue stays correct. The non-obvious cost: harness/tests/shell.spec.js navigates to /index.html six times, so a path move at the root silently retargets the whole shell spec at a different page. The deployed arrangement IS what the harness serves (improvise-vvl3), which is the property that makes the harness trustworthy and also the property that makes a layout change a test change.').
|
||||
wire_carries(command,both_directions,'user intent up, projection commands down').
|
||||
wire_carries(snapshot,on_connect_only,'improvise-q08: full ViewState + render cache filled from current viewport').
|
||||
wire_carries(command,both_directions,'user intent client->server, projections server->client').
|
||||
@@ -381,7 +452,7 @@ kb_issue_found(mcp_lenience,'the prolog MCP server accepted \`:- discontiguous f
|
||||
kb_issue_found(static_str_blocks_deserialize,'a borrowed &\'static str field makes Deserialize unimplementable, and rustc reports it on every ENCLOSING enum variant rather than at the offending field -- five near-identical \"lifetime may not live long enough\" errors pointing at MinibufferConfig, none naming buffer_key. Read past the reported site to the borrowed field.').
|
||||
kb_issue_found(ticket_premise_false_8uz,'improvise-8uz asserted selected/row_offset/col_offset were \"PERSISTED in .improv\". They were not: improv.pest has no cursor production and format_md emitted none. The claim shaped the ticket\'s whole option set, so it was checked with a test before any of the options was weighed.').
|
||||
kb_issue_found(ticket_premise_false_rml,'improvise-rml called category_panel.rs an \"exact copy\" of tile_bar.rs. It was not: the colors were identical but the labels differed (\"Row ↕\" vs \"Row\", \"None ∅\" vs \"·\"). Only the color table was true duplication; the four label sets are four deliberate vocabularies and were preserved as four methods.').
|
||||
kb_issue_found(duplicated_active_view,'adding ViewState::active_view created a second copy of "which view is active". The improvise-8uz characterization test caught the desync immediately because it called workbook.switch_view directly. Held in lockstep by two writers until improvise-jytm, which made the divergence deliberate: the guard test the_two_active_view_names_never_diverge was REPLACED, not deleted, by the_documents_default_view_outlives_this_clients_switches and a_fresh_session_opens_the_documents_default_view -- a deliberate split with no guard is how one silently regresses into the other.').
|
||||
kb_issue_found(duplicated_active_view,'adding ViewState::active_view created a second copy of \"which view is active\". The improvise-8uz characterization test caught the desync immediately because it called workbook.switch_view directly. Held in lockstep by two writers until improvise-jytm, which made the divergence deliberate: the guard test the_two_active_view_names_never_diverge was REPLACED, not deleted, by the_documents_default_view_outlives_this_clients_switches and a_fresh_session_opens_the_documents_default_view -- a deliberate split with no guard is how one silently regresses into the other.').
|
||||
functional(invariant/2).
|
||||
functional(location/3).
|
||||
functional(doctrine/2).
|
||||
|
||||
Reference in New Issue
Block a user