Files
reverse-engineering-anything/scripts/verify/rea-numeric-limits.mjs
T
N0zoM1z0andgensokyo-dev 91b11f62c9 fix(settings): preserve syntax guard contracts across stack integration
Retain existing merge resolutions for update modes, source-scan failures and external-reader fixtures while replaying the settings layer on the merged inventory. Preserve parent provenance, candidate counts and presentation-test resource bounds.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-11-11 19:48:33 +08:00

1064 lines
36 KiB
JavaScript

import { readFile, readdir, stat } from "node:fs/promises";
import { join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import ts from "typescript6";
// Recognized numeric REA_ limit reads must be an analysis setting or a
// documented exception with a reason. External behavioral witnesses are
// reported separately and are not verified by this Node-only scan.
// restart-only is durable. per-call is a
// launch-captured limit whose runtime migration deletes the registry entry.
// The three limits moving on #9868, #9865, and #9836 are not in this tree;
// once their names appear here they must join ANALYSIS_SETTINGS.
const root = fileURLToPath(new URL("../../", import.meta.url));
const definitionsPath = join(root, "src/domain/analysisSettingDefinitions.ts");
const registryPath = join(root, "src/domain/numericLimitRegistry.ts");
const listFiles = async (dir) => {
const output = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist") continue;
output.push(...(await listFiles(path)));
} else if (
entry.name.endsWith(".ts") &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".fixture.ts")
)
output.push(path);
}
return output;
};
/** Registry text is a declaration, not a producer that uses the limit. */
export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts";
/**
* These limits are moving onto runtime analysis settings on their own PRs.
* A producer or a registry entry cannot satisfy that requirement.
*/
export const REQUIRED_RUNTIME_LIMITS = [
"REA_NTFS_MFT_MAX_FILE_BYTES",
"REA_REGF_HIVE_MAX_FILE_BYTES",
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
];
const unwrapExpression = (node) => {
let current = node;
while (
ts.isAsExpression(current) ||
ts.isSatisfiesExpression(current) ||
ts.isParenthesizedExpression(current) ||
ts.isTypeAssertionExpression(current) ||
ts.isNonNullExpression(current)
)
current = current.expression;
return current;
};
const literalText = (node) =>
ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)
? node.text
: undefined;
/** Resolve a symbol only when a source construct needs lexical ownership. */
const lexicalSymbols = (sourceFile) => {
let checker;
return (identifier) => {
if (checker === undefined) {
const options = { noLib: true, noResolve: true };
const host = ts.createCompilerHost(options);
host.getSourceFile = (name) =>
name === sourceFile.fileName ? sourceFile : undefined;
checker = ts
.createProgram([sourceFile.fileName], options, host)
.getTypeChecker();
}
return checker.getSymbolAtLocation(identifier);
};
};
/** Track only potentially relevant aliases; bind each use lexically before trusting it. */
const stringBindings = (sourceFile, symbols, unresolved) => {
const candidates = new Set();
const declarations = [];
const collect = (node) => {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer !== undefined
)
declarations.push(node);
ts.forEachChild(node, collect);
};
collect(sourceFile);
const potential = (node) => {
const expression = unwrapExpression(node);
if (exactLimit(literalText(expression)) !== undefined) return true;
if (ts.isIdentifier(expression)) return candidates.has(expression.text);
if (ts.isPropertyAccessExpression(expression))
return (
exactLimit(expression.name.text) !== undefined ||
potential(expression.expression)
);
if (ts.isElementAccessExpression(expression))
return (
expression.argumentExpression !== undefined &&
potential(expression.argumentExpression)
);
if (ts.isObjectLiteralExpression(expression))
return expression.properties.some(
(property) =>
ts.isPropertyAssignment(property) && potential(property.initializer),
);
if (ts.isConditionalExpression(expression))
return potential(expression.whenTrue) || potential(expression.whenFalse);
if (ts.isBinaryExpression(expression))
return potential(expression.left) || potential(expression.right);
return false;
};
let changed = true;
while (changed) {
changed = false;
for (const declaration of declarations) {
if (
!candidates.has(declaration.name.text) &&
potential(declaration.initializer)
) {
candidates.add(declaration.name.text);
changed = true;
}
}
}
const unknown = (node, reason) => {
const path = sourceFile.fileName;
const offset = node.getStart();
if (
!unresolved.some(
(item) =>
item.path === path &&
item.offset === offset &&
item.reason === reason,
)
)
unresolved.push({ path, offset, reason });
};
const initializer = (identifier) => {
if (!candidates.has(identifier.text)) return undefined;
const declaration = symbols(identifier)?.valueDeclaration;
if (
declaration === undefined ||
!ts.isVariableDeclaration(declaration) ||
!ts.isVariableDeclarationList(declaration.parent) ||
declaration.initializer === undefined
)
return undefined;
if ((declaration.parent.flags & ts.NodeFlags.Const) === 0) {
unknown(identifier, "mutable setting-key or value alias");
return undefined;
}
if (declaration.pos >= identifier.pos) {
unknown(identifier, "alias initialization order is not proven");
return undefined;
}
return declaration.initializer;
};
const candidateNames = (node) => {
const names = new Set();
const seen = new Set();
const visit = (part) => {
const literal = literalText(part);
if (literal !== undefined && exactLimit(literal) !== undefined)
names.add(literal);
if (ts.isIdentifier(part)) {
const declaration = symbols(part)?.valueDeclaration;
if (
declaration !== undefined &&
ts.isVariableDeclaration(declaration) &&
declaration.initializer !== undefined &&
!seen.has(declaration)
) {
seen.add(declaration);
visit(declaration.initializer);
}
}
ts.forEachChild(part, visit);
};
visit(node);
return names;
};
return { initializer, unknown, candidateNames };
};
/** Resolve immutable string keys; object fields are not immutable just because their binding is const. */
const boundLimitName = (node, bindings, seen = new Set()) => {
if (node === undefined) return undefined;
const expression = unwrapExpression(node);
const literal = literalText(expression);
if (literal !== undefined) return literal;
if (ts.isIdentifier(expression)) {
const value = bindings.initializer(expression);
if (value === undefined || seen.has(value)) return undefined;
seen.add(value);
return boundLimitName(value, bindings, seen);
}
if (
ts.isPropertyAccessExpression(expression) &&
ts.isIdentifier(expression.expression)
) {
const value = bindings.initializer(expression.expression);
if (value === undefined || seen.has(value)) return undefined;
seen.add(value);
const object = unwrapExpression(value);
if (!ts.isObjectLiteralExpression(object)) return undefined;
const property = object.properties.find(
(item) =>
ts.isPropertyAssignment(item) &&
(ts.isIdentifier(item.name) || ts.isStringLiteral(item.name)) &&
item.name.text === expression.name.text,
);
if (property !== undefined)
bindings.unknown(
expression,
"object setting-key property may have changed",
);
}
return undefined;
};
const isPropertyAccess = (node) =>
ts.isPropertyAccessExpression(node) ||
ts.isPropertyAccessChain?.(node) === true;
const isElementAccess = (node) =>
ts.isElementAccessExpression(node) ||
ts.isElementAccessChain?.(node) === true;
/** A plain assignment or delete does not read the old property value. */
const isWriteOnly = (node) => {
let target = node;
while (ts.isParenthesizedExpression(target.parent)) target = target.parent;
const parent = target.parent;
return (
(ts.isBinaryExpression(parent) &&
parent.left === target &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) ||
ts.isDeleteExpression(parent)
);
};
/**
* Number(), parseInt(), and schema parse() read the value they are given.
* The accessed property or element is that read, through parentheses and
* other wrappers, so a suffixless name is still classified.
*/
const rememberNumericArgument = (
discovered,
path,
node,
bindings,
seen = new Set(),
) => {
const expression = unwrapExpression(node);
if (ts.isIdentifier(expression)) {
const initializer = bindings.initializer(expression);
if (initializer !== undefined && !seen.has(initializer)) {
seen.add(initializer);
rememberNumericArgument(discovered, path, initializer, bindings, seen);
}
} else if (isPropertyAccess(expression))
remember(discovered, path, expression.name.text, expression, true);
else if (isElementAccess(expression))
remember(
discovered,
path,
boundLimitName(expression.argumentExpression, bindings),
expression,
true,
);
// Propagate numeric context only through values, not tests, receivers or
// arguments to an unrelated function call.
if (ts.isConditionalExpression(expression)) {
rememberNumericArgument(
discovered,
path,
expression.whenTrue,
bindings,
seen,
);
rememberNumericArgument(
discovered,
path,
expression.whenFalse,
bindings,
seen,
);
} else if (
ts.isBinaryExpression(expression) &&
[
ts.SyntaxKind.QuestionQuestionToken,
ts.SyntaxKind.BarBarToken,
ts.SyntaxKind.PlusToken,
ts.SyntaxKind.MinusToken,
ts.SyntaxKind.AsteriskToken,
ts.SyntaxKind.SlashToken,
ts.SyntaxKind.PercentToken,
ts.SyntaxKind.AsteriskAsteriskToken,
ts.SyntaxKind.LessThanLessThanToken,
ts.SyntaxKind.GreaterThanGreaterThanToken,
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken,
ts.SyntaxKind.AmpersandToken,
ts.SyntaxKind.BarToken,
ts.SyntaxKind.CaretToken,
].includes(expression.operatorToken.kind)
) {
rememberNumericArgument(discovered, path, expression.left, bindings, seen);
rememberNumericArgument(discovered, path, expression.right, bindings, seen);
} else if (
ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
) {
rememberNumericArgument(discovered, path, expression.right, bindings, seen);
} else if (
ts.isPrefixUnaryExpression(expression) &&
[
ts.SyntaxKind.PlusToken,
ts.SyntaxKind.MinusToken,
ts.SyntaxKind.TildeToken,
].includes(expression.operator)
) {
rememberNumericArgument(
discovered,
path,
expression.operand,
bindings,
seen,
);
} else if (ts.isTemplateExpression(expression)) {
for (const span of expression.templateSpans)
rememberNumericArgument(
discovered,
path,
span.expression,
bindings,
seen,
);
}
};
export const isNumericLimit = (name) =>
REQUIRED_RUNTIME_LIMITS.includes(name) ||
/(?:_BYTES|_MS|_MIB|_SECONDS|_POLLS|_JOBS|_ELEMENTS|_COUNT|_SESSIONS|_COMMANDS)$/u.test(
name,
) ||
/_MAX_/u.test(name) ||
/_(?:TIMEOUT|DEADLINE|BUDGET)(?:_|$)/u.test(name) ||
/_HEAP(?:_|$)/u.test(name);
const exactLimit = (text) =>
text !== undefined && /^REA_[A-Z0-9_]+$/u.test(text) ? text : undefined;
const limitName = (text) => {
const name = exactLimit(text);
return name !== undefined && isNumericLimit(name) ? name : undefined;
};
const numericSchemaText = (node) =>
/z\.(?:number|int|coerce\.number)|(?:Bytes|Timeout|Deadline|Budget|Heap|Count|Integer|MiB)Schema\b/u.test(
node.getText(),
);
const isParseAccess = (expression) =>
isPropertyAccess(expression) &&
(expression.name.text === "safeParse" || expression.name.text === "parse");
/** Number() and a numeric schema read their argument. Other parsers do not. */
const parsesNumericArgument = (node, globalConverter) => {
if (!ts.isCallExpression(node)) return false;
const expression = unwrapExpression(node.expression);
if (
ts.isIdentifier(expression) &&
["Number", "parseInt", "parseFloat"].includes(expression.text) &&
globalConverter(expression)
)
return true;
if (
isPropertyAccess(expression) &&
ts.isIdentifier(expression.expression) &&
expression.expression.text === "Number" &&
globalConverter(expression.expression) &&
(expression.name.text === "parseInt" ||
expression.name.text === "parseFloat")
)
return true;
return isParseAccess(expression) && numericSchemaText(expression.expression);
};
const numericParse = (node, globalConverter) => {
if (!ts.isCallExpression(node)) return false;
const expression = unwrapExpression(node.expression);
return (
parsesNumericArgument(node, globalConverter) || isParseAccess(expression)
);
};
/** Distinguish local bindings from the standard globals used by discovery. */
const globalConverterLookup = (sourceFile, symbols) => {
let declared;
return (identifier) => {
if (declared === undefined) {
declared = new Set();
const find = (node) => {
if (
ts.isIdentifier(node) &&
["Number", "parseInt", "parseFloat", "process"].includes(node.text) &&
ts.isDeclarationName(node)
)
declared.add(node.text);
ts.forEachChild(node, find);
};
find(sourceFile);
}
if (!declared.has(identifier.text)) return true;
return symbols(identifier) === undefined;
};
};
const globalLookups = new WeakMap();
const globalLookupFor = (source) => {
let lookup = globalLookups.get(source);
if (lookup === undefined) {
lookup = globalConverterLookup(source, lexicalSymbols(source));
globalLookups.set(source, lookup);
}
return lookup;
};
/** Direct, unshadowed process.env reads are stronger than candidate syntax. */
const isDirectEnvironmentRead = (node) => {
const expression = unwrapExpression(node);
if (!isPropertyAccess(expression) && !isElementAccess(expression))
return false;
const receiver = unwrapExpression(expression.expression);
if (!isPropertyAccess(receiver) || receiver.name.text !== "env") return false;
const process = unwrapExpression(receiver.expression);
return (
ts.isIdentifier(process) &&
process.text === "process" &&
globalLookupFor(process.getSourceFile())(process)
);
};
const readLifecycle = (node) => {
let current = node.parent;
while (current !== undefined) {
if (ts.isConstructorDeclaration(current)) return "construction";
if (
ts.isPropertyDeclaration(current) &&
!current.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
)
)
return "construction";
if (ts.isFunctionLike(current)) {
let value = current;
while (
ts.isParenthesizedExpression(value.parent) ||
ts.isAsExpression(value.parent) ||
ts.isNonNullExpression(value.parent)
)
value = value.parent;
if (
!ts.isCallExpression(value.parent) ||
value.parent.expression !== value
)
return "unknown";
current = value.parent;
}
current = current.parent;
}
return "module";
};
const remember = (discovered, path, name, node, force = false) => {
const limit = force ? exactLimit(name) : limitName(name);
if (limit === undefined) return;
const read = readLifecycle(node);
const site = {
path,
read,
offset: node.getStart(),
evidence: isDirectEnvironmentRead(node) ? "direct" : "candidate",
};
const current = discovered.get(limit);
if (current === undefined) {
discovered.set(limit, { path, read, sites: [site] });
return;
}
if (
!current.sites.some(
(other) => other.path === path && other.offset === site.offset,
)
)
current.sites.push(site);
if (current.read !== read) current.read = "unknown";
};
/**
* Producers are syntax, not text. A read is a property or element access,
* including a const string used as the element, a string argument to a call,
* an object-binding key (rename and default included), an `in` check, or an
* object key that a spread can copy. A numeric parser also reads a property or
* element access inside its argument. Comments, array literals and template
* text are not reads; executable substitutions are visited normally.
*/
export const discoverNumericLimitUses = (files) => {
const discovered = new Map();
const unresolved = [];
discovered.unresolved = unresolved;
for (const file of files) {
const relativePath = file.path.split(sep).join("/");
if (relativePath === NUMERIC_LIMIT_DECLARATION) continue;
const sourceFile = ts.createSourceFile(
relativePath,
file.source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const symbols = lexicalSymbols(sourceFile);
const bindings = stringBindings(sourceFile, symbols, unresolved);
const globalConverter = globalConverterLookup(sourceFile, symbols);
globalLookups.set(sourceFile, globalConverter);
const visit = (node) => {
if (
(ts.isPropertyAccessExpression(node) ||
ts.isPropertyAccessChain?.(node)) &&
!isWriteOnly(node)
)
remember(discovered, relativePath, node.name.text, node);
if (ts.isElementAccessExpression(node) && !isWriteOnly(node)) {
const name = boundLimitName(node.argumentExpression, bindings);
remember(discovered, relativePath, name, node);
if (
name === undefined &&
node.argumentExpression !== undefined &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === "env"
) {
bindings.unknown(
node.argumentExpression,
"dynamic environment key is not statically known",
);
for (const candidate of bindings.candidateNames(
node.argumentExpression,
))
remember(
discovered,
relativePath,
candidate,
node.argumentExpression,
);
}
}
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
const parsesNumber = parsesNumericArgument(node, globalConverter);
for (const argument of node.arguments ?? []) {
if (parsesNumber)
rememberNumericArgument(
discovered,
relativePath,
argument,
bindings,
);
else
remember(
discovered,
relativePath,
boundLimitName(argument, bindings),
node,
numericParse(node, globalConverter),
);
}
}
if (
ts.isPrefixUnaryExpression(node) &&
(node.operator === ts.SyntaxKind.PlusToken ||
node.operator === ts.SyntaxKind.MinusToken ||
node.operator === ts.SyntaxKind.TildeToken)
)
rememberNumericArgument(
discovered,
relativePath,
node.operand,
bindings,
);
if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.InKeyword
)
remember(
discovered,
relativePath,
boundLimitName(node.left, bindings),
node,
);
if (
ts.isBindingElement(node) &&
node.dotDotDotToken === undefined &&
ts.isObjectBindingPattern(node.parent)
) {
const key = node.propertyName ?? node.name;
if (
ts.isIdentifier(key) ||
ts.isStringLiteral(key) ||
ts.isNumericLiteral(key)
)
remember(discovered, relativePath, key.text, node);
}
if (
(ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)) &&
(ts.isIdentifier(node.name) ||
ts.isStringLiteral(node.name) ||
ts.isNumericLiteral(node.name))
)
remember(
discovered,
relativePath,
node.name.text,
node,
ts.isPropertyAssignment(node) && numericSchemaText(node.initializer),
);
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
return discovered;
};
/** Tracked limits join ANALYSIS_SETTINGS. The exception registry cannot classify them. */
export const requiredRuntimeLimitFailure = (
name,
{ inRuntime, inRegistry },
) => {
if (!REQUIRED_RUNTIME_LIMITS.includes(name)) return undefined;
if (inRuntime && !inRegistry) return undefined;
return `${name} must join ANALYSIS_SETTINGS; numericLimitRegistry cannot classify it`;
};
/** Parse the registry as TypeScript so provenance is owned by its entry. */
export const parseNumericLimitRegistry = (source) => {
const file = ts.createSourceFile(
NUMERIC_LIMIT_DECLARATION,
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const declaration = file.statements
.filter(ts.isVariableStatement)
.flatMap((statement) => [...statement.declarationList.declarations])
.find(
(entry) =>
ts.isIdentifier(entry.name) &&
entry.name.text === "NUMERIC_LIMIT_REGISTRY",
);
if (declaration?.initializer === undefined)
throw new Error("NUMERIC_LIMIT_REGISTRY declaration is missing");
const array = unwrapExpression(declaration.initializer);
if (!ts.isArrayLiteralExpression(array))
throw new Error("NUMERIC_LIMIT_REGISTRY must be an array literal");
const fields = (node) => {
if (!ts.isObjectLiteralExpression(node))
throw new Error("Numeric limit registry entry must be an object literal");
const result = new Map();
for (const property of node.properties) {
if (
!ts.isPropertyAssignment(property) ||
(!ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name))
)
throw new Error(
"Numeric limit registry properties must be named assignments",
);
if (result.has(property.name.text))
throw new Error(`Duplicate registry property ${property.name.text}`);
result.set(property.name.text, property.initializer);
}
return result;
};
const text = (values, key, required = true) => {
const node = values.get(key);
if (node === undefined && !required) return undefined;
const value =
node === undefined ? undefined : literalText(unwrapExpression(node));
if (value === undefined)
throw new Error(`Registry ${key} must be a string literal`);
return value;
};
return array.elements.map((element) => {
const value = fields(element);
const name = text(value, "name");
const update_mode = text(value, "update_mode");
const policy = text(value, "policy");
const read = text(value, "read");
const reason = text(value, "reason");
if (
exactLimit(name) === undefined ||
update_mode !== "restart" ||
!["restart-only", "per-call"].includes(policy) ||
!["startup", "per-call", "construction"].includes(read)
)
throw new Error(`Invalid numeric limit registry entry ${name}`);
const source = value.get("provenance");
if (source === undefined)
return { name, update_mode, policy, read, reason };
const provenance = fields(unwrapExpression(source));
const kind = text(provenance, "kind");
const producer = text(provenance, "source");
const witness = text(provenance, "witness");
const generator = text(provenance, "generator", false);
const generatorWitness = text(provenance, "generatorWitness", false);
if (
!["external-reader", "generated-control"].includes(kind) ||
(kind === "generated-control" &&
(generator === undefined || generatorWitness === undefined)) ||
(kind === "external-reader" &&
(generator !== undefined || generatorWitness !== undefined))
)
throw new Error(`Invalid external provenance for ${name}`);
return {
name,
update_mode,
policy,
read,
reason,
provenance: {
kind,
source: producer,
witness,
...(generator === undefined ? {} : { generator }),
...(generatorWitness === undefined ? {} : { generatorWitness }),
},
};
});
};
/** Extract only environment properties owned by the settings declaration. */
export const parseRuntimeSettingEnvironments = (source) => {
const file = ts.createSourceFile(
"analysisSettingDefinitions.ts",
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const declaration = file.statements
.filter(ts.isVariableStatement)
.flatMap((statement) => [...statement.declarationList.declarations])
.find(
(entry) =>
ts.isIdentifier(entry.name) && entry.name.text === "ANALYSIS_SETTINGS",
);
if (declaration?.initializer === undefined)
throw new Error("ANALYSIS_SETTINGS declaration is missing");
const settings = unwrapExpression(declaration.initializer);
if (!ts.isObjectLiteralExpression(settings))
throw new Error("ANALYSIS_SETTINGS must be an object literal");
const names = new Set();
for (const setting of settings.properties) {
if (
!ts.isPropertyAssignment(setting) ||
!ts.isObjectLiteralExpression(unwrapExpression(setting.initializer))
)
throw new Error("Analysis setting must be a static object assignment");
const definition = unwrapExpression(setting.initializer);
if (
definition.properties.some((field) => {
if (ts.isPropertyAssignment(field))
return (
!ts.isIdentifier(field.name) && !ts.isStringLiteral(field.name)
);
if (!ts.isSpreadAssignment(field) || !ts.isIdentifier(field.expression))
return true;
const name = field.expression.text;
const binding = file.statements
.filter(ts.isVariableStatement)
.flatMap((statement) =>
(statement.declarationList.flags & ts.NodeFlags.Const) !== 0
? [...statement.declarationList.declarations]
: [],
)
.find(
(entry) => ts.isIdentifier(entry.name) && entry.name.text === name,
);
if (binding?.initializer === undefined) return true;
const spread = unwrapExpression(binding.initializer);
if (!ts.isObjectLiteralExpression(spread)) return true;
let escaped = false;
const inspect = (node) => {
if (
ts.isIdentifier(node) &&
node.text === name &&
node !== binding.name &&
!(
ts.isSpreadAssignment(node.parent) &&
node.parent.expression === node
)
)
escaped = true;
ts.forEachChild(node, inspect);
};
inspect(file);
if (escaped) return true;
return spread.properties.some(
(property) =>
!ts.isPropertyAssignment(property) ||
(!ts.isIdentifier(property.name) &&
!ts.isStringLiteral(property.name)) ||
["environment", "updateMode"].includes(property.name.text),
);
})
)
throw new Error(
"Analysis setting fields must be static named assignments",
);
const modes = definition.properties.filter(
(field) =>
ts.isPropertyAssignment(field) &&
(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) &&
field.name.text === "updateMode",
);
if (
modes.length !== 1 ||
!["runtime", "restart"].includes(
literalText(unwrapExpression(modes[0].initializer)),
)
)
throw new Error("Analysis setting must declare a static updateMode");
const fields = definition.properties.filter(
(field) =>
ts.isPropertyAssignment(field) &&
(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) &&
field.name.text === "environment",
);
if (fields.length > 1)
throw new Error("Duplicate analysis setting environment");
if (fields.length === 0) continue;
const name = literalText(unwrapExpression(fields[0].initializer));
if (name === undefined || !/^REA_[A-Z0-9_]+$/u.test(name))
throw new Error(
"Analysis setting environment must be a static REA_ name",
);
names.add(name);
}
return names;
};
/** Existing launch-seed migrations. A new per-call policy is rejected. */
const TEMPORARY_PER_CALL_LIMITS = new Set([
"REA_BRIDGE_MAX_REQUEST_BYTES",
"REA_DEBUGGER_COMMAND_DEADLINE_MS",
"REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES",
"REA_HOPPER_CALLER_SCAN_BUDGET_MS",
"REA_JEB_MCP_TIMEOUT_MS",
"REA_KUNA_PROJECT_TIMEOUT_MS",
"REA_KUNA_TIMEOUT_MS",
"REA_PWNTOOLS_MAX_INPUT_BYTES",
"REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
"REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
"REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
]);
/** Temporary exceptions must be removed from both the registry and allowlist. */
export const perCallLimitFailures = (listed, temporary) => {
const failures = [];
for (const [name, policy] of listed)
if (policy === "per-call" && !temporary.has(name))
failures.push(
`${name} is a new per-call exception. Move it into ANALYSIS_SETTINGS instead of extending the temporary list.`,
);
for (const name of temporary)
if (listed.get(name) !== "per-call")
failures.push(
`${name} is no longer a temporary per-call exception; remove it from the allowlist`,
);
return failures;
};
const direct =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
if (direct) {
const failures = [];
const definitions = await readFile(definitionsPath, "utf8");
const registry = await readFile(registryPath, "utf8");
const runtime = parseRuntimeSettingEnvironments(definitions);
const entries = parseNumericLimitRegistry(registry);
const listed = new Map();
for (const entry of entries) {
const { name, policy, read, reason } = entry;
if (listed.has(name)) failures.push(`${name} is listed twice`);
listed.set(name, entry);
if (reason.trim().length < 40)
failures.push(`${name} needs a reason that states why`);
if (runtime.has(name))
failures.push(
`${name} is both a runtime setting and on the exception list`,
);
if (policy === "restart-only" && read === "per-call")
failures.push(
`${name} is restart-only but is recorded as a per-call read`,
);
if (
policy === "restart-only" &&
read !== "startup" &&
read !== "construction"
)
failures.push(`${name} is restart-only but is not read at startup`);
}
failures.push(
...perCallLimitFailures(
new Map([...listed].map(([name, entry]) => [name, entry.policy])),
TEMPORARY_PER_CALL_LIMITS,
),
);
const discovered = discoverNumericLimitUses(
await Promise.all(
(await listFiles(join(root, "src"))).map(async (path) => ({
path: relative(root, path),
source: await readFile(path, "utf8"),
})),
),
);
for (const issue of discovered.unresolved)
console.log(
`${issue.path}:${issue.offset}: numeric limit candidate unresolved (${issue.reason})`,
);
for (const [name, file] of [...discovered].sort(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
)) {
if (!runtime.has(name) && !listed.has(name))
failures.push(
`${name} in ${file.path} is neither an analysis setting nor docs/design/rea-limit-inventory.md via src/domain/numericLimitRegistry.ts`,
);
}
for (const name of listed.keys()) {
const site = discovered.get(name);
const declared = listed.get(name);
if (declared?.provenance !== undefined) {
for (const path of [
declared.provenance.source,
declared.provenance.witness,
declared.provenance.generator,
declared.provenance.generatorWitness,
]) {
if (path === undefined) continue;
try {
if (!(await stat(join(root, path))).isFile())
failures.push(`${name} provenance path is not a file: ${path}`);
} catch (cause) {
failures.push(
`${name} provenance path cannot be read: ${path} (${String(cause)})`,
);
}
}
console.log(
`${name}: behavior not verified by Node lint; reader witness: npm run test:focused -- ${declared.provenance.witness}`,
);
if (declared.provenance.kind === "generated-control")
console.log(
`${name}: generated producer not verified by Node lint; producer witness: npm run test:local -- ${declared.provenance.generatorWitness}`,
);
} else if (site === undefined) {
failures.push(
`${name} is registered but has no recognized TypeScript candidate; dynamic readers remain unverified`,
);
} else if (
!site.sites.some((observation) => observation.evidence === "direct")
) {
console.log(
`${name}: candidate syntax only; no direct process.env read was verified by Node lint`,
);
}
}
const present = new Set([...discovered.keys(), ...listed.keys(), ...runtime]);
for (const name of REQUIRED_RUNTIME_LIMITS) {
if (!present.has(name)) continue;
const failure = requiredRuntimeLimitFailure(name, {
inRuntime: runtime.has(name),
inRegistry: listed.has(name),
});
if (failure !== undefined) failures.push(failure);
}
const parameterOwners = [
"src/artifacts/ArtifactReaderLimits.ts",
"src/domain/artifactReaderLimits.ts",
"src/domain/webNetworkCapture.ts",
];
for (const relativePath of parameterOwners) {
const source = await readFile(join(root, relativePath), "utf8");
if (/update_mode:\s*"runtime"/u.test(source))
failures.push(
`${relativePath} is a per-call parameter, so update_mode cannot be runtime`,
);
if (!/update_mode:\s*"parameter"/u.test(source))
failures.push(`${relativePath} must report update_mode parameter`);
}
const forbidden =
/(?<![A-Za-z0-9_])(?:rea_restart_required|restartRequired|restart_required)(?![A-Za-z0-9_])|effect:\s*"(?:immediate|next_process)"|SettingChangeEffect/gu;
// Docs and verification receipts keep the field names they recorded.
// Source, tests, skills, and scripts cannot reintroduce those names.
const scanRoots = ["src", "tests", ".agents", "scripts"].map((dir) =>
join(root, dir),
);
const skipDir = new Set([
"node_modules",
"dist",
".turbo",
".cache",
"rea-numeric-limits.mjs",
]);
const listText = async (dir) => {
const output = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (skipDir.has(entry.name)) continue;
const path = join(dir, entry.name);
if (entry.isDirectory()) output.push(...(await listText(path)));
else if (/\.(?:ts|tsx|mjs|js|md|json|py|yml|yaml)$/u.test(entry.name))
output.push(path);
}
return output;
};
for (const dir of scanRoots) {
for (const path of await listText(dir)) {
if (path.endsWith(`${sep}rea-numeric-limits.mjs`)) continue;
const source = await readFile(path, "utf8");
const hit = source.match(forbidden);
if (hit)
failures.push(
`${relative(root, path).split(sep).join("/")} still uses ${[...new Set(hit)].join(", ")}`,
);
}
}
if (failures.length > 0) {
console.error(`Numeric REA_ limit check failed:\n${failures.join("\n")}`);
process.exit(1);
}
console.log(
`Numeric REA_ limits: ${runtime.size} runtime, ${[...listed.values()].filter((entry) => entry.policy === "restart-only").length} restart-only, ${[...listed.values()].filter((entry) => entry.policy === "per-call").length} per-call pending migration, ${discovered.size} discovered.`,
);
console.log(
"Read-site kinds describe syntax only; Node lint does not verify reachability, invocation frequency or complete reader coverage.",
);
}