Skip to content

feat(checkpoints): per-file/per-step rollback core: rollback.ts + changeJournal read-failure semantics (B3c v2-11, epic #1375) - #54

Open
easonLiangWorldedtech wants to merge 1 commit into
feat/fws-v2-10-shadow-restore-servicefrom
feat/fws-v2-11-rollback-core
Open

feat(checkpoints): per-file/per-step rollback core: rollback.ts + changeJournal read-failure semantics (B3c v2-11, epic #1375)#54
easonLiangWorldedtech wants to merge 1 commit into
feat/fws-v2-10-shadow-restore-servicefrom
feat/fws-v2-11-rollback-core

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Owner

What this PR does

The rollback core half of the per-file/per-step rollback feature (B3c, epic Zoo-Code-Org#1375), stacked on #53 (v2-10, the shadow restore service this PR consumes):

  • src/core/checkpoints/rollback.ts (new): the rollback API — rollbackFile(task, stepCheckpointId, filePath), rollbackStep(task, stepFiles, stepCheckpointId?), and restoreLatestFile(task, filePath) (the forward direction). "Rollback" = UNDO the change-card step: every file the step touched is restored to its PRE-step state, resolved from the B2 change journal (changes.jsonl) entries — an earlier step's entry checkpoint when one exists; the task-start baseline (service.baseHash) for the file's first change (undoing a create removes the file, undoing a delete restores it); multi-write steps (two entries sharing one checkpoint id) resolve through the first entry. A file written again by a later step is rejected (File was modified in a later step; roll back the latest change card first) instead of silently overwriting the newer state; restoreLatestFile on a file the task never wrote is a successful no-op. All failures are shaped into per-file outcomes (RollbackFileOutcome / RollbackStepOutcome), never thrown.
  • Restores delegate to the existing shadow-git service — getCheckpointService(task)restoreFile(target, filePath) from feat(checkpoints): shadow restoreFile service for per-file rollback (B3c v2-10, epic #1375) #53 — so only the named file's working-tree content is replaced; the shadow repo HEAD and the checkpoint list are untouched.
  • src/core/checkpoints/changeJournal.ts: loadChanges now treats only an ABSENT journal file as an empty history (ENOENT → []). Any other read failure (permissions, I/O, EISDIR) is rethrown — a journal that cannot be read must not be indistinguishable from one that is legitimately empty, or a rollback could report a no-op success without ever reading the history.
  • __tests__/rollback.spec.ts (new): 26 tests across the three rollbackFile / rollbackStep / restoreLatestFile describes (resolution rules, multi-write, latest-only rejection, per-file failure isolation, journal-unavailable vs journal-unreadable, non-Error rejection stringification, exact diagnostic-message pinning) using a real journal on disk (appendChange) with the checkpoint service double; __tests__/changeJournal.spec.ts gains the EISDIR read-failure propagation test and a nullish-rejection rethrow test.

Line count / split rationale

794 changed lines (789+/5-) across 5 files — above the 400 soft design target, under the 1000 hard cap; rationale: rollback.ts + its full spec + the changeJournal ENOENT-only delta are one unit — the plan's Stryker invariant requires every killing test for this PR's diff lines to live in the same PR, and the plan itself marks the rollback spec split as the only non-file-boundary cut (it was evaluated and kept whole here: the core/restore describes are the killing tests for rollback.ts, which is this PR's only new source file). The stryker.config.mjs delta (10 lines) is the gate configuration needed for the module-scope constants documented under Provenance.

Plan note: the approved v2 plan placed the rollback core at #v2-10 and the shadow service at #v2-11 (rows B12/B13). rollback.ts calls service.restoreFile(...) from #53 at compile time, so the service PR must land first; the two PRs' content is swapped relative to the plan rows (documented in #53's body), with the slugs renamed to match.

Provenance

  • rollback.ts is byte-identical to the B3c head 10fdd195b on the upstream rollback branch (blob 508789c9…).
  • rollback.spec.ts derives from the B3c spec with one v2-new mutation-testing addition: the service-error test now also pins the exact console.error diagnostic ([checkpointRollback] failed to restore … from checkpoint …: …) — the Stryker run at the original head showed the L141 message literal uncovered by assertions.
  • The changeJournal ENOENT-only delta is a v2-new tightening (15+/4-) required by the rollback consumers, with its regression tests (EISDIR propagation; nullish-rejection rethrow, which pins the optional-chaining in the ENOENT guard).
  • stryker.config.mjs gains coverageAnalysis: "perTest" + ignoreStatic: true (v2-new gate config, 10 lines): under Stryker 10's vitest runner the test environment is long-lived per run, so module-scope code (the six const error-message declarations in rollback.ts) executes once at first load and its mutants are unobservable at test time — they report static: true / coveredBy: [] and would always "survive". Stryker's ignoreStatic reports them as Ignored instead of Survived (six such mutants in this PR's diff); the values themselves stay covered by the exact pinning assertions in rollback.spec.ts.
  • The B3c slice's UI half (change cards webview + settings) arrives in #v2-12..#v2-15.

Not included here (later v2 PRs)

  • Change-cards UI core (ChangeCard component + spec + ChatRow + webview message handler wiring) — v2-12.
  • Restore-latest / open-in-editor / a11y UI — v2-13; CR round fixes — v2-14; i18n (17 locales) — v2-15.

Local gates

  • tsc --noEmit (src): clean.
  • Vitest (core/checkpoints): 6 files, 89 tests passed.
  • ESLint --prune-suppressions --max-warnings=0 on the touched spec files + full pnpm lint via the pre-commit hook: clean; src/eslint-suppressions.json counts unchanged (content-equal verified after the run).
  • Prettier: all 5 files content-clean under the repo config.
  • Stryker diff gate (base = feat(checkpoints): shadow restoreFile service for per-file rollback (B3c v2-10, epic #1375) #53 head 2a499d841, head 3e75647dc): 123 changed-code mutants, 123 Killed, 0 Survived, 0 NoCoverage, 0 timeout (117 changed lines; the 6 module-scope string-literal mutants of rollback.ts L50-55 reported Ignored-static via ignoreStatic, see Provenance).

…restore + changeJournal read-failure semantics (B3c v2-11, epic Zoo-Code-Org#1375)
@easonLiangWorldedtech easonLiangWorldedtech added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Required CI passed. Waiting for automated review of the latest commit.

If automated review does not start, a maintainer must restart it.

Review-state labels are managed by this workflow; do not edit them manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit coderabbit-review-active Required CI passed; CodeRabbit review is active

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants