Repository navigation
Description
Where this was measured. Branch vps2f-2357-u1-writer, head 20182be34777812b2aaa8c0b21d47536d926dae1 (PR #2385), file src/services/file-safety/safeWriteText.ts, 1,236 lines at that head. Every line number below is read from that tree.
The behaviour. Step 2b (:859-904) re-validates the ancestry the caller walked before anything is committed. For each recorded ancestor it stats with bigint and maps ENOENT to AncestorReplacedError "no longer exists" at :868-869, refuses a dev or ino change at :873-874, and requires the authorized name to resolve to the authorized location at :886-902. Because the preflight at :367-382 skips a recorded ancestor that is merely absent (only ENOENT is skipped, :373-377, since an absent ancestor is a directory this call is about to create), the case where fs.mkdir at :637 recreates that name during this very write reaches step 2b as a recreated directory with a different identity, and is refused at :873-874 rather than accepted.
What a refusal leaves behind, read off the cleanup path. With the commit never taken (stagingCommitted false at :1208) the staged file is removed at :1210, the staging directory this write created is removed at :1216-1221 (empty, after its file is gone), and the DACL dump is removed at :1223-1225. The ancestor directories this write created are not removed: _createdAncestorChain (:396-422) exists so step 4b can fsync the chain, and the only rmdir calls in the file are the per-write staging directory (:1160, :1220). The reason written at :429-433 for the staging case applies to ancestors too: with a shared name, one write best-effort rmdir can delete a directory another write created but has not yet opened. So the safe direction is refusal, and the cost is empty directories left in place.
The open question. Should an ancestor that this write own mkdir recreated under a recorded name be accepted when it is equivalent, rather than refused? Accepting means proving the directory nobody else populated is still empty or still ours, which is new code with new failure modes. The refusal is conservative and already names the directory in the error, so a user can act on it.
What is not claimed. No severity and no measured incident. This describes a design question about a refusal path, not a broken write.
Where it relates. #2388 carries the ordering work around the same window (creation and staging happen before step 2b runs, which the reviewer accepted as true in row 4524187283). Neither issue states the recreated-ancestor question.
Price status. Unpriced. Any fix changes executable lines. #2385 is frozen at 526 counted executable lines against the gate cap of 500 (job 129791888819, log line 1884 at 2026-11-10T11:09:22.585Z) pending a maintainer decision on a narrow exclusion or a split, so this travels with the follow-up branch that carries it, priced by the CI-leg method (git merge-tree of the branch head with main, then scripts/stryker-diff.mjs selectFromGit against the resulting synthetic merge commit), not onto the frozen PR.
Found while answering CodeRabbit threads on #2385: reviewer rows 4524159747 and 4524187283 raised it, author answers 4524183148 and 4524228052 carry the line-level evidence. Filed on the same ruling as the reporting-channel finding.
Activity