One classifier for a typed string, one exhaustive walk over Rejection

improvise-j7v6: the rule turning a typed string into a `CellValue` was
written twice — `resolve::typed` (already moved into improvise-protocol
since the ticket was filed) and `commit_regular_cell_value`'s
if/else-if/else on the host's key path. The host now calls
`improvise_protocol::typed`, which is `pub` for that reason.

Per doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished),
`a_typed_string_is_classified_as_the_host_classifies_it` did not survive
as a comparison: type identity replaced its claim. It is re-scoped to the
thing this crate still owes — the FRAME the string composes — and renamed
`a_typed_string_composes_the_frame_that_carries_it`.

The ticket's flagged risk (gotcha 34: a shared classifier that disagrees
with the persistence layer is a third answer) is now asserted rather than
argued: tests/typed_string_agreement.rs round-trips what `typed` calls
text and what it calls a number through format_md/parse_md. Proved
discriminating: dropping `pipe_quote` from the writer fails it.

improvise-8uj8: `every_rejection_message_says_the_edit_did_not_land`
drove a hand-written five of seven `Rejection` variants while claiming
the fact held "over all of them". `Rejection::all` (the same cyclic walk
`AppMode::all` uses) and `Rejection::refused_an_edit` (exhaustive) now
drive it, and its claim is re-scoped to what is true: a rejection that
refused an EDIT says nothing was applied, and the two Save answers —
which refused no edit — say the document was not written. Renamed
`every_rejection_message_says_what_it_refused`.

Proved both ways: wording SaveFailed as "hmm" fails the test, which the
old list could not have seen; adding a Rejection variant fails the build
at basis.rs:101 and :126.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-20 20:36:51 -07:00
parent e89b6467e1
commit bc77e68f40
6 changed files with 139 additions and 31 deletions
@@ -318,16 +318,12 @@ mod tests {
/// Commit a cell value: a synthetic records key stages into the drill or applies
/// directly in plain records mode; a real key writes to the model (gotcha 19).
fn commit_regular_cell_value(key: &CellKey, value: &str, effects: &mut Vec<Box<dyn Effect>>) {
if value.is_empty() {
effects.push(Box::new(effect::ClearCell(key.clone())));
} else if let Ok(n) = value.parse::<f64>() {
effects.push(Box::new(effect::SetCell(key.clone(), CellValue::Number(n))));
} else {
effects.push(Box::new(effect::SetCell(
key.clone(),
CellValue::Text(value.to_string()),
)));
}
// The classification is the protocol's, not a second copy of it: the
// browser composes `SetCell` from the same rule (improvise-j7v6).
effects.push(match improvise_protocol::typed(value) {
Some(value) => Box::new(effect::SetCell(key.clone(), value)) as Box<dyn Effect>,
None => Box::new(effect::ClearCell(key.clone())),
});
}
/// Stage a synthetic edit in drill state so it can be applied atomically on exit.