Files
suaveplan/openspec/archive/2026-04-24-enterprise-undo-2/tasks.md
2026-05-27 23:29:53 +01:00

17 KiB

Tasks: enterprise-undo

1. Scaffold

  • 1.1 Create universal/control-flow/undo/ with package.json (name: "@suaveplan/undo", version: "0.1.0", sideEffects: false, workspace:^ deps for @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/diff; external deps zod, @opentelemetry/api, tslib via catalog).
  • 1.2 Add tsconfig.json extending @suaveplan/typescript-config/library.json and tsconfig.lib.json for build output.
  • 1.3 Add vite.config.ts (tsup/vite library build targeting ESM + CJS dual output).
  • 1.4 Add vitest.config.ts with pool: 'forks', provider: 'istanbul', 100/100/100/100 coverage thresholds, setupFiles: ['@suaveplan/testing/polyfills'].
  • 1.5 Add @vitest/coverage-istanbul to devDependencies via catalog:testing.
  • 1.6 Create tests/ directory with cross-cutting guard tests: dep-graph (no disallowed @suaveplan/* at wrong layer), tree-shake (import from subpath, assert zero side-effect bytes), file-size budget (no file exceeds 300 lines), no-disallowed-patterns (any, TODO, ts-ignore, .skip).
  • 1.7 Add package to root CLAUDE.md listing, README.md table, and FOLDERS.yml.
  • 1.8 Create README.md skeleton at the package root (install, quick-start stub, Modules section placeholder — expand in task 13).

2. Types and errors

  • 2.1 Create src/types/undo.types.ts — full type definitions: Command<T>, PatchCommand<T>, AsyncCommand<T>, StackEntry<T>, Branch<T>, UndoStackOptions, BranchingOptions, UndoStackState<T>, BranchingState<T>, TransactionHandle<T>, AsyncTransactionHandle<T>, CommandMergerOptions, SelectiveUndoOptions, SelectiveUndoResult<T>, DependencyChecker<T>, HistoryCompressorOptions, CompressionResult, MergePredicate<T>.
  • 2.2 Add isPatchCommand<T> and isAsyncCommand<T> type guards to src/types/undo.types.ts.
  • 2.3 Add src/types/undo.types.md co-located docs (≥200 words, purpose, all exported types documented).
  • 2.4 Create src/errors/undo-error.ts — UndoError class extending SuaveplanError; call defineCodes to register UNDO.EMPTY_STACK, UNDO.AT_BRANCH_ROOT, UNDO.DEPENDENCY_CONFLICT, UNDO.COMMAND_FAILED, UNDO.TRANSACTION_ACTIVE, UNDO.TRANSACTION_INACTIVE; each code typed with { commandId?: string; branchId?: string; conflictIds?: string[] } context.
  • 2.5 Create src/errors/undo-error.md co-located docs.
  • 2.6 Create src/errors/undo-error.test.ts — test construction, code narrowing, context fields, and that each code is unique and stable.
  • 2.7 Create src/schemas/undo.schemas.ts — Zod schemas: UndoStackOptionsSchema, BranchingOptionsSchema, HistoryCompressorOptionsSchema, CommandMergerOptionsSchema, SelectiveUndoOptionsSchema; each validated and exported; schemas must match the TypeScript interfaces (use z.infer assertion tests).
  • 2.8 Create src/schemas/undo.schemas.md co-located docs.
  • 2.9 Create src/schemas/undo.schemas.test.ts — parse valid/invalid inputs for each schema.

3. Command primitives

  • 3.1 Create src/command/command.ts — implement createCommand, composeCommands, executeCommand, undoCommand, createPatchCommand, estimateCommandSize. Replace bare Date.now() with injectable Clock; replace bare crypto.randomUUID() with injectable IdGenerator. Accept optional { clock?, idGenerator? } options param on factory functions while keeping zero-config defaults.
  • 3.2 createPatchCommand must use diff + invertPatch from @suaveplan/diff — no local JSON Patch implementation.
  • 3.3 executeCommand / undoCommand must use applyPatch from @suaveplan/diff for PatchCommand branches.
  • 3.4 Create src/command/command.md co-located docs (≥200 words, simple + advanced examples, API reference, implementation notes on @suaveplan/diff delegation).
  • 3.5 Create src/command/command.test.ts — unit tests: command creation (id unique, timestamp set, label preserved), compose (execute order, undo reverse order, empty compose), execute/undo for closure commands, execute/undo for patch commands (round-trip), estimateCommandSize for both variants, metadata propagation. Timeout { timeout: 5000 } on each it.
  • 3.6 Property-based test in src/command/command.test.ts: fast-check fc.anything() states; executeCommand(undoCommand(state, cmd), forward) restores state for patch commands.

4. UndoStack (sync linear stack)

  • 4.1 Create src/stack/stack.ts — UndoStack<T> class. Constructor signature: constructor(initial: T, options?: UndoStackOptions, bus?: EventBusLike). Replace bare Date.now() and crypto.randomUUID() with injected clock/idGenerator forwarded through to command factories. Emit events on push, undo, redo, clear when bus is provided. Throw UndoError with UNDO.TRANSACTION_ACTIVE / UNDO.TRANSACTION_INACTIVE codes instead of bare Error. Enforce maxEntries and maxMemoryBytes limits; call onTruncate callback.
  • 4.2 Methods: push, pushState, undo, redo, beginTransaction (returns TransactionHandle<T>), clear, reset, snapshot, getUndoStack, getRedoStack. Getters: current, canUndo, canRedo, undoSize, redoSize, activeBranchId.
  • 4.3 Validate constructor options with UndoStackOptionsSchema (parse and throw UndoError on invalid options).
  • 4.4 OTel: instrument push, undo, redo with undo.push.total, undo.undo.total, undo.redo.total counters; record undo.stack.depth histogram after each mutation.
  • 4.5 Create src/stack/stack.md co-located docs.
  • 4.6 Create src/stack/stack.test.ts — unit tests: push increments undoSize, undo/redo round-trip, canUndo/canRedo flags, maxEntries truncation calls onTruncate, maxMemoryBytes truncation, transaction commit, transaction rollback, nested-transaction guard, clear resets memory, snapshot shape, event emission (push/undo/redo/clear). Each it has { timeout: 5000 }.
  • 4.7 Property-based test in src/stack/stack.test.ts: push N commands, then undo all, then redo all — final state equals state after push N.

5. BranchingHistory

  • 5.1 Create src/branch/branch.ts — BranchingHistory<T> class. Constructor: constructor(initial: T, options?: BranchingOptions, clock?: Clock, idGenerator?: IdGenerator). Use clock.now() (from @suaveplan/types) for Branch.createdAt and for command timestamps forwarded to factories. Use idGenerator.generate() for new branch IDs.
  • 5.2 Methods: push, pushState, undo, redo, switchBranch, getBranch, deleteBranch, beginTransaction (returns TransactionHandle<T>), snapshot. Getters: current, canUndo, canRedo, activeBranchId, branches.
  • 5.3 switchBranch: throw UndoError with UNDO.AT_BRANCH_ROOT code (and { branchId } context) when target branch does not exist.
  • 5.4 deleteBranch: throw UndoError when attempting to delete the active branch.
  • 5.5 branchOnDiverge (default true): when pushing with redo entries pending, fork redo entries to a new branch rather than truncating. When false, truncate in-place.
  • 5.6 Validate options with BranchingOptionsSchema.
  • 5.7 OTel: instrument push, undo, redo, switchBranch with undo.push.total / undo.undo.total / undo.redo.total (label stack_type: 'branching'); record undo.stack.depth histogram.
  • 5.8 Create src/branch/branch.md co-located docs (emphasise branching semantics, replay algorithm, Clock injection).
  • 5.9 Create src/branch/branch.test.ts — unit tests: linear undo/redo, diverge creates new branch, switchBranch replays state correctly, deleteBranch frees memory, branchOnDiverge: false truncates, transaction commit/rollback, Clock injection (deterministic createdAt). Property test: branch isolation — mutating one branch never changes another branch's entries.

6. Async commands and AsyncUndoStack

  • 6.1 Create src/async/async-command.ts — createAsyncCommand, composeAsyncCommands, executeAsyncCommand, undoAsyncCommand. Accept optional { clock?, idGenerator? }. Wrap rejecting execute/undo calls with UndoError code UNDO.COMMAND_FAILED (with original error as cause).
  • 6.2 Create src/async/async-command.md co-located docs.
  • 6.3 Create src/async/async-command.test.ts — tests for each factory + compose order + error wrapping. Use { timeout: 30000 } on async it blocks.
  • 6.4 Create src/async/async-stack.ts — AsyncUndoStack<T>. Constructor: constructor(initial: T, options?: UndoStackOptions, bus?: EventBusLike). Async push, undo, redo wrap command failures in UndoError with UNDO.COMMAND_FAILED. beginTransaction returns AsyncTransactionHandle<T>. clear, reset, snapshot, getters mirror UndoStack. Throw UndoError with UNDO.TRANSACTION_ACTIVE / UNDO.TRANSACTION_INACTIVE on guard violations. Validate options with UndoStackOptionsSchema. Emit bus events on push/undo/redo/clear when provided.
  • 6.5 OTel: undo.push.total, undo.undo.total, undo.redo.total with stack_type: 'async'; undo.stack.depth histogram.
  • 6.6 Create src/async/async-stack.md co-located docs.
  • 6.7 Create src/async/async-stack.test.ts — async push/undo/redo round-trip, AbortSignal cancellation (pass signal into execute/undo functions and test abort path), error wrapping, transaction commit/rollback, event emission. { timeout: 30000 } on async tests.

7. HistoryCompressor

  • 7.1 Create src/compression/history-compressor.ts — HistoryCompressor<T> and compressHistory function. Constructor validates options with HistoryCompressorOptionsSchema. compress(stack) groups old entries by label, squashes via composeCommands, respects maxHistorySize, rebuilds the stack by replaying from initial state. Returns CompressionResult & { stack: UndoStack<T> }.
  • 7.2 OTel: increment undo.compress.total; record undo.compress.ratio histogram (originalCount / compressedCount).
  • 7.3 Create src/compression/history-compressor.md co-located docs.
  • 7.4 Create src/compression/history-compressor.test.ts — tests: no-op when within depth, squashes old entries, respects maxHistorySize, rebuild preserves current state, CompressionResult shape. Property test: compressing a stack twice with the same depth never changes state (idempotency).

8. SelectiveUndoStack

  • 8.1 Create src/selective/selective-undo.ts — SelectiveUndoStack<T> extending UndoStack<T>. Constructor: constructor(initial: T, options?: SelectiveUndoOptions, bus?: EventBusLike). undoById(id) detects conflicts via dependencyCheck, throws UndoError with UNDO.DEPENDENCY_CONFLICT (with { conflictIds } context) when options.throwOnConflict is set, otherwise returns SelectiveUndoResult with success: false. getDependents(id), canUndoById(id) remain non-throwing.
  • 8.2 Validate options with SelectiveUndoOptionsSchema.
  • 8.3 OTel: undo.undo.total with stack_type: 'selective' when undoById succeeds.
  • 8.4 Create src/selective/selective-undo.md co-located docs (explain dependency detection algorithm, re-application order, conflict reporting).
  • 8.5 Create src/selective/selective-undo.test.ts — tests: undo by ID with no deps, undo by ID with conflicting dep (returns conflict list), getDependents, canUndoById, re-application preserves later commands, unknown ID returns success: false, throwOnConflict path. { timeout: 5000 } on each it.

9. CommandMerger

  • 9.1 Create src/merge/command-merger.ts — CommandMerger<T>. Constructor validates options with CommandMergerOptionsSchema. add(command) returns a flushed command when the time or count window closes, otherwise undefined. flush(), reset(), canMerge(a, b). Getters: hasPending, pendingCount. Default predicate merges commands with identical label.
  • 9.2 Use injectable Clock (from @suaveplan/types) for time-window calculation so tests can control elapsed time without real Date.now().
  • 9.3 Create src/merge/command-merger.md co-located docs.
  • 9.4 Create src/merge/command-merger.test.ts — tests: merge within window (count and time), flush on window close, custom predicate, empty flush returns undefined, reset discards pending, hasPending/pendingCount getters, deterministic with injected clock. Property test: merging [a, b, c] into a single command and executing it produces the same state as executing a, b, c individually.

10. OTel instrumentation (cross-cutting review)

  • 10.1 Confirm all six counters and two histograms are wired to the undo meter in each module (stack, branch, async, selective, compression).
  • 10.2 Verify all meters are no-ops when no exporter is registered (use @opentelemetry/api default no-op SDK in tests — no real exporter needed).
  • 10.3 Add integration tests in tests/otel.test.ts that wire a @opentelemetry/api in-memory exporter and assert counter increments for push, undo, redo, and compress operations across all stack types.

11. Property-based tests

  • 11.1 Create tests/properties/command.prop.test.ts — fast-check: createCommand + execute + undo round-trip for arbitrary state transforms.
  • 11.2 Create tests/properties/stack.prop.test.ts — fast-check: push N commands, undo all, redo all → state matches; undoSize never exceeds maxEntries.
  • 11.3 Create tests/properties/branch.prop.test.ts — fast-check: branch isolation (mutating one branch never changes another's entries); undo/redo cursor invariant.
  • 11.4 Create tests/properties/compress.prop.test.ts — fast-check: compressHistory idempotency; compressed state equals original current state.
  • 11.5 Create tests/properties/merge.prop.test.ts — fast-check: CommandMerger merge associativity; merged command executes identically to sequential individual commands.
  • 11.6 All property tests use { timeout: 30000 } and ≥ 100 fast-check examples.

12. Benchmarks

  • 12.1 Create src/__benchmarks__/stack.bench.ts — mitata: UndoStack.push / undo / redo with no-op closure commands — 10 000 iterations; with patch commands on a 50-field JSON object — 10 000 iterations.
  • 12.2 Create src/__benchmarks__/branch.bench.ts — mitata: BranchingHistory.push + switchBranch — 100 branches of 10 commands each.
  • 12.3 Create src/__benchmarks__/compress.bench.ts — mitata: compressHistory on a 1 000-entry stack with compressionDepth: 100.
  • 12.4 Publish baseline throughput numbers in README.md Benchmarks section. Add CI regression comment (>10% degradation fails the gate).

13. Documentation

  • 13.1 Ensure every implementation file has a sibling .md with ≥200 words, a simple example, an advanced example, full API reference, and implementation notes. Files requiring .md: command.ts, stack.ts, branch.ts, async-command.ts, async-stack.ts, history-compressor.ts, selective-undo.ts, command-merger.ts, undo-error.ts, undo.types.ts, undo.schemas.ts.
  • 13.2 Finalize README.md at package root: one-paragraph description, install snippet, quick-start with UndoStack and BranchingHistory, Modules section linking each co-located .md, Benchmarks section with baseline numbers. No category-level README.md inside src/ subdirectories.
  • 13.3 Verify docs coverage: bun run docs:check --filter=@suaveplan/undo ≥ 90%.

14. Verify and publish

  • 14.1 bunx turbo lint --filter=@suaveplan/undo — zero Biome errors or suppressions.
  • 14.2 bunx turbo typecheck --filter=@suaveplan/undo — zero TypeScript errors; no any, ts-ignore, ts-expect-error.
  • 14.3 bunx turbo test --filter=@suaveplan/undo — all tests green; no .skip, .only.
  • 14.4 bunx turbo test:coverage --filter=@suaveplan/undo — 100/100/100/100 (statement / branch / function / line).
  • 14.5 bunx turbo build --filter=@suaveplan/undo — ESM + CJS dual output; subpath exports in package.json verified (one entry per feature module: ./command/command, ./stack/stack, ./branch/branch, ./async/async-command, ./async/async-stack, ./compression/history-compressor, ./selective/selective-undo, ./merge/command-merger, ./types/undo.types, ./schemas/undo.schemas, ./errors/undo-error, ".").
  • 14.6 Banned-pattern scan: grep -rn 'TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|@deprecated' src/ → zero hits.
  • 14.7 Missing-.md scan: every .ts implementation file (excluding .test.ts, .bench.ts, barrel index.ts, constants/index.ts) has a sibling .md → zero missing.
  • 14.8 Tree-shake test: importing only ./stack/stack does not pull in BranchingHistory or HistoryCompressor — assert byte count.
  • 14.9 Fold spec delta into openspec/specs/undo/spec.md.
  • 14.10 Archive this change folder to openspec/archive/2026-04-23-enterprise-undo/.
  • 14.11 bun scripts/publish.ts --package @suaveplan/undo.
  • 14.12 Update root README.md enterprise column for @suaveplan/undo to ✅.