Files
fusion/scripts/lib/source-projection.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

88 lines
4.0 KiB
JavaScript

/*
FNXC:MergerUnification 2026-08-09-12:20:
FN-8898 needs offset-aligned code and specifier projections without changing the
existing live check-no-getdatabase validator. Blank spans rather than deleting
them so checks can validate a retained string-literal form at a code offset.
*/
/** Blank comments and optionally strings while preserving offsets and newlines. */
export function maskSource(source, { blankStrings }) {
const output = source.split("");
const blank = (start, end) => {
for (let index = start; index < end; index++) if (output[index] !== "\n" && output[index] !== "\r") output[index] = " ";
};
/**
* FNXC:MergerUnification 2026-08-09-12:36:
* FN-8898 scans executable regex literals as code because their patterns can
* contain the guarded identifiers. Skip their delimiters while lexing so a
* character class such as /[//]/ cannot masquerade as a line comment and
* hide the executable code that follows it.
*/
const skipRegexLiteral = (start, end) => {
let inClass = false;
for (let cursor = start + 1; cursor < end; cursor++) {
if (source[cursor] === "\\") { cursor++; continue; }
if (source[cursor] === "[") { inClass = true; continue; }
if (source[cursor] === "]") { inClass = false; continue; }
if (source[cursor] === "/" && !inClass) {
cursor++;
while (cursor < end && /[A-Za-z]/.test(source[cursor])) cursor++;
return cursor;
}
if (source[cursor] === "\n" || source[cursor] === "\r") return start + 1;
}
return end;
};
const isRegexStart = (index) => {
const prefix = source.slice(0, index).trimEnd();
const prior = prefix.at(-1);
// FNXC:MergerUnification 2026-08-09-12:36: Regex literals can begin after
// control-flow parentheses or an operator, including `value / /pattern/`.
if (!prior || "=(:,;[!&|?{}~<>)+-*/%".includes(prior)) return true;
return /\b(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await)$/.test(prefix);
};
const scan = (start, end) => {
for (let index = start; index < end;) {
if (source[index] === "/" && !source.startsWith("//", index) && !source.startsWith("/*", index) && isRegexStart(index)) {
index = skipRegexLiteral(index, end); continue;
}
if (source.startsWith("//", index)) {
const close = source.indexOf("\n", index + 2); const until = close < 0 ? end : close;
blank(index, until); index = until; continue;
}
if (source.startsWith("/*", index)) {
const close = source.indexOf("*/", index + 2); const until = close < 0 ? end : close + 2;
blank(index, until); index = until; continue;
}
const quote = source[index];
if (quote === "'" || quote === '"') {
let cursor = index + 1;
while (cursor < end) { if (source[cursor] === "\\") cursor += 2; else if (source[cursor++] === quote) break; }
if (blankStrings) blank(index, cursor); index = cursor; continue;
}
if (quote === "`") {
let cursor = index + 1;
if (blankStrings) blank(index, index + 1);
while (cursor < end) {
if (source[cursor] === "\\") { if (blankStrings) blank(cursor, Math.min(cursor + 2, end)); cursor += 2; continue; }
if (source[cursor] === "`") { if (blankStrings) blank(cursor, cursor + 1); cursor++; break; }
if (source[cursor] === "$" && source[cursor + 1] === "{") {
if (blankStrings) blank(cursor, cursor + 2);
let depth = 1, expressionStart = cursor + 2; cursor += 2;
while (cursor < end && depth) { if (source[cursor] === "{") depth++; else if (source[cursor] === "}") depth--; cursor++; }
scan(expressionStart, depth === 0 ? cursor - 1 : end);
if (depth === 0 && blankStrings) blank(cursor - 1, cursor);
continue;
}
if (blankStrings) blank(cursor, cursor + 1);
cursor++;
}
index = cursor; continue;
}
index++;
}
};
scan(0, source.length);
return output.join("");
}