refactor(file-safety): delegate safeWriteJson to safeWriteText atomic publish (S3 v2-2, epic #1375) - #45
Open
easonLiangWorldedtech wants to merge 1 commit into
Conversation
Review statusThanks 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. |
… publish (S3 v2-2, epic Zoo-Code-Org#1375)
easonLiangWorldedtech
force-pushed
the
feat/fws-v2-2-safewritejson
branch
from
September 6, 2026 00:59
2ecc5d4 to
d183e29
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
v2-2 of the S3 file-safety rebuild (epic #1375): rewrite
safeWriteJsonto delegate its backup/commit/rollback dance to the atomic text publish primitive (safeWriteText, v2-1 / #43).The staging/backup/rollback logic that was inlined in
safeWriteJson(temp write → backup rename → commit rename → rollback) is replaced by one call:contentis intentionally not passed — the staged temp file is the content source (thetempPathcontract of the primitive).Why
safeWriteJson(used by task persistence, MCP settings, model caches, import/export, …) no longer carries a second, hand-rolled copy of the crash-safety dance.safeWriteJsonkeeps its own loudconsole.errorat the outer boundary (loud-failing, per the epic).json-stream-stringifyinto the temp file (large task-history payloads stay out of memory); the staged temp is created beside the resolved target so the commit rename is same-volume (EXDEV-safe).Scope
src/utils/safeWriteJson.ts(252 → 182 lines): public API unchanged —safeWriteJson(filePath, data, { prettyPrint?, merge? }),SafeWriteJsonOptions,LOCK_STALE_MS. Kept as-is: theproper-lockfileadvisory lock (same retry/staleness settings), the merge-under-lock read-modify-write, the directory create+access preamble, the loud outer catch. Replaced: the inline temp/backup/commit/rollback block and its catch-block rollback/cleanup. New:resolvePublishTargetfor symlink resolution, deterministic staged-temp name (. <name>.new_<ts>_<rand>.tmpbeside the resolved target), and explicit unlink of a partial temp when the stream itself fails.src/utils/__tests__/safeWriteJson.test.ts(545 → 597 lines): 21 tests retained; 2 adapted to the delegated semantics (backup-cleanup failure is now silent — the write still succeeds; rollback failure now leaves the original content orphaned under the generated backup name and the target path gone); 2 new tests pin the delegation contract: one pinsDate.now/Math.randomand asserts the exact staged temp path; the other spies thesafeWriteTextboundary (the spy delegates to the real implementation) and asserts the exact call — resolved target path, the empty content string (dead whentempPathis supplied — the staged file is the content source, and the pin is what keeps that argument covered), the stagedtempPath, andbackup: true.src/eslint-suppressions.json: allowedno-explicit-anycounts reduced to match the rewrite (4 → 3 insafeWriteJson.ts, 27 → 26 in the spec) — twoanyannotations were removed by the refactor; counts never increase.Gates (local, pre-push)
utils/__tests__/safeWriteJson.test.ts+services/file-safety/__tests__/safeWriteText.spec.ts→ 54/54; all 17safeWriteJsonconsumer suites (delegation-concurrent, importExport, FileContextTracker, TaskHistoryStore ×3, taskMessages, flushPendingToolResultsToHistory, generateImageTool, cache-manager, McpHub, ClineProvider ×6) → 465/465.--prune-suppressions --max-warnings=0on the touched files: clean; fullpnpm lint11/11 packages (suppression counts synced above).pnpm check-types: 11/11.scripts/stryker-diff.mjs ci, base29110ed66= v2-1 head, head2ecc5d446): 6 changed-line mutants, 0 Survived / 0 NoCoverage (StringLiteral 2, MethodExpression 1, OptionalChaining 1, ObjectLiteral 1, BooleanLiteral 1 — all killed). The one survivor from the pre-pin run (the empty""content literal — equivalent by thetempPathcontract) is now killed by the boundary pin test.Line budget
Standalone diff vs the PR base (v2-1 head
29110ed66): 215 lines (99+/116- across 3 files) — merge-tree measured. Comfortably under the 400-line soft design target.Stacking / merge order
This PR is based on
feat/fws-v2-1-atomic-publish-core(#43) because it importssafeWriteTextfrom it. Merge #43 first, then this branch rebases cleanly ontomainand the PR base switches tomain— the diff is identical either way.