Repository navigation
feat(tools): guarded write core under the shared lock (U5, #1375) #1914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
easonLiangWorldedtech
wants to merge
49
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:fws/u5-guard-core
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
83b4a96
split unit U1 of PR 1833 (issue 1375)
f9216d7
fix(file-safety): close the pre-merge findings on the publish primiti…
96655c7
fix(file-safety): propagate a non-ENOENT lstat failure in resolvePubl…
a7af8f1
fix(file-safety): keep the rollback pair typed and the mock stand-ins…
d853019
rebuild unit u2 on the fixed chain
49943f8
chore(lint): prune the safeWriteJson suppression this unit earns
3cb97b2
rebuild unit u3 on the fixed chain
3a14c48
fix(task): declare the observation registry on Task in this unit
d6bcf47
rebuild unit u4 on the fixed chain
ca084b1
rebuild unit u5 on the fixed chain
e739fb9
chore(ci): rerun the Windows test lane
16988d8
fix(tools): the clipping notice must agree with the recorded complete…
315c69a
test(file-safety): check the paths the cleanup assertions actually name
36cd1ab
test(file-safety): fix the readlink mock parameter type
ceafb44
fix(file-safety): inherit unit 1 committed guard
d3135a3
fix(file-safety): keep the publish error message in RollbackFailureError
ffb9d67
test: re-trigger required checks - the queued runs were cancelled by …
563a3c7
fix(file-safety): give the Windows DACL dump a per-write name
a8aadc8
test(file-safety): cover the committed guard in backup mode and align…
0f2cf5d
fix(tools): confine a guarded write to the task workspace
a7dc10b
test(tools): type the realpath double for the containment cases
709fbd8
fix(tools): fail closed on an unresolvable workspace and re-check con…
9bc3e64
fix(tools,fileLock): stop indenting the clipped view, canonicalize th…
1331d0d
fix(tools): stop a queued write from surviving a cancel-then-resume
17e7e59
fix(tools): make the guarded create an atomic no-replace publish
3db87ee
fix(tools): pin the publish to the target that was authorized
44db939
fix(file-safety): keep the create path working and the lock key stabl…
b28abb9
fix(guardedWrite): fail closed on an unresolvable workspace and pin t…
6835cde
fix(u5): drop the duplicated failIfExist key the assertion rewrite le…
f353b89
fix(file-safety): one lock key whether the parent directory exists yet
aa25af0
fix(u5): refuse a path that runs through a dangling link
d082d49
test(u5): the pin and the publish must agree on an aliased ancestor
67f79b6
fix(file-safety): take U1's copy-never-move backup into this unit
e2674db
fix(u5): align the unit with main's lint re-enable and format gate
easonLiangWorldedtech 6375a09
fix(u5): restore the unicode spec's clipped-lines expectations the au…
easonLiangWorldedtech b95efab
fix(file-safety): fsync the copied target after the no-replace copy f…
easonLiangWorldedtech 0c70f6d
test(utils): type the readlink mock access in the cycle test
easonLiangWorldedtech a75f05c
fix(file-safety): open the copied target with write access before fsync
easonLiangWorldedtech 8bb7a32
fix(file-safety): close the moved-ancestor bypass and clean partial c…
easonLiangWorldedtech e5e71aa
fix(file-safety): fail closed when the volume has no atomic no-replac…
easonLiangWorldedtech e3d66db
fix(file-safety): only a vanished ancestor name is a containment viol…
7d4a556
chore(knip): keep the guarded-write module out of the production dead…
88934a3
Merge remote-tracking branch 'upstream/main' into fws/u5-guard-core
easonLiangWorldedtech 8dd1df0
style: apply repo Prettier to the merge resolution
easonLiangWorldedtech 14ebcdd
fix(task): advance the cancellation generation in the run-state abort…
easonLiangWorldedtech 563b215
fix(file-safety): surface an unremovable pre-write backup and cover f…
easonLiangWorldedtech 3a2da1e
style: apply prettier formatting to the orphaned-backup regression tests
easonLiangWorldedtech 153a04c
fix(file-safety): refresh the observation after a post-commit durabil…
easonLiangWorldedtech 39065a5
fix(file-safety): let safeWriteJson treat an orphaned pre-write backu…
easonLiangWorldedtech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { describe, it, expect, vi } from "vitest" | ||
|
|
||
| import { ObservationRegistry } from "../observationRegistry" | ||
|
|
||
| describe("ObservationRegistry", () => { | ||
| it("observe → get returns the recorded version and observedAt", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "1:2:300:4000000000:5000000000") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts") | ||
| expect(obs).toBeDefined() | ||
| expect(obs!.version).toBe("1:2:300:4000000000:5000000000") | ||
| expect(typeof obs!.observedAt).toBe("number") | ||
| }) | ||
|
|
||
| it("re-observe replaces the entry with a fresh observedAt", () => { | ||
| vi.useFakeTimers() | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| const first = reg.get("/a/b/c.ts")! | ||
| expect(first.version).toBe("v1") | ||
|
|
||
| vi.advanceTimersByTime(50) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
| const second = reg.get("/a/b/c.ts")! | ||
| expect(second.version).toBe("v2") | ||
| expect(second.observedAt).toBeGreaterThan(first.observedAt) | ||
|
|
||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it("has returns true for observed paths, false otherwise", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/x.ts", "t1") | ||
| expect(reg.has("/x.ts")).toBe(true) | ||
| expect(reg.has("/y.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("size reflects the number of observed entries", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.size).toBe(0) | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| expect(reg.size).toBe(2) | ||
| }) | ||
|
|
||
| it("clear removes all entries and resets size to 0", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| reg.clear() | ||
| expect(reg.size).toBe(0) | ||
| expect(reg.get("/a.ts")).toBeUndefined() | ||
| expect(reg.has("/b.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("get on empty registry returns undefined", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.get("/any.ts")).toBeUndefined() | ||
| }) | ||
|
|
||
| it("separate instances are independent — observing in one does not appear in the other", () => { | ||
| const regA = new ObservationRegistry() | ||
| const regB = new ObservationRegistry() | ||
| regA.observe("/shared.ts", "v1") | ||
| expect(regA.get("/shared.ts")).toBeDefined() | ||
| expect(regB.get("/shared.ts")).toBeUndefined() | ||
| regB.observe("/shared.ts", "v2") | ||
| expect(regA.get("/shared.ts")!.version).toBe("v1") | ||
| expect(regB.get("/shared.ts")!.version).toBe("v2") | ||
| }) | ||
|
|
||
| describe("completeness scope (S4b follow-up #46)", () => { | ||
| it("defaults to a complete observation when the read scope is not given", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("records a partial observation when the read only returned a view of the file", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(false) | ||
| }) | ||
|
|
||
| it("re-observing replaces the entry's completeness with the new read's scope", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("re-observing with a partial scope downgrades a previously complete entry", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| reg.observe("/a/b/c.ts", "v2", false) | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /** | ||
| * Per-task file observation registry (upstream epic #1375, phase A2). | ||
| * | ||
| * Each Task owns its own instance so parent and subtask observations are | ||
| * independent. The S4 guarded-write will compare these versions against the | ||
| * token recomputed pre-write to detect stale reads or file replacement. | ||
| * | ||
| * Pure in-memory — zero I/O, no dependencies. The S4 guarded-write consults | ||
| * these observations for the version check and for the completeness check that | ||
| * gates a full-file replacement. | ||
| */ | ||
|
|
||
| export interface FileObservation { | ||
| /** Version token derived from on-disk fs.stat (bigint mode). */ | ||
| version: string | ||
| /** Millisecond timestamp when the observation was recorded. */ | ||
| observedAt: number | ||
| /** | ||
| * Whether the read that produced this observation returned the complete | ||
| * file. A slice, line-range, truncated, or indentation-block read returns | ||
| * only a view of the file; such an observation authorizes targeted edits | ||
| * on the view the model saw, but never a full-file replacement. | ||
| */ | ||
| complete: boolean | ||
| } | ||
|
|
||
| export class ObservationRegistry { | ||
| private readonly entries = new Map<string, FileObservation>() | ||
|
|
||
| /** | ||
| * Record an observation for a file at its absolute path. | ||
| * | ||
| * Re-observing replaces the entry with a fresh observedAt timestamp, the | ||
| * new version token, and the read's completeness. `complete` defaults to | ||
| * true for callers that read the whole file themselves (spec doubles, | ||
| * WriteToFileTool). A caller whose read is internal to a targeted edit must | ||
| * carry the model's prior completeness instead, so the tool's own read cannot | ||
| * upgrade a partial read into authority for a full-file replacement. | ||
| */ | ||
| observe(absolutePath: string, version: string, complete: boolean = true): void { | ||
| this.entries.set(absolutePath, { version, observedAt: Date.now(), complete }) | ||
| } | ||
|
|
||
| get(absolutePath: string): FileObservation | undefined { | ||
| return this.entries.get(absolutePath) | ||
| } | ||
|
|
||
| has(absolutePath: string): boolean { | ||
| return this.entries.has(absolutePath) | ||
| } | ||
|
|
||
| clear(): void { | ||
| this.entries.clear() | ||
| } | ||
|
|
||
| get size(): number { | ||
| return this.entries.size | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.