17 KiB
17 KiB
Tasks: enterprise-undo
1. Scaffold
- 1.1 Create
universal/control-flow/undo/withpackage.json(name: "@suaveplan/undo",version: "0.1.0",sideEffects: false,workspace:^deps for@suaveplan/error,@suaveplan/types,@suaveplan/events,@suaveplan/diff; external depszod,@opentelemetry/api,tslibvia catalog). - 1.2 Add
tsconfig.jsonextending@suaveplan/typescript-config/library.jsonandtsconfig.lib.jsonfor build output. - 1.3 Add
vite.config.ts(tsup/vite library build targeting ESM + CJS dual output). - 1.4 Add
vitest.config.tswithpool: 'forks',provider: 'istanbul', 100/100/100/100 coverage thresholds,setupFiles: ['@suaveplan/testing/polyfills']. - 1.5 Add
@vitest/coverage-istanbultodevDependenciesviacatalog: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.mdlisting,README.mdtable, andFOLDERS.yml. - 1.8 Create
README.mdskeleton 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>andisAsyncCommand<T>type guards tosrc/types/undo.types.ts. - 2.3 Add
src/types/undo.types.mdco-located docs (≥200 words, purpose, all exported types documented). - 2.4 Create
src/errors/undo-error.ts—UndoErrorclass extendingSuaveplanError; calldefineCodesto registerUNDO.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.mdco-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 (usez.inferassertion tests). - 2.8 Create
src/schemas/undo.schemas.mdco-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— implementcreateCommand,composeCommands,executeCommand,undoCommand,createPatchCommand,estimateCommandSize. Replace bareDate.now()with injectableClock; replace barecrypto.randomUUID()with injectableIdGenerator. Accept optional{ clock?, idGenerator? }options param on factory functions while keeping zero-config defaults. - 3.2
createPatchCommandmust usediff+invertPatchfrom@suaveplan/diff— no local JSON Patch implementation. - 3.3
executeCommand/undoCommandmust useapplyPatchfrom@suaveplan/diffforPatchCommandbranches. - 3.4 Create
src/command/command.mdco-located docs (≥200 words, simple + advanced examples, API reference, implementation notes on@suaveplan/diffdelegation). - 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),estimateCommandSizefor both variants, metadata propagation. Timeout{ timeout: 5000 }on eachit. - 3.6 Property-based test in
src/command/command.test.ts: fast-checkfc.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 bareDate.now()andcrypto.randomUUID()with injected clock/idGenerator forwarded through to command factories. Emit events onpush,undo,redo,clearwhenbusis provided. ThrowUndoErrorwithUNDO.TRANSACTION_ACTIVE/UNDO.TRANSACTION_INACTIVEcodes instead of bareError. EnforcemaxEntriesandmaxMemoryByteslimits; callonTruncatecallback. - 4.2 Methods:
push,pushState,undo,redo,beginTransaction(returnsTransactionHandle<T>),clear,reset,snapshot,getUndoStack,getRedoStack. Getters:current,canUndo,canRedo,undoSize,redoSize,activeBranchId. - 4.3 Validate constructor
optionswithUndoStackOptionsSchema(parse and throwUndoErroron invalid options). - 4.4 OTel: instrument
push,undo,redowithundo.push.total,undo.undo.total,undo.redo.totalcounters; recordundo.stack.depthhistogram after each mutation. - 4.5 Create
src/stack/stack.mdco-located docs. - 4.6 Create
src/stack/stack.test.ts— unit tests: push increments undoSize, undo/redo round-trip,canUndo/canRedoflags,maxEntriestruncation callsonTruncate,maxMemoryBytestruncation, transaction commit, transaction rollback, nested-transaction guard,clearresets memory,snapshotshape, event emission (push/undo/redo/clear). Eachithas{ 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). Useclock.now()(from@suaveplan/types) forBranch.createdAtand for command timestamps forwarded to factories. UseidGenerator.generate()for new branch IDs. - 5.2 Methods:
push,pushState,undo,redo,switchBranch,getBranch,deleteBranch,beginTransaction(returnsTransactionHandle<T>),snapshot. Getters:current,canUndo,canRedo,activeBranchId,branches. - 5.3
switchBranch: throwUndoErrorwithUNDO.AT_BRANCH_ROOTcode (and{ branchId }context) when target branch does not exist. - 5.4
deleteBranch: throwUndoErrorwhen attempting to delete the active branch. - 5.5
branchOnDiverge(defaulttrue): when pushing with redo entries pending, fork redo entries to a new branch rather than truncating. Whenfalse, truncate in-place. - 5.6 Validate
optionswithBranchingOptionsSchema. - 5.7 OTel: instrument
push,undo,redo,switchBranchwithundo.push.total/undo.undo.total/undo.redo.total(labelstack_type: 'branching'); recordundo.stack.depthhistogram. - 5.8 Create
src/branch/branch.mdco-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,switchBranchreplays state correctly,deleteBranchfrees memory,branchOnDiverge: falsetruncates, transaction commit/rollback, Clock injection (deterministiccreatedAt). 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 rejectingexecute/undocalls withUndoErrorcodeUNDO.COMMAND_FAILED(with original error ascause). - 6.2 Create
src/async/async-command.mdco-located docs. - 6.3 Create
src/async/async-command.test.ts— tests for each factory + compose order + error wrapping. Use{ timeout: 30000 }on asyncitblocks. - 6.4 Create
src/async/async-stack.ts—AsyncUndoStack<T>. Constructor:constructor(initial: T, options?: UndoStackOptions, bus?: EventBusLike). Asyncpush,undo,redowrap command failures inUndoErrorwithUNDO.COMMAND_FAILED.beginTransactionreturnsAsyncTransactionHandle<T>.clear,reset,snapshot, getters mirrorUndoStack. ThrowUndoErrorwithUNDO.TRANSACTION_ACTIVE/UNDO.TRANSACTION_INACTIVEon guard violations. Validate options withUndoStackOptionsSchema. Emitbusevents on push/undo/redo/clear when provided. - 6.5 OTel:
undo.push.total,undo.undo.total,undo.redo.totalwithstack_type: 'async';undo.stack.depthhistogram. - 6.6 Create
src/async/async-stack.mdco-located docs. - 6.7 Create
src/async/async-stack.test.ts— async push/undo/redo round-trip,AbortSignalcancellation (pass signal intoexecute/undofunctions 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>andcompressHistoryfunction. Constructor validates options withHistoryCompressorOptionsSchema.compress(stack)groups old entries by label, squashes viacomposeCommands, respectsmaxHistorySize, rebuilds the stack by replaying from initial state. ReturnsCompressionResult & { stack: UndoStack<T> }. - 7.2 OTel: increment
undo.compress.total; recordundo.compress.ratiohistogram (originalCount / compressedCount). - 7.3 Create
src/compression/history-compressor.mdco-located docs. - 7.4 Create
src/compression/history-compressor.test.ts— tests: no-op when within depth, squashes old entries, respectsmaxHistorySize, rebuild preserves current state,CompressionResultshape. 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>extendingUndoStack<T>. Constructor:constructor(initial: T, options?: SelectiveUndoOptions, bus?: EventBusLike).undoById(id)detects conflicts viadependencyCheck, throwsUndoErrorwithUNDO.DEPENDENCY_CONFLICT(with{ conflictIds }context) whenoptions.throwOnConflictis set, otherwise returnsSelectiveUndoResultwithsuccess: false.getDependents(id),canUndoById(id)remain non-throwing. - 8.2 Validate
optionswithSelectiveUndoOptionsSchema. - 8.3 OTel:
undo.undo.totalwithstack_type: 'selective'whenundoByIdsucceeds. - 8.4 Create
src/selective/selective-undo.mdco-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 returnssuccess: false,throwOnConflictpath.{ timeout: 5000 }on eachit.
9. CommandMerger
- 9.1 Create
src/merge/command-merger.ts—CommandMerger<T>. Constructor validatesoptionswithCommandMergerOptionsSchema.add(command)returns a flushed command when the time or count window closes, otherwiseundefined.flush(),reset(),canMerge(a, b). Getters:hasPending,pendingCount. Default predicate merges commands with identicallabel. - 9.2 Use injectable
Clock(from@suaveplan/types) for time-window calculation so tests can control elapsed time without realDate.now(). - 9.3 Create
src/merge/command-merger.mdco-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 returnsundefined,resetdiscards pending,hasPending/pendingCountgetters, deterministic with injected clock. Property test: merging[a, b, c]into a single command and executing it produces the same state as executinga,b,cindividually.
10. OTel instrumentation (cross-cutting review)
- 10.1 Confirm all six counters and two histograms are wired to the
undometer in each module (stack,branch,async,selective,compression). - 10.2 Verify all meters are no-ops when no exporter is registered (use
@opentelemetry/apidefault no-op SDK in tests — no real exporter needed). - 10.3 Add integration tests in
tests/otel.test.tsthat wire a@opentelemetry/apiin-memory exporter and assert counter increments forpush,undo,redo, andcompressoperations 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;undoSizenever exceedsmaxEntries. - 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:compressHistoryidempotency; compressed state equals original current state. - 11.5 Create
tests/properties/merge.prop.test.ts— fast-check:CommandMergermerge 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 / redowith 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:compressHistoryon a 1 000-entry stack withcompressionDepth: 100. - 12.4 Publish baseline throughput numbers in
README.mdBenchmarks section. Add CI regression comment (>10% degradation fails the gate).
13. Documentation
- 13.1 Ensure every implementation file has a sibling
.mdwith ≥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.mdat package root: one-paragraph description, install snippet, quick-start withUndoStackandBranchingHistory, Modules section linking each co-located.md, Benchmarks section with baseline numbers. No category-levelREADME.mdinsidesrc/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; noany,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 inpackage.jsonverified (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-
.mdscan: every.tsimplementation file (excluding.test.ts,.bench.ts, barrelindex.ts,constants/index.ts) has a sibling.md→ zero missing. - 14.8 Tree-shake test: importing only
./stack/stackdoes not pull inBranchingHistoryorHistoryCompressor— 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.mdenterprise column for@suaveplan/undoto ✅.