Skip to content

feat(undo): metadata table + 0.7 format-migration boundary#44

Merged
mfreed merged 1 commit into
mainfrom
feat/undo-boundary
May 25, 2026
Merged

feat(undo): metadata table + 0.7 format-migration boundary#44
mfreed merged 1 commit into
mainfrom
feat/undo-boundary

Conversation

@mfreed
Copy link
Copy Markdown
Member

@mfreed mfreed commented May 25, 2026

  • New per-app companion table tigerfs.<app>_metadata (sibling of _history/_log/_savepoint) for
    non-operational events about a workspace. UUIDv7 entry_id PK in the same clock domain as log_id, plus subject,
    user_id, description, payload JSONB.
  • 0.6→0.7 migration writes one boundary row with subject = 'history-format-migration' as its final step. The
    undo engine refuses any undo whose target precedes the row's entry_id (single, to-log-id, and to-savepoint flows;
    all-or-nothing — no partial work). Error surfaces as EPERM with the row's description as the hint.
  • Why it's needed: ADR-017's relational-directory migration rewrites history rows' parent_id from the current
    source state, not historical state. For files moved between directories pre-migration, the historical location is
    destroyed — undoing such an entry would silently restore content while leaving the row at its current directory with
    no signal. Pre-migration entries stay readable in .log/ and .history/; only .undo/ refuses.
  • Single source of truth: subject names and the _metadata suffix live in
    internal/tigerfs/fs/synth/metadata.go; no literal strings inline. The undo engine's hard-coded blockingSubjects
    map owns the policy; the metadata table records facts.
  • Fresh installs unaffected: empty metadata table → checkBoundary returns nil immediately → zero per-undo
    overhead.
  • Tests: 12 unit cases for checkBoundary, 3 cache-load cases, DB round-trip + soft-fail, plus extended
    TestSynth_MigrateAddParentPointer (full 0.6 fixture → migrate → block via ExecuteUndoSingle/ExecuteUndoToLogID;
    allow via post-migration edit/ExecuteUndoToSavepoint/ExecuteUndoToLogID; idempotency) and new
    TestSynth_FreshInstall_MetadataFastPath.
  • Docs: docs/spec.md § Migration Boundaries, docs/history.md (Limitations bullet + four-companion-tables
    update), and new docs/adr/019-undo-boundary-via-metadata-table.md documenting alternatives considered.

Adds a fourth per-app companion table tigerfs.<app>_metadata for
non-operational events about a workspace -- sibling of _history,
_log, _savepoint. The 0.6→0.7 migration appends one row with
subject='history-format-migration' as its final step; the undo engine
refuses any undo whose target precedes that row's entry_id.

Why this is needed

ADR-017's relational-directory migration rewrites <app>_history rows'
parent_id from the *current* source state, not historical state. For
files moved between directories before the migration, the historical
parent_id is destroyed. Pre-migration log entries are structurally
valid against the new schema but semantically unsafe to undo: an undo
of an old "edit" that was actually a directory rename would silently
restore content while leaving the row at its current location, with
no signal to the user.

Mechanism

- New table tigerfs.<app>_metadata (entry_id UUIDv7 PK, subject TEXT,
  user_id TEXT, description TEXT, payload JSONB). Created by build.go
  for fresh installs and by addParentPointerMigration for upgrades.
- Subject values and the "_metadata" suffix are defined once in
  fs/synth/metadata.go and referenced by every consumer. No literal
  strings inline.
- loadSynthCache queries metadata at mount time for history-enabled
  apps, caches in ViewInfo.Metadata. Soft-fails to nil on transient
  DB errors -- fail-open is the right default (the alternative would
  refuse all undo on a momentary DB hiccup).
- fs/undo.go gains blockingSubjects (a hard-coded map of subjects
  that block undo) and checkBoundary, wired into ExecuteUndoSingle
  and ExecuteUndo. Engine owns the blocking-set decision; the
  metadata table records facts, not policy.

Refuse policy

If any blocking-subject entry has entry_id > target_log_id, the whole
undo is refused -- no partial work, no silent skip. Applies to all
three entry points (single, to-log-id, to-savepoint).

Tests

- Unit: 12 cases for checkBoundary, 3 cache-load cases, DB
  round-trip + soft-fail.
- Integration: TestSynth_MigrateAddParentPointer extended with the
  full 0.6 fixture → migrate → boundary verification flow (block via
  ExecuteUndoSingle and ExecuteUndoToLogID, allow via post-migration
  edit + ExecuteUndoToSavepoint + ExecuteUndoToLogID, idempotency).
  TestSynth_FreshInstall_MetadataFastPath validates that workspaces
  that never migrated have an empty metadata table and zero per-undo
  overhead.

Behavioral impact

Pre-v0.7 log entries in upgraded workspaces remain readable in .log/
and .history/ but cannot be undone -- noted in docs/history.md
Limitations and docs/spec.md § Migration Boundaries.

See ADR-019 for the design rationale and alternatives considered.
@mfreed mfreed merged commit 07e782a into main May 25, 2026
2 checks passed
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