Files
fusion/scripts/check-file-claimed.mjs
gsxdsm 9c9dd7db2f FN-8706: scan paginated PR files for claims
Scan paginated pull-request file lists so claim checks remain reliable for large diffs.

- Replace truncated PR diff scans with count-validated paginated GitHub API requests.
- Fail closed when open PR or file-list data is incomplete, malformed, or exceeds the API ceiling.
- Add coverage for large diffs, API failures, count mismatches, and claim precedence.

Files changed:
 scripts/__tests__/check-file-claimed.test.mjs | 186 ++++++++++++++++++++++++++
 scripts/check-file-claimed.mjs                | 119 +++++++++++-----
 2 files changed, 272 insertions(+), 33 deletions(-)

Fusion-Task-Id: FN-8706
Fusion-Task-Lineage: 1edf3546-553b-4e35-8c41-cd5143c08e0a
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 09:39:38 -07:00

147 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/*
FNXC:FleetCoordination 2026-07-31-06:10 (fleet):
WHY THIS EXISTS. Every fleet worker pushes as the SAME GitHub account, so `gh pr list --author "@me"`
returns all 17 open PRs and no worker can tell their own from a teammate's. There is no way to ask "is
this file already being converted?" short of fetching every open PR's file list by hand — 25+ API calls
that a worker will not make before starting, and I did not make either.
MEASURED COST, not a hypothetical: four of my PRs were superseded by teammates landing the same work
first (#3096, #3116, #3140 shrank to tests; #3125 to nothing and was closed). In every case both
implementations were correct and independently reached the same design. The fleet is not making
mistakes — it is doing correct work twice, and finding coverage gaps only by accident when the rebases
collide.
WHAT THIS DOES. One command, one answer:
node scripts/check-file-claimed.mjs packages/engine/src/self-healing.ts
It prints the open PRs touching that path, so "claimed?" is answerable before the work starts rather
than at rebase time.
WHAT IT DOES NOT DO, deliberately. It cannot see work that is in progress and unpushed, so it narrows
the window rather than closing it. Closing it needs distinguishable authorship — a per-worker
`Co-Authored-By` or a title prefix — which is a coordination decision, not a script. This is the part
that can be fixed from inside the repo.
*/
import { execFileSync } from "node:child_process";
const targets = process.argv.slice(2).filter((a) => !a.startsWith("-"));
if (targets.length === 0) {
console.error("usage: node scripts/check-file-claimed.mjs <path> [<path>...]");
console.error(" paths are matched as substrings of each PR's changed-file list");
process.exit(2);
}
const PR_LIMIT = 300;
const PR_FILE_PAGE_SIZE = 100;
const PR_FILE_API_CEILING = 3000;
function gh(args) {
return execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
}
function unknown(message) {
console.error(`claim-check: ${message}`);
console.error("claim-check: cannot prove a file is unclaimed from incomplete data. Treat as UNKNOWN, not free.");
for (const target of targets) console.log(`UNKNOWN ${target}`);
process.exit(2);
}
function readOpenPullRequests() {
let output;
try {
output = gh(["pr", "list", "--state", "open", "--limit", String(PR_LIMIT), "--json", "number,title,changedFiles"]);
} catch (error) {
unknown(`gh failed while listing open PRs — ${error?.message ?? error}`);
}
try {
const open = JSON.parse(output);
if (!Array.isArray(open)) throw new TypeError("expected an array");
if (open.length >= PR_LIMIT) unknown(`${open.length} open PRs hit the --limit ${PR_LIMIT} cap, so the list may be truncated.`);
return open;
} catch (error) {
unknown(`open PR list was malformed — ${error?.message ?? error}`);
}
}
/*
FNXC:FleetClaims 2026-08-01-16:09:
GitHub refuses PR diffs over 300 files, but its pull-request-files API exposes a paginated list up to
its documented 3,000-file ceiling. Reconcile every validated filename record with the authoritative
`changedFiles` count before using a PR as claim evidence; missing, malformed, failed, mismatched, or
above-ceiling data stays UNKNOWN. Evaluate every PR before deciding so incomplete data always outranks
a known claim and workers never treat a partial scan as permission to overlap.
*/
function filesForPullRequest(pr) {
if (!Number.isInteger(pr?.number) || !Number.isInteger(pr?.changedFiles) || pr.changedFiles < 0) {
return { complete: false, reason: `PR #${pr?.number ?? "unknown"} has no valid changed-file count` };
}
if (pr.changedFiles > PR_FILE_API_CEILING) {
return { complete: false, reason: `PR #${pr.number} changes ${pr.changedFiles} files, above the ${PR_FILE_API_CEILING}-file API ceiling` };
}
const files = [];
const pageCount = Math.ceil(pr.changedFiles / PR_FILE_PAGE_SIZE);
for (let page = 1; page <= pageCount; page += 1) {
let output;
try {
output = gh([
"api",
`repos/{owner}/{repo}/pulls/${pr.number}/files?per_page=${PR_FILE_PAGE_SIZE}&page=${page}`,
]);
} catch (error) {
return { complete: false, reason: `PR #${pr.number} files API failed on page ${page} — ${error?.message ?? error}` };
}
try {
const records = JSON.parse(output);
if (!Array.isArray(records) || records.some((record) => typeof record?.filename !== "string" || record.filename.length === 0)) {
throw new TypeError("expected an array of filename records");
}
files.push(...records.map((record) => record.filename));
} catch (error) {
return { complete: false, reason: `PR #${pr.number} files API returned malformed page ${page} — ${error?.message ?? error}` };
}
}
if (files.length !== pr.changedFiles) {
return { complete: false, reason: `PR #${pr.number} returned ${files.length} files but reports ${pr.changedFiles}` };
}
return { complete: true, files };
}
const open = readOpenPullRequests();
const hits = new Map(targets.map((target) => [target, []]));
const incomplete = [];
for (const pr of open) {
const result = filesForPullRequest(pr);
if (!result.complete) {
incomplete.push(result.reason);
continue;
}
for (const target of targets) {
if (result.files.some((file) => file.includes(target))) hits.get(target).push(pr);
}
}
if (incomplete.length > 0) unknown(incomplete.join("; "));
let claimed = false;
for (const target of targets) {
const prs = hits.get(target);
if (prs.length === 0) {
console.log(`UNCLAIMED ${target}`);
continue;
}
claimed = true;
console.log(`CLAIMED ${target}`);
for (const pr of prs) console.log(` #${pr.number} ${pr.title}`);
}
/* Exit 1 when anything is claimed, so a worker can gate on it: `... && start-work`. */
process.exit(claimed ? 1 : 0);