Skip to content

Fix undo ledger holes at the store seam and refuse outdated draft stores#127

Open
kostyafarber wants to merge 3 commits into
mainfrom
fix/ledger-store-seam
Open

Fix undo ledger holes at the store seam and refuse outdated draft stores#127
kostyafarber wants to merge 3 commits into
mainfrom
fix/ledger-store-seam

Conversation

@kostyafarber

Copy link
Copy Markdown
Collaborator

Adversarial re-review of #119 and #121 (rebased on #124) found the undo ledger and the SQLite draft store disagreeing about what a replay restores. This PR fixes the seam and adds a structural test invariant so the class stays fixed.

Bugs fixed

  1. Wedged batch undo (FK violation on reverse replay). Undoing [DeleteAxis, DeleteSource] replays steps reversed, so the source row is re-inserted before its axis row exists; the location upsert hits a foreign-key violation, the hand-back mechanism restores the entry, and every retry fails identically — undo is permanently stuck. The store now skips location entries on axes that have no row (mirroring replace_font_state); the axis step that follows in the same replay re-emits the located sources and fills the values in. Both batch orders plus redo/undo cycles are covered by regression tests.

  2. SourceCreated replay wiped source fields. The change record carried only name + location, so undoing a source delete (or an axis delete, which re-emits located sources) blanked filename, color, role, layer_name, and the source lib in the draft store while memory kept them — divergence that surfaced on crash-recovery. SourceCreated now snapshots the full source and the store handler writes all of it, including source_lib.

  3. SCHEMA_VERSION never bumped. feat: UFO round-trip preservation contract #119/fix: designspace saves preserve project structure with atomic writes (re-target) #124 added tables and columns (fontinfo_remainder, source_lib, font_binaries, sources.color, sources.layer_name) while the version stayed 1, so older draft stores failed opaquely with "no such table". Version is now 2 and an outdated store is refused with an actionable error ("delete the draft store and re-import the font") — per the pre-release policy: baseline edited in place, no migration machinery.

  4. order_index never properly persisted. Created axes/sources/glyphs were hardcoded to order 0 and a glyph rename reset the glyph's order. Creates now append at COUNT(*) (mirroring the in-memory Vec::push), deletes compact the sequence (mirroring Vec::remove), and identity changes no longer touch order.

  5. Bonus hole caught by the new invariant: restoring a deleted glyph layer (source-delete undo, glyph replay) re-created a bare row — contours, anchors, components, guidelines, and layer lib never reached the store. GlyphLayerValue now carries the layer's full content and replays emit it alongside the created row.

Structural enforcement

workspace_test.rs now routes every undo/redo through undo_and_verify / redo_and_verify, which reload the font from the draft store and assert_eq! it against the in-memory font (Font equality covers all persisted data, index caches excluded). This is the invariant that catches bugs 1, 2, 4, and 5 structurally, and new intent tests inherit it.

Tests

  • cargo test --workspace: all green (122 shift-font, 39 shift-store, 48 shift-workspace, plus backends/bridge/source suites)
  • New: old-schema refusal (v1 fixture), full-source SourceCreated round-trip, missing-axis location skip, order append/compaction, [DeleteAxis, DeleteSource] batch undo/redo in both orders, source extras surviving delete undo
  • cargo fmt and cargo clippy --all-targets clean; pre-commit hooks (check/fmt/clippy/test) passed per commit

🤖 Generated with Claude Code

The sources/source_lib/fontinfo_remainder/font_binaries changes from the
UFO preservation and designspace work reshaped the baseline schema while
SCHEMA_VERSION stayed 1, so pre-existing draft stores failed opaquely
with "no such table" mid-session. Bump the version and refuse outdated
stores up front with an actionable error (delete the draft store and
re-import) — pre-release policy is baseline-in-place, no migrations.
Undo/redo replays persist through the same change-set seam as apply,
but the seam dropped state the in-memory replay kept, so the draft
store silently diverged from the live font:

- SourceCreated carried only name + location; replaying a source
  delete undo (or an axis-delete undo, which re-emits located sources)
  wiped filename, color, role, layer_name, and source lib in the
  store. The change now snapshots the full source.
- A batch like [DeleteAxis, DeleteSource] wedged undo forever: the
  reversed replay re-inserts the source before its axis row exists,
  hitting a foreign-key violation on the location upsert, and the
  hand-back retry loop can never get past it. Location entries on
  axes without rows are now skipped (mirroring replace_font_state);
  the axis step that follows re-emits the source and fills them in.
- order_index was hardcoded 0 for created axes/sources/glyphs and a
  glyph rename reset it. Creates now append at COUNT(*), deletes
  compact the sequence (mirroring Vec::remove), renames leave order
  untouched.
- Restoring a deleted glyph layer re-created a bare row; contours,
  anchors, components, guidelines, and layer lib never reached the
  store. GlyphLayerValue now carries the layer's full content and
  replays emit it alongside the created row.
Every undo/redo in the workspace intent tests now reloads the font
from the draft store and asserts it equals the in-memory font, so any
field a future replay drops or misorders fails structurally instead
of surfacing at crash-recovery. Adds regression tests for the
previously wedged [DeleteAxis, DeleteSource] batch (both orders,
including redo/undo cycles) and for source extras surviving delete
undo, driven by the kitchen-sink sample font.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant