fix(proof): report proof sources unavailable when the missions root's real location leaves the project (#1813)

Found in review of 081c4d24. A missions root configured through a link (accepted by the legacy slices-root route) can resolve outside the project, where the watcher has no structural anchor, yet coverage was certified and the sources reported watched. Coverage is now certified only while the real location the recursive watch was attached at is known and inside the project, checked at construction and on every reconciliation, so a link re-pointed outside also turns the observation unavailable and notifies clients. Nothing outside the project is subscribed to; readers, project authority and publication are unchanged.
This commit is contained in:
dev-engineer committed 2026-10-24 23:59:50 +00:00
1 parent 081c4d24e1
commit 424337a78b
2 files changed
+34 -6

No files matched your search

@@ -109,13 +109,14 @@ export function watchProofSources(missionsRoot: string, invalidate: () => void,
let wanted: Shallow = new Map();
const shallow = new Map<string, fs.FSWatcher>();
let recursive: fs.FSWatcher | undefined;
// The real location the recursive watch was attached at (it follows a linked root to that directory).
let recursiveReal = missionsRoot;
// The real location the recursive watch was attached at (it follows a linked root to that directory); null if
// it could not be established.
let recursiveReal: string | null = null;
const notify = (next: string) => { try { bus.emit({ type: "proof.sources_changed", scope: missionsRoot, revision: next }); } catch { /* Quiet refresh remains the repair path. */ } };
/** A directory at or below `changed` may have been replaced: its watches are dropped, to be reattached by path. */
const stale = (changed: string) => {
for (const [dir, watcher] of shallow) if (within(dir, changed)) { watcher.close(); shallow.delete(dir); }
if (recursive && (within(missionsRoot, changed) || within(recursiveReal, changed))) { recursive.close(); recursive = undefined; }
if (recursive && (within(missionsRoot, changed) || (recursiveReal !== null && within(recursiveReal, changed)))) { recursive.close(); recursive = undefined; }
};
const onEvent = (dir: string | null) => (_event: string, filename: string | Buffer | null) => {
if (closed) return;
@@ -144,7 +145,7 @@ export function watchProofSources(missionsRoot: string, invalidate: () => void,
* longer needed (new coverage first). Returns whether anything was attached. */
const resubscribe = (next: Shallow): boolean => {
let added = false;
const real = canonical(missionsRoot) ?? missionsRoot;
const real = canonical(missionsRoot);
if (recursive && real !== recursiveReal) { recursive.close(); recursive = undefined; }
if (!recursive) { recursive = watchDir(missionsRoot, true); recursiveReal = real; added = true; }
for (const dir of next.keys()) {
@@ -162,7 +163,8 @@ export function watchProofSources(missionsRoot: string, invalidate: () => void,
const evaluate = (): string => {
let read = basis();
for (let i = 0; ; i++) {
if (!resubscribe(read.watches)) { covered = true; return read.hash; }
// A root whose real location is unknown or outside the project has no anchors there: never certified.
if (!resubscribe(read.watches)) { covered = recursiveReal !== null && within(recursiveReal, project); return read.hash; }
if (i === 2) { covered = false; return read.hash; }
read = basis();
}
@@ -195,7 +197,7 @@ export function watchProofSources(missionsRoot: string, invalidate: () => void,
timer.unref();
};
try {
recursiveReal = canonical(missionsRoot) ?? missionsRoot;
recursiveReal = canonical(missionsRoot);
recursive = watchDir(missionsRoot, true);
try { revision = evaluate(); readable = true; } catch (e) {
if (e instanceof WatchFailure) throw e;
@@ -357,6 +357,32 @@ describe("QA bounded adverse coverage at 8b236e06", () => {
expect(opened.some((h) => !h.root.startsWith(f.root) && h.root !== path.dirname(f.root))).toBe(false);
} finally { watch.close(); }
});
for (const start of ["outside", "inside"] as const) it(`reports unavailable while a linked missions root's real location is outside the project (starts ${start})`, () => {
const f = fixture(), linked = path.join(f.root, "linked-missions"), inside = path.join(f.root, "real", "missions");
const other = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "policy-reuse-other-"))); roots.push(other);
const outside = path.join(other, "missions");
// Another project's missions tree, readable on its own.
fs.cpSync(f.missions, outside, { recursive: true }); fs.copyFileSync(f.projectFile, path.join(other, "project.yaml")); fs.mkdirSync(path.dirname(inside)); fs.renameSync(f.missions, inside);
fs.symlinkSync(start === "outside" ? outside : inside, linked);
vi.useFakeTimers();
const opened: Array<{ root: string; event: (event: string, name: string | null) => void }> = [];
vi.spyOn(fs, "watch").mockImplementation(((root: string, _options: unknown, listener: (event: string, name: string | null) => void) => {
opened.push({ root, event: listener });
return { on: vi.fn(), close: vi.fn() };
}) as unknown as typeof fs.watch);
const emit = vi.fn(), watch = watchProofSources(linked, vi.fn(), { emit } as unknown as EventBus);
try {
if (start === "inside") {
expect(watch.observation().state).toBe("watching");
// The supported alias is re-pointed outside the project; the link's own parent sees it.
fs.unlinkSync(linked); fs.symlinkSync(outside, linked);
opened.filter((h) => h.root === f.root).at(-1)!.event("rename", "linked-missions"); vi.advanceTimersByTime(50);
expect(emit.mock.calls.map(([e]) => e.revision)).toContain("unavailable");
}
expect(watch.observation().state).toBe("unavailable");
expect(opened.some((h) => h.root.startsWith(other))).toBe(false);
} finally { watch.close(); }
});
it("QA observes replacement of an external evidence parent or honestly reports unavailable", () => {
const t = adverseFixture();
try {