model::id adds slugify, which case folds, turns each run of spaces into one
`_`, strips everything outside a-z, 0-9, `-` and `_`, then drops leading
underscores. It also adds IdAllocator, which keeps one increment-only counter
per document plus every id seen this session; claim admits hand-written ids
verbatim. Full case folding comes from the unicase crate. Nothing calls it yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
improvise-pntu: FoldMemo was keyed by (key, span) while the aggregation
and covers were call arguments, so one memo across two covers answered
the first (5.0 where 2.0 was right). FoldMemo::new(agg, covers) now owns
both and the _in functions read them off it; GridLayout::fold_memo builds
the grid's. The mismatched call no longer compiles.
improvise-0jw5: a test in improvise-command counts fold evaluations across
one grid_body call (18) against a fresh-memo control (90). A per-cell or
per-total fresh memo fails it. The counter is exposed through a new
test-support feature on improvise-core, enabled only as a dev-dependency.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
ADR 002's surrogate, and the reverse index a server resolves it through.
The hash is specified rather than inherited: SipHash-2-4 under a constant
key over an explicit length-prefixed byte serialization of the sorted
(category, item) pairs. Not DefaultHasher/RandomState (randomly seeded per
process), not #[derive(Hash)] (no stability guarantee). 128 bits, argued in
the doc comment: a collision is a silent write to the wrong record and no
later check catches it, while the cost of the wider hash is a bounded 8
bytes per visible cell. The wire form is hex-as-a-string, because a JSON
number above 2^53 comes back changed in a JavaScript host.
RecordHash::of normalizes, so a cell key and its record hash alike -- the
_Measure-drop is a property of the constructor, not a rule callers follow.
The index unifies ADR 002's resolution with ADR 001's record_versions:
RecordHash -> RecordEntry { record, cells, version }, one lookup answering
both "which record do you mean?" and "is your basis stale?". A zero cell
count is a tombstone rather than a removal, so a delete-and-recreate cannot
hand a stale basis its own version number back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Lay the groundwork for Phase B of improvise-36h: a new
`crates/improvise-core/` sub-crate that will house the pure-data core
(model, view, workbook, format) once the files move in the next commit.
Only scaffolding here:
- New `crates/improvise-core/Cargo.toml` mirroring improvise-formula's
structure; declares deps on improvise-formula, anyhow, indexmap, serde.
- New `crates/improvise-core/src/lib.rs` with the single re-export
`pub use improvise_formula as formula;` so that in the next commit the
moved code's `crate::formula::*` paths resolve unchanged.
- Root `Cargo.toml` adds the new crate to the workspace members and the
main crate's `[dependencies]`, ready to receive the move.
No source files change yet; cargo check --workspace still compiles as
before.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>