Files
2026-05-27 23:29:53 +01:00

131 lines
17 KiB
Markdown

# Tasks: enterprise-undo
## 1. Scaffold
- [x] 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).
- [x] 1.2 Add `tsconfig.json` extending `@suaveplan/typescript-config/library.json` and `tsconfig.lib.json` for build output.
- [x] 1.3 Add `vite.config.ts` (tsup/vite library build targeting ESM + CJS dual output).
- [x] 1.4 Add `vitest.config.ts` with `pool: 'forks'`, `provider: 'istanbul'`, 100/100/100/100 coverage thresholds, `setupFiles: ['@suaveplan/testing/polyfills']`.
- [x] 1.5 Add `@vitest/coverage-istanbul` to `devDependencies` via `catalog:testing`.
- [x] 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`).
- [x] 1.7 Add package to root `CLAUDE.md` listing, `README.md` table, and `FOLDERS.yml`.
- [x] 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
- [x] 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>`.
- [x] 2.2 Add `isPatchCommand<T>` and `isAsyncCommand<T>` type guards to `src/types/undo.types.ts`.
- [x] 2.3 Add `src/types/undo.types.md` co-located docs (≥200 words, purpose, all exported types documented).
- [x] 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.
- [x] 2.5 Create `src/errors/undo-error.md` co-located docs.
- [x] 2.6 Create `src/errors/undo-error.test.ts` — test construction, code narrowing, context fields, and that each code is unique and stable.
- [x] 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).
- [x] 2.8 Create `src/schemas/undo.schemas.md` co-located docs.
- [x] 2.9 Create `src/schemas/undo.schemas.test.ts` — parse valid/invalid inputs for each schema.
## 3. Command primitives
- [x] 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.
- [x] 3.2 `createPatchCommand` must use `diff` + `invertPatch` from `@suaveplan/diff` — no local JSON Patch implementation.
- [x] 3.3 `executeCommand` / `undoCommand` must use `applyPatch` from `@suaveplan/diff` for `PatchCommand` branches.
- [x] 3.4 Create `src/command/command.md` co-located docs (≥200 words, simple + advanced examples, API reference, implementation notes on `@suaveplan/diff` delegation).
- [x] 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`.
- [x] 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)
- [x] 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.
- [x] 4.2 Methods: `push`, `pushState`, `undo`, `redo`, `beginTransaction` (returns `TransactionHandle<T>`), `clear`, `reset`, `snapshot`, `getUndoStack`, `getRedoStack`. Getters: `current`, `canUndo`, `canRedo`, `undoSize`, `redoSize`, `activeBranchId`.
- [x] 4.3 Validate constructor `options` with `UndoStackOptionsSchema` (parse and throw `UndoError` on invalid options).
- [x] 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.
- [x] 4.5 Create `src/stack/stack.md` co-located docs.
- [x] 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 }`.
- [x] 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
- [x] 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.
- [x] 5.2 Methods: `push`, `pushState`, `undo`, `redo`, `switchBranch`, `getBranch`, `deleteBranch`, `beginTransaction` (returns `TransactionHandle<T>`), `snapshot`. Getters: `current`, `canUndo`, `canRedo`, `activeBranchId`, `branches`.
- [x] 5.3 `switchBranch`: throw `UndoError` with `UNDO.AT_BRANCH_ROOT` code (and `{ branchId }` context) when target branch does not exist.
- [x] 5.4 `deleteBranch`: throw `UndoError` when attempting to delete the active branch.
- [x] 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.
- [x] 5.6 Validate `options` with `BranchingOptionsSchema`.
- [x] 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.
- [x] 5.8 Create `src/branch/branch.md` co-located docs (emphasise branching semantics, replay algorithm, Clock injection).
- [x] 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
- [x] 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`).
- [x] 6.2 Create `src/async/async-command.md` co-located docs.
- [x] 6.3 Create `src/async/async-command.test.ts` — tests for each factory + compose order + error wrapping. Use `{ timeout: 30000 }` on async `it` blocks.
- [x] 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.
- [x] 6.5 OTel: `undo.push.total`, `undo.undo.total`, `undo.redo.total` with `stack_type: 'async'`; `undo.stack.depth` histogram.
- [x] 6.6 Create `src/async/async-stack.md` co-located docs.
- [x] 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
- [x] 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> }`.
- [x] 7.2 OTel: increment `undo.compress.total`; record `undo.compress.ratio` histogram (`originalCount / compressedCount`).
- [x] 7.3 Create `src/compression/history-compressor.md` co-located docs.
- [x] 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
- [x] 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.
- [x] 8.2 Validate `options` with `SelectiveUndoOptionsSchema`.
- [x] 8.3 OTel: `undo.undo.total` with `stack_type: 'selective'` when `undoById` succeeds.
- [x] 8.4 Create `src/selective/selective-undo.md` co-located docs (explain dependency detection algorithm, re-application order, conflict reporting).
- [x] 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
- [x] 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`.
- [x] 9.2 Use injectable `Clock` (from `@suaveplan/types`) for time-window calculation so tests can control elapsed time without real `Date.now()`.
- [x] 9.3 Create `src/merge/command-merger.md` co-located docs.
- [x] 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)
- [x] 10.1 Confirm all six counters and two histograms are wired to the `undo` meter in each module (`stack`, `branch`, `async`, `selective`, `compression`).
- [x] 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).
- [x] 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
- [x] 11.1 Create `tests/properties/command.prop.test.ts` — fast-check: `createCommand` + execute + undo round-trip for arbitrary state transforms.
- [x] 11.2 Create `tests/properties/stack.prop.test.ts` — fast-check: push N commands, undo all, redo all → state matches; `undoSize` never exceeds `maxEntries`.
- [x] 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.
- [x] 11.4 Create `tests/properties/compress.prop.test.ts` — fast-check: `compressHistory` idempotency; compressed state equals original current state.
- [x] 11.5 Create `tests/properties/merge.prop.test.ts` — fast-check: `CommandMerger` merge associativity; merged command executes identically to sequential individual commands.
- [x] 11.6 All property tests use `{ timeout: 30000 }` and ≥ 100 fast-check examples.
## 12. Benchmarks
- [x] 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.
- [x] 12.2 Create `src/__benchmarks__/branch.bench.ts` — mitata: `BranchingHistory.push` + `switchBranch` — 100 branches of 10 commands each.
- [x] 12.3 Create `src/__benchmarks__/compress.bench.ts` — mitata: `compressHistory` on a 1 000-entry stack with `compressionDepth: 100`.
- [x] 12.4 Publish baseline throughput numbers in `README.md` Benchmarks section. Add CI regression comment (>10% degradation fails the gate).
## 13. Documentation
- [x] 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`.
- [x] 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.
- [x] 13.3 Verify docs coverage: `bun run docs:check --filter=@suaveplan/undo` ≥ 90%.
## 14. Verify and publish
- [x] 14.1 `bunx turbo lint --filter=@suaveplan/undo` — zero Biome errors or suppressions.
- [x] 14.2 `bunx turbo typecheck --filter=@suaveplan/undo` — zero TypeScript errors; no `any`, `ts-ignore`, `ts-expect-error`.
- [x] 14.3 `bunx turbo test --filter=@suaveplan/undo` — all tests green; no `.skip`, `.only`.
- [x] 14.4 `bunx turbo test:coverage --filter=@suaveplan/undo` — 100/100/100/100 (statement / branch / function / line).
- [x] 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`, `"."`).
- [x] 14.6 Banned-pattern scan: `grep -rn 'TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|@deprecated' src/` → zero hits.
- [x] 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.
- [x] 14.8 Tree-shake test: importing only `./stack/stack` does not pull in `BranchingHistory` or `HistoryCompressor` — assert byte count.
- [x] 14.9 Fold spec delta into `openspec/specs/undo/spec.md`.
- [x] 14.10 Archive this change folder to `openspec/archive/2026-04-23-enterprise-undo/`.
- [x] 14.11 `bun scripts/publish.ts --package @suaveplan/undo`.
- [x] 14.12 Update root `README.md` enterprise column for `@suaveplan/undo` to ✅.