docs: reconcile the docs with the code after wave 1
An independent review checked every factual claim in the docs against the code at22d43b2, after eight PRs landed in one wave. This applies its replacement text. context/repo-map.md - gotcha 44 rewritten: the cache DOES frame the grid now (improvise-anr9, AxisFrame/FrameEntry/header_level_widths). The surviving half — the TUI builds at offsets zero, so a cache position is absolute — is kept, because four code comments cite gotcha 44 for exactly that. - gotcha 45: ClientFrame::Save exists (improvise-368h); MarkDirty is gone and dirtiness is read off EffectKind::Model(_); Rc<T>: Storage lives in improvise-io (improvise-hmws); OpfsFs::list joins dir (improvise-vhcz); OpfsFs is run by harness/ and bindings.rs is now the dark corner. - gotcha 47: Host::commands answers the frame whole (improvise-955n); the open hole is clause 4, not a per-command Checked. set_viewport "must" -> "does". - gotcha 43: the frame, not the command, is what is answered; two of three tiers hoist. - gotcha 21 widened: three effects abort, and tidy-up goes at the head. - gotcha 48: web/app.js is executed by harness/tests/shell.spec.js; improvise-m18f -> improvise-taxx (fixed); the open half is improvise-xps8. - gotcha 42: the never-rendered / improvise-hwpi sentence pair deleted. - gotcha 18: live_records and analyze_records are both decided now. - gotcha 1: the records columns are [coords…, +cat, measures…, +]. - AppMode is 17 variants, ModeKey is 17, eight text-entry modes; the keymap table moved to improvise-protocol/src/keymap.rs (six references). - the Wasm section: the mirror is gone; the wasm gate covers eight crates. - the File Inventory regenerated rather than patched (57/79 rows were stale); the duplicate ws-server/host.rs collapsed to a pointer at the 23-line shim; harness/, protocol keymap.rs/viewmodel.rs, improvise-gp fit.rs/dataset.rs, cmd/fit.rs, model/record.rs and the integration tests added. - the suite speed re-measured rather than re-asserted. README.md — the four "no browser here" sites narrowed to what is still true; 17 modes; the browser section names the Playwright harness. AGENTS.md — the dead-ID audit recipe was itself defective: two paths, a character class blind to a commit SHA, and a two-name exclusion list. Widened, made runnable under the worktree shell guard, and its own prose no longer spells any id. prolog-sessions/kb.pl — seven invariants replaced in place (they were false), five location/3 facts re-sited plus three new, six doctrines and seven invariants added. doctrine 48->54, invariant 44->51, decision 47 unchanged, location 31->34. kb_consistent passes. Comment-only Rust changes: a duplicated sentence in worker.rs, records_columns' doc omitting NewCategory, improvise-709f2df -> commit709f2df, and three more copies of "OpfsFs has never been run". cargo test --workspace: 1309 passed, 0 failed, 2 ignored — unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-12
@@ -18,7 +18,7 @@ layer(rendering,improvise_main).
|
||||
layer(protocol,improvise_protocol).
|
||||
layer(fitting,gp_tree).
|
||||
layer(fitting_bridge,improvise_gp).
|
||||
location(add_keybinding,'crates/improvise-command/src/command/keymap.rs','default_keymaps()').
|
||||
location(add_keybinding,'crates/improvise-protocol/src/keymap.rs','Keymaps::default_keymaps()').
|
||||
location(add_command,'crates/improvise-command/src/command/cmd/registry.rs','default_registry()').
|
||||
location(add_effect,'crates/improvise-command/src/ui/effect.rs','impl Effect').
|
||||
location(formula_eval,'crates/improvise-core/src/model/types.rs','eval_formula / eval_expr').
|
||||
@@ -38,13 +38,16 @@ location(wizard_render,'src/ui/import_wizard_ui.rs','ImportWizardWidget').
|
||||
location(save_effect,'crates/improvise-command/src/ui/effect.rs','Save, save_to_current_path').
|
||||
location(abort_chain,'crates/improvise-command/src/ui/effect.rs','AbortChain').
|
||||
location(apply_effects,'crates/improvise-command/src/ui/app.rs','App::apply_effects').
|
||||
location(sequence_dispatch,'crates/improvise-command/src/command/keymap.rs','Keymap::dispatch Binding::Sequence').
|
||||
location(text_entry_builder,'crates/improvise-command/src/command/keymap.rs','TextEntry, buffer_of').
|
||||
location(panel_keymap_builder,'crates/improvise-command/src/command/keymap.rs','PanelKeymap, bind_panel_toggles').
|
||||
location(sequence_dispatch,'crates/improvise-command/src/command/keymap.rs','Keymap::dispatch, effects_of, Binding::Sequence flattening').
|
||||
location(text_entry_builder,'crates/improvise-protocol/src/keymap.rs','TextEntry, TextEntry::with_parent').
|
||||
location(panel_keymap_builder,'crates/improvise-protocol/src/keymap.rs','PanelKeymap').
|
||||
location(records_layout,'crates/improvise-core/src/view/layout.rs','RecordRow, RecordCol, build_records_mode, records_display').
|
||||
location(storage_trait,'crates/improvise-io/src/storage.rs','Storage, PhysicalFs, MemoryFs').
|
||||
location(effect_kind,'crates/improvise-command/src/ui/effect.rs','EffectKind, ModelChange, Effect::kind').
|
||||
location(prefix_resolution,'crates/improvise-command/src/command/keymap.rs','KeymapSet::prefix_keymap, SetTransientPrefix').
|
||||
location(prefix_resolution,'crates/improvise-protocol/src/keymap.rs','Keymaps::prefix_keymap; SetTransientPrefix in improvise-command').
|
||||
location(grid_frame,'crates/improvise-protocol/src/render.rs','AxisFrame, FrameEntry, header_level_widths').
|
||||
location(browser_harness,'harness/','playwright.config.js; tests/shell.spec.js (chromium); tests/opfs.spec.js (chromium+firefox; webkit has no OPFS)').
|
||||
location(storage_contract,'crates/improvise-io/src/storage.rs','contract::storage_contract -- public, not cfg(test)').
|
||||
location(wasm_ci,'.github/workflows/ci.yml','wasm job').
|
||||
location(view_state_membership,'crates/improvise-command/src/ui/app.rs','ViewState doc comment states both membership tests; App carries the session-local residue tag').
|
||||
location(axis_presentation,'crates/improvise-core/src/view/axis.rs','plain_label/glyph/short_label/initial/color/next on Axis; AxisColor semantic palette').
|
||||
@@ -58,9 +61,9 @@ invariant(stale_layout,'app.layout rebuilds only after a whole effect batch; pre
|
||||
invariant(wizard_back_is_history_stack,'advance pushes the step it leaves onto ImportWizard::history; previous pops it, so back retraces the path actually taken through the conditional transitions rather than re-deriving a reverse table').
|
||||
invariant(wizard_validate_where_fixable,'preconditions live in validation_error() and are checked on the step where the user can still act -- never deferred to build_model after the final confirm').
|
||||
invariant(wizard_done_is_confirm,'NameModel is terminal for advance; Enter there calls build(), which parks the workbook in ImportWizard::built on Done. AcceptWizardModel takes it. previous() from Done discards built').
|
||||
invariant(failed_save_aborts_batch,'Save sets app.abort_effects on failure (no file path, or write error), so effects chained after it -- the Quit in wq/ZZ -- are skipped and the error stays on screen').
|
||||
invariant(an_abort_stops_the_chain_not_the_bookkeeping,'Three effects set abort_effects: Save and SaveAs on a failed write or missing path (improvise-8tk, both arms), and AddCategory on refusal (improvise-2zq). An abort at the TAIL of a batch also ate mode restoration and buffer clearing, stranding the user where draw_bottom_bar renders the minibuffer instead of the status line, so the error was never drawn. Tidy-up therefore goes at the HEAD, where an abort cannot reach it -- ordering rather than a runs-anyway flag. An abort is not a rollback.').
|
||||
invariant(sequence_one_batch,'Binding::Sequence flattens all step effects into one Vec applied by a single apply_effects call, so abort_effects carries across sequence steps').
|
||||
invariant(minibuffer_spine,'Seven text-entry modes built by TextEntry builder in keymap.rs; SearchMode hand-written').
|
||||
invariant(minibuffer_spine,'Eight text-entry modes carry a MinibufferConfig. Seven are built by TextEntry::new in improvise-protocol/src/keymap.rs; RecordsEditing is TextEntry::with_parent off Editing, which is why a count taken off the ::new call sites reads seven. SearchMode is hand-written -- no commit key.').
|
||||
invariant(records_mode_all_none,'ToggleRecordsMode puts every regular category on Axis::None, so EditOrDrill regular_none is always true in records mode and the edit branch depends entirely on is_synthetic').
|
||||
invariant(buffer_name_single_source,'the buffer a text-entry mode types into is named once, in that mode AppMode constructor MinibufferConfig; TextEntry reads it off the mode so the prompt and the keystrokes cannot address different buffers').
|
||||
invariant(panel_keymap_spine,'the three panel modes share a spine built by PanelKeymap::new(panel): Esc/Tab to Normal, arrows and j/k to move-panel-cursor for that panel, every panel toggle key, and : for command mode. Content keys come through chained .bind/.bind_args/.delete because they vary in shape not just command name').
|
||||
@@ -70,10 +73,10 @@ invariant(records_dim_is_column_index,'the synthetic records CellKey stores the
|
||||
invariant(storage_is_two_ops,'persistence only ever reads or writes one whole document, so the Storage trait is read/write bytes and nothing else; a backend that is not a filesystem implements it without directories, metadata or seeking').
|
||||
invariant(gzip_is_a_name_property,'the .gz suffix selects gzip in save_to/load_from, so it is a property of the document name and not of the storage backend').
|
||||
invariant(effect_kind_no_default,'Effect::kind has no default impl, so a new effect cannot forget to classify itself; the failure mode would be silent (a mis-declared ViewOnly edit never reaches connected clients)').
|
||||
invariant(view_lives_in_model_state,'the View is stored inside model_state.workbook, so touching model_state does NOT imply a model change: axis/page/format are layout_geometry, cursor and scroll are per-client view_only, Save/SaveAs/MarkDirty are bookkeeping').
|
||||
invariant(view_lives_in_model_state,'The View is stored inside model_state.workbook, so touching model_state does NOT imply a model change: axis/page/format are layout_geometry, cursor and scroll are per-client view_only, Save/SaveAs are bookkeeping and ViewOnly. MarkDirty no longer exists (improvise-gv9r): App::apply_effects raises model_state.dirty for any EffectKind::Model(_), so a document change that omits it is unrepresentable rather than validated.').
|
||||
invariant(view_state_is_a_projection,'the server does NOT serialize ViewState to maintain a client mirror -- it applies the same commands through reduce_view (improvise-cqq). Steady-state traffic is Commands only; ViewState is serialized solely for the bootstrap Snapshot').
|
||||
invariant(bootstrap_excludes_host_state,'because the only serialization point is the on-connect Snapshot, host-only state must not live in ViewState: a fresh client should not inherit a half-open import wizard, an armed prefix key, or another session staged drill edits').
|
||||
invariant(prefix_is_an_address,'ViewState::transient_prefix stores the (KeyCode,KeyModifiers) that opened a prefix; KeymapSet::prefix_keymap resolves it, walking parents. Assumes prefixes are FLAT -- guarded by prefix_keymaps_are_flat').
|
||||
invariant(prefix_is_an_address,'ViewState::transient_prefix stores the (KeyCode,KeyModifiers) that opened a prefix. The table and Keymaps::prefix_keymap are in improvise-protocol/src/keymap.rs; improvise-command wraps them as KeymapSet::prefix_keymap and owns dispatch. Resolution assumes prefixes are FLAT -- guarded by prefix_keymaps_are_flat.').
|
||||
invariant(compile_check_is_not_runtime_proof,'the wasm CI gate is cargo check, so it catches nothing that merely panics at runtime: Instant and std::fs both COMPILE for wasm32-unknown-unknown').
|
||||
invariant(effects_never_touch_fs,'every document read or write in the command layer goes through App::storage; production code in improvise-command contains no std::fs').
|
||||
invariant(io_keeps_path_wrappers,'improvise-io keeps path-taking wrappers (save, load, export_csv, parse_csv_with_warnings) delegating to _to/_from/_bytes variants, so the native CLI is unchanged while the byte-level path is backend-agnostic').
|
||||
@@ -87,12 +90,19 @@ invariant(basis_has_no_default_constructor,'a zero-valued Basis is not \"no clai
|
||||
invariant(basis_is_coordination_not_authentication,'Basis derives Deserialize and has public fields, so any claim can arrive on the wire. Rejection::RecordUnstated closes OMISSION, not INVENTION. BasisCache::claim being the only constructor is a statement about clients written against this crate, not about the wire.').
|
||||
invariant(the_server_decides_which_records_are_checked,'reduce.rs::records_named is an exhaustive read of the ModelCommand, with one or-pattern for the variants that name no record so a new record-writing variant fails the build. Reading the list off the CLIENT claim would be a force by omission: the record tier skipped, no flag to remove, nothing in the message saying so.').
|
||||
invariant(grid_extent_is_the_whole_grid,'project::grid_of reads GridExtent from GridLayout::row_count/col_count, never from the clipped viewport ranges. The TUI builds its cache at offsets zero so the two coincide there -- the bug is only visible in a browser.').
|
||||
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. Host::commands applies in order and abandons at the first rejection.').
|
||||
invariant(opfs_flat_root,'OpfsFs::list returns BARE names where MemoryFs and PhysicalFs return dir-joined ones, so Path::new(\"\") is the one dir at which all three agree and the only one at which storage_contract describes OpfsFs. A worker using a subdirectory gets an empty picker in a browser and a full one in every test.').
|
||||
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 out of reach; impl Storage for Rc<OpfsFs> is refused by the orphan rule, so each host needs a local newtype.').
|
||||
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(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.').
|
||||
invariant(the_create_exemption_is_a_property_not_a_list,'A record that NEITHER resolves in the document NOR is claimed by the basis is one the frame is about to create, and only that combination is exempt from the record tier -- because BasisCache::claim filters to records the cache was told about, so [AddItem(East), SetCell(East)] could not claim one however it is ordered. A record that does not resolve but IS claimed is still RecordGone.').
|
||||
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(opfs_is_run_but_bindings_is_not,'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. Never instantiated by any harness: bindings.rs.').
|
||||
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,'workspace suite must stay under 2 seconds').
|
||||
@@ -141,6 +151,12 @@ 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_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(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.').
|
||||
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.').
|
||||
|
||||
Reference in New Issue
Block a user