mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
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>
This commit is contained in:
1 parent
1626461ec1
commit
91b11f62c9
5 files changed
+106
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
/(?<![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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in new issue
Block a user