Files
fusion/scripts/check-prerebase-inert.mjs
gsxdsm 3d6a908b95 FN-8898: document inert prerebase settings
Clarify that legacy prerebase settings are inert on the production merge path.

- Mark retained prerebase configuration and audit events as legacy-only.
- Add a static validator and tests preventing new prerebase callers.
- Update merge architecture, testing, and settings documentation.

Files changed:
 AGENTS.md                                          |   2 +-
 docs/architecture.md                               |   3 +-
 docs/settings-reference.md                         |   6 +-
 docs/testing.md                                    |   2 +-
 package.json                                       |   6 +-
 packages/core/src/types/settings/settings-scope.ts |  32 +++--
 .../src/errors/transient-merge-error-classifier.ts |  12 +-
 packages/engine/src/merge/merger-auto-prerebase.ts |  12 +-
 packages/engine/src/util/run-audit.ts              |   2 +
 scripts/__tests__/check-prerebase-inert.test.mjs   |  73 +++++++++++
 scripts/__tests__/run-static-gate-checks.test.mjs  |   1 +
 scripts/__tests__/verify-fast.test.mjs             |   1 +
 scripts/check-prerebase-inert.mjs                  | 146 +++++++++++++++++++++
 scripts/lib/source-projection.mjs                  |  87 ++++++++++++
 14 files changed, 359 insertions(+), 26 deletions(-)

Fusion-Task-Id: FN-8898

Fusion-Task-Lineage: 9cfd836d-17c2-44a0-a076-56fef0917935

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-09 06:01:02 -07:00

147 lines
9.7 KiB
JavaScript

#!/usr/bin/env node
/*
FNXC:MergerUnification 2026-08-09-12:20:
Master-plan U0 makes runAiMerge the only production merge path. This tracked-file
validator protects the retained-but-inert prerebase surface from a new live caller.
Its exact self-exclusion is necessary because its regex literals are executable code;
no other script is excluded.
*/
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { fileURLToPath, URL } from "node:url";
import { resolve } from "node:path";
import { maskSource } from "./lib/source-projection.mjs";
export const repoRoot = fileURLToPath(new URL("..", import.meta.url));
export const SELF_EXCLUDED_PATHS = Object.freeze(["scripts/check-prerebase-inert.mjs", "scripts/lib/source-projection.mjs"]);
export const EXEMPT_PATHS = Object.freeze(["packages/engine/src/merger.ts", "packages/engine/src/index.ts"]);
const EXCLUDED_SEGMENTS = new Set(["node_modules", "dist", ".git", ".fusion", ".worktrees", ".next", ".turbo", "coverage", "screenshots", "docs", "__tests__", "__mocks__", "__fixtures__", "__snapshots__", "fixtures", "e2e"]);
const EXTENSION = /\.(?:ts|tsx|mts|cts|js|mjs|cjs)$/;
const EXCLUDED_FILE = /(?:\.(?:test|spec)\.[^.]+$|\.d\.ts$|(?:^|\.)vitest(?:\.|$)|(?:^|\.)[^/]*config\.[^.]+$)/;
const PREBASE_IDS = /\b(?:decideAutoPrerebase|probeDivergence|runAutoPrerebase)\b/;
const LEGACY = /\baiMergeTask\b/;
const SPECIFIER = String.raw`(?:[^"'\\]|\\.)*merger-auto-prerebase(?:\.js|\.ts)?`;
const MODULE_BINDING = new RegExp(String.raw`(?:\bimport\s+(?:[^"';]*?\s+from\s*)?|\bexport\s+(?:[^"';]*?\s+from\s*)?|\bimport\s*\(|\brequire\s*\()\s*(["'])${SPECIFIER}\1`, "g");
const QUOTED_ACCESS = /(?:\[\s*|\b(?:Reflect\.get|Object\.getOwnPropertyDescriptor)\s*\([^,]+,\s*)(["'])aiMergeTask\1\s*(?:\]|\))/g;
export function isCorpusPath(path) {
const segments = path.split("/");
if (SELF_EXCLUDED_PATHS.includes(path) || segments.some((segment) => EXCLUDED_SEGMENTS.has(segment))) return false;
const name = segments.at(-1) ?? "";
return EXTENSION.test(name) && !EXCLUDED_FILE.test(name);
}
export function partitionCorpus(paths) {
return { exempt: paths.filter((path) => EXEMPT_PATHS.includes(path)), general: paths.filter((path) => !EXEMPT_PATHS.includes(path)) };
}
export function detectLegacyBindings(code) {
const rules = [];
if (/\b(?:import|export)\s*\{[^}]*\baiMergeTask\b[^}]*\}/s.test(code)) rules.push("aiMergeTask binding");
if (/\{[^}]*\baiMergeTask\b(?:\s*:\s*[A-Za-z_$][\w$]*)?[^}]*\}\s*=/s.test(code)) rules.push("aiMergeTask destructuring");
if (/(?:\.|\?\.)\s*aiMergeTask\b/.test(code)) rules.push("aiMergeTask member access");
if (/\baiMergeTask\s*(?:<[^;<>()]*>)?\s*\(/s.test(code)) rules.push("aiMergeTask call");
if (LEGACY.test(code)) rules.push("aiMergeTask binding");
return [...new Set(rules)];
}
export function detectQuotedAccess(specifierProjection) {
const codeProjection = maskSource(specifierProjection, { blankStrings: true });
QUOTED_ACCESS.lastIndex = 0;
// FNXC:MergerUnification 2026-08-09-12:20: Specifier projection retains
// strings, so also require the access opener at this offset to survive in
// code. This keeps scanner data strings from masquerading as an access.
for (let match; (match = QUOTED_ACCESS.exec(specifierProjection));) {
const codeAtMatch = codeProjection.slice(match.index);
if (/^(?:\[|Reflect\.get\b|Object\.getOwnPropertyDescriptor\b)/.test(codeAtMatch)) return ["quoted aiMergeTask identifier"];
}
return [];
}
export function detectsPrerebaseSpecifier(specifierProjection) {
const codeProjection = maskSource(specifierProjection, { blankStrings: true });
MODULE_BINDING.lastIndex = 0;
// The retained-string projection alone cannot distinguish an import-shaped
// data string from real syntax. At the same offset, the binding keyword must
// survive the code-only projection.
for (let match; (match = MODULE_BINDING.exec(specifierProjection));) {
if (/^(?:import|export|require)\b/.test(codeProjection.slice(match.index))) return true;
}
return false;
}
function occurrences(code) { return [...code.matchAll(/\baiMergeTask\b/g)]; }
export function checkExemption(path, code, specifier, source = code) {
// FNXC:MergerUnification 2026-08-09-12:20: The historical merger source has
// regex syntax predating the lightweight projection. Count comment-free code
// occurrences here rather than granting either exempt file a whole-file pass.
const commentFree = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match) => match.replace(/[^\n]/g, " "));
const found = occurrences(commentFree);
if (path === "packages/engine/src/merger.ts") {
if (found.length !== 1) return `${path}: per-occurrence exemption expected exactly one code occurrence`;
const at = found[0].index;
return /export\s+(?:async\s+)?function\s+aiMergeTask\s*(?:<[^>]*>)?\s*\(/.test(commentFree.slice(Math.max(0, at - 40), at + 160))
? null : `${path}: per-occurrence exemption expected declaration-shaped occurrence on code-only projection`;
}
if (path === "packages/engine/src/index.ts") {
if (found.length !== 1) return `${path}: per-occurrence exemption expected exactly one code occurrence`;
const at = found[0].index;
const window = specifier.slice(Math.max(0, at - 80), at + 240);
return /export\s*\{[\s\S]*\baiMergeTask\b[\s\S]*\}\s*from\s*["'][^"']*\/merger\.js["']/.test(window)
? null : `${path}: per-occurrence exemption re-export shape mismatch (specifier-preserving projection)`;
}
return `${path}: unknown exemption path`;
}
export function listAllTrackedFiles(root = repoRoot) {
const result = spawnSync("git", ["ls-files"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
if (result.status !== 0) throw new Error(result.stderr?.trim() || "git ls-files failed");
return result.stdout.split("\n").filter(Boolean);
}
export function listTrackedFiles(root = repoRoot) { return listAllTrackedFiles(root).filter(isCorpusPath); }
/** Compute the required source-root set difference; generic segments must not hide source. */
export function protectedCorpusHoles(allPaths = listAllTrackedFiles(), corpus = listTrackedFiles()) {
const protectedPath = /^(?:packages\/[^/]+\/src\/|packages\/dashboard\/app\/|plugins\/[^/]+\/src\/)/;
const scanned = new Set(corpus);
return allPaths.filter((path) => protectedPath.test(path) && isCorpusPath(path) && !scanned.has(path));
}
export function checkDocs(root = repoRoot) {
const content = readFileSync(resolve(root, "docs/settings-reference.md"), "utf8");
return ["prerebaseAutoEnabled", "prerebaseHotFiles", "prerebaseDivergenceThreshold"].flatMap((setting) => {
const row = content.split("\n").find((line) => line.startsWith(`| \`${setting}\` |`));
if (!row) return [`docs contract: missing ${setting} row`];
const failures = [];
if (row.includes("Stage 1/2")) failures.push("stale Stage 1/2 wording");
if (!row.includes("**Legacy and inert (master-plan U0)**")) failures.push("missing inert marker");
if (!row.includes("runAiMerge")) failures.push("missing runAiMerge");
return failures.map((failure) => `docs contract: ${setting}: ${failure}`);
});
}
export function scanSources(paths = listTrackedFiles(), root = repoRoot) {
const failures = [], identifierPaths = new Set(), specifierPaths = new Set();
const holes = protectedCorpusHoles(undefined, paths);
if (holes.length) failures.push(`corpus no-hole: excluded protected sources ${holes.join(", ")}`);
const { exempt, general } = partitionCorpus(paths);
if (exempt.length !== EXEMPT_PATHS.length || new Set([...exempt, ...general]).size !== paths.length) failures.push("layer routing: exempt/general corpus partition mismatch");
for (const path of paths) {
const source = readFileSync(resolve(root, path), "utf8");
const hasToken = /decideAutoPrerebase|probeDivergence|runAutoPrerebase|aiMergeTask|merger-auto-prerebase/.test(source);
const mustCheckExemption = EXEMPT_PATHS.includes(path);
if (!hasToken && !mustCheckExemption) continue;
const code = maskSource(source, { blankStrings: true });
const specifier = maskSource(source, { blankStrings: false });
if (PREBASE_IDS.test(code)) identifierPaths.add(path);
if (detectsPrerebaseSpecifier(specifier)) specifierPaths.add(path);
if (mustCheckExemption) {
const failure = checkExemption(path, code, specifier, source); if (failure) failures.push(failure);
} else {
for (const rule of detectLegacyBindings(code)) failures.push(`${path}: ${rule}`);
for (const rule of detectQuotedAccess(specifier)) failures.push(`${path}: ${rule}`);
}
}
const expectedIds = ["packages/engine/src/merge/merger-auto-prerebase.ts", "packages/engine/src/merger.ts"];
const expectedSpecifiers = ["packages/engine/src/merger.ts"];
if (JSON.stringify([...identifierPaths].sort()) !== JSON.stringify(expectedIds)) failures.push(`identifier reachability: expected ${expectedIds.join(", ")}, got ${[...identifierPaths].sort().join(", ")}`);
if (JSON.stringify([...specifierPaths].sort()) !== JSON.stringify(expectedSpecifiers)) failures.push(`module specifier: expected ${expectedSpecifiers.join(", ")}, got ${[...specifierPaths].sort().join(", ")}`);
return failures;
}
export function formatFailureMessage(matches) {
return ["[check-prerebase-inert] retained legacy prerebase contract violated.", "Re-wiring prerebase or aiMergeTask by identifier, import, alias, or package path requires updating docs/settings-reference.md, AGENTS.md item 10, and docs/architecture.md.", ...matches].join("\n");
}
export function main() { const failures = [...checkDocs(), ...scanSources()]; if (!failures.length) return 0; console.error(formatFailureMessage(failures)); return 1; }
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exitCode = main();