fix(core): a model over a source refuses every write in every build and drops its own cells (improvise-rnak)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (dbsp-sources)
2026-10-10 18:57:13 -07:00
committed by Edward Langley
parent 7c5f77708c
commit bebaeb2d61
5 changed files with 112 additions and 8 deletions
+7 -4
View File
@@ -65,10 +65,13 @@ the owned `DataStore`. That is the whole evaluator hook (improvise-rnak, bw4p.14
there is still one evaluator, `Model`'s, and it computes every formula value, over a source whose
cells it never owns.
1. **A model over a source is read-only.** `set_cell` and `clear_cell` `debug_assert` that no source
is set, because they write the owned store, which is then not read. A release build would drop the
write silently; the assert is the guard, not the design. The design is that a caller who holds a
source writes to the source (ADR 003: the trait has no writes), and the model is built over it.
1. **A model over a source is read-only, in every build.** `set_cell`, `clear_cell`, `remove_category` and
`remove_item` `assert!` that no source is set (a `debug_assert` first shipped and was lost in release:
the write went to the owned store, which nothing reads). A caller who holds a source writes to the
source (ADR 003: the trait has no writes). `Model::over` drops the model's own cells, so no stale copy
is left for `get_cell`, persistence or the harness to read. `data` is `pub`, so a caller can still write
it directly; it is not read, and nothing prevents that. Structural edits (a category or item removed)
are refused too, because the source's cells would still carry them.
2. **A shared source changes behind its readers by interior mutability of its own.** The model holds
an `Arc`, so a database connection or a test double behind a lock is the same object the
backend feeds from.
+25
View File
@@ -2397,3 +2397,28 @@ Extremes with no numbers, and a NaN as an extreme, are dropped by `Numbers::from
Review of #356 (Codex): `DataStore::keys` went through `cells` and cloned every value. It reads keys
only now; the test pins the keys and their order against `cells`. No clone-count test: a value clone is
not observable from outside, so the equality is all that can be asserted.
### a_model_over_a_source_refuses_a_clear
Review of #357: the `clear_cell` guard had no test. The refusal is an `assert!`, so the test holds in a
release build too (`cargo test --release -p improvise-core --lib over_tests` was run: the earlier
`debug_assert!` failed all four refusal tests there, `a_model_over_a_source_refuses_a_write` among
them, and was the bug). Rival: the guard removed from `clear_cell` fails this test only.
### a_model_over_a_source_refuses_to_remove_a_category
`remove_category` swept the owned store with no guard. Rival: the guard removed.
### a_model_over_a_source_refuses_to_remove_an_item
The same for `remove_item`.
### putting_a_populated_model_over_a_source_drops_its_own_cells
`Model::over` empties the owned store, so `get_cell` on an over-model is `None` for a cell the
source holds, and the evaluator still reads the source's value.
### records_mode_reads_the_records_from_a_source
Review of #357: a records-mode `GridLayout` over a source. `group_records` reading the owned store
left all 563 tests green; with this test it fails (the grid has no rows).