From 91b11f62c9b609e27ac46ce2b905e91ff2efb47d Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:43:01 +0000 Subject: [PATCH] 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> --- docs/design/rea-limit-inventory.md | 2 +- scripts/verify/rea-numeric-limits.d.mts | 1 + scripts/verify/rea-numeric-limits.mjs | 74 ++++++++++++++++++- .../numericLimitGuardPermission.test.ts | 11 ++- tests/evaluation/numericLimitCheck.test.ts | 25 ++++++- 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/docs/design/rea-limit-inventory.md b/docs/design/rea-limit-inventory.md index 363402f9cd..28666a7cb7 100644 --- a/docs/design/rea-limit-inventory.md +++ b/docs/design/rea-limit-inventory.md @@ -133,7 +133,7 @@ These stay out of `update_analysis_settings`. ## Restart-only non-numeric settings -No floor or maximum. They are read from the launch environment (startup). None of them set `effect`. Recovery text tells the caller to change the launch +No floor or maximum. They are read from the launch environment (startup). None of them report a structured update mode. Recovery text tells the caller to change the launch environment, which takes effect on the next process. They stay restart-only because they choose a command, a binary, a filesystem root, the browser sandbox, secret patterns, the provider, or the shape of the MCP server diff --git a/scripts/verify/rea-numeric-limits.d.mts b/scripts/verify/rea-numeric-limits.d.mts index 07ff580e9f..f61bd401c8 100644 --- a/scripts/verify/rea-numeric-limits.d.mts +++ b/scripts/verify/rea-numeric-limits.d.mts @@ -23,6 +23,7 @@ export function perCallLimitFailures( /** Parse static registry declarations, including external-reader provenance. */ export function parseNumericLimitRegistry(source: string): Array<{ name: string; + update_mode: "restart"; policy: "restart-only" | "per-call"; read: "startup" | "per-call" | "construction"; reason: string; diff --git a/scripts/verify/rea-numeric-limits.mjs b/scripts/verify/rea-numeric-limits.mjs index 5c2ef31e20..9a6b6de2fe 100644 --- a/scripts/verify/rea-numeric-limits.mjs +++ b/scripts/verify/rea-numeric-limits.mjs @@ -701,17 +701,20 @@ export const parseNumericLimitRegistry = (source) => { 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, policy, read, reason }; + 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"); @@ -728,6 +731,7 @@ export const parseNumericLimitRegistry = (source) => { throw new Error(`Invalid external provenance for ${name}`); return { name, + update_mode, policy, read, reason, @@ -814,13 +818,26 @@ export const parseRuntimeSettingEnvironments = (source) => { !ts.isPropertyAssignment(property) || (!ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name)) || - property.name.text === "environment", + ["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) && @@ -980,6 +997,59 @@ if (direct) { 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 = + /(? + 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); diff --git a/tests/boundary/process/numericLimitGuardPermission.test.ts b/tests/boundary/process/numericLimitGuardPermission.test.ts index 8936d6fce7..58f01d3bad 100644 --- a/tests/boundary/process/numericLimitGuardPermission.test.ts +++ b/tests/boundary/process/numericLimitGuardPermission.test.ts @@ -14,8 +14,17 @@ processTest.skipIf( const root = await createTestTempDirectory("rea-limit-permission-"); await mkdir(join(root, "scripts/verify"), { recursive: true }); await mkdir(join(root, "tests/nested"), { recursive: true }); - for (const name of ["src", ".agents", "node_modules"]) + for (const name of ["src", ".agents", "node_modules", "bridge"]) await symlink(resolve(name), join(root, name), "dir"); + await symlink( + resolve("scripts/hopper-demo-x11.py"), + join(root, "scripts/hopper-demo-x11.py"), + ); + await symlink( + resolve("tests/boundary"), + join(root, "tests/boundary"), + "dir", + ); await copyFile( "scripts/verify/rea-numeric-limits.mjs", join(root, "scripts/verify/rea-numeric-limits.mjs"), diff --git a/tests/evaluation/numericLimitCheck.test.ts b/tests/evaluation/numericLimitCheck.test.ts index 99095d6d93..09fd0e54c5 100644 --- a/tests/evaluation/numericLimitCheck.test.ts +++ b/tests/evaluation/numericLimitCheck.test.ts @@ -17,11 +17,21 @@ it("reads only actual settings environment properties", () => { // environment: "REA_COMMENT_MAX_BYTES" const text = 'environment: "REA_STRING_MAX_BYTES"'; export const ANALYSIS_SETTINGS = { - first: { environment: "REA_REAL_MAX_BYTES" }, - parameter: { defaultValue: 1 }, + first: { updateMode: "runtime", environment: "REA_REAL_MAX_BYTES" }, + parameter: { updateMode: "runtime", defaultValue: 1 }, } as const; `), ]).toEqual(["REA_REAL_MAX_BYTES"]); + expect(() => + parseRuntimeSettingEnvironments( + 'export const ANALYSIS_SETTINGS = { a: { environment: "REA_REAL_MAX_BYTES" } };', + ), + ).toThrow(/updateMode/); + expect(() => + parseRuntimeSettingEnvironments( + 'export const ANALYSIS_SETTINGS = { a: { updateMode: "parameter" } };', + ), + ).toThrow(/updateMode/); expect(() => parseRuntimeSettingEnvironments( "export const ANALYSIS_SETTINGS = { a: { environment: value } };", @@ -49,12 +59,13 @@ it("reads only actual settings environment properties", () => { it("parses declared external provenance rather than inferring it from prose", () => { const source = `export const NUMERIC_LIMIT_REGISTRY = [{ read: "startup", name: "REA_HOPPER_TIMEOUT_MS", reason: "external reader", - policy: "restart-only", + update_mode: "restart", policy: "restart-only", provenance: { kind: "external-reader", source: "scripts/helper.py", witness: "tests/helper.test.ts" }, }] as const;`; expect(parseNumericLimitRegistry(source)).toEqual([ { name: "REA_HOPPER_TIMEOUT_MS", + update_mode: "restart", policy: "restart-only", read: "startup", reason: "external reader", @@ -65,6 +76,14 @@ it("parses declared external provenance rather than inferring it from prose", () }, }, ]); + expect(() => + parseNumericLimitRegistry(source.replace('update_mode: "restart", ', "")), + ).toThrow(/update_mode/); + expect(() => + parseNumericLimitRegistry( + source.replace('update_mode: "restart"', 'update_mode: "runtime"'), + ), + ).toThrow(/Invalid numeric limit registry entry/); expect(() => parseNumericLimitRegistry( source.replace('source: "scripts/helper.py"', "source: dynamicPath"),