Select binding facts at reads without mixing whole-binding and temporal values. Share conditional activation rules, preserve partial expression order, and bound immutable ancestry work. Memoize temporal results within each query using initializer identity, capture point, budget and dependency proofs; preserve depth limits and source Evidence. Fixes #3881
465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect } from "vitest";
|
|
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
|
import { verifyJavaScriptReturnShapes } from "../../fixtures/javascriptReturnShapes.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const source = `
|
|
export function captured(x) { const saved=x; x=2; return {type:"ok",value:saved}; }
|
|
export function assigned(x) { x=2; const saved=x; return {value:saved}; }
|
|
export function between(x) { const before=x; x=2; const after=x; return {before,after}; }
|
|
export function intermediate(x) { x=1; const saved=x; x=2; return {value:saved}; }
|
|
export function nested(x) { let saved; { saved=x; } x=2; return {value:saved}; }
|
|
export function alias(x) { const first=x; const saved=first; x=2; return {value:saved}; }
|
|
export function defaulted(x=1) { const saved=x; x=2; return {value:saved}; }
|
|
export function destructured({x}) { const saved=x; x=2; return {value:saved}; }
|
|
export function local() { let value="before"; value="after"; return {value}; }
|
|
`;
|
|
|
|
cliTest(
|
|
"preserves incoming values and proven assignments through CLI and stdio MCP",
|
|
async ({ cli }) => {
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`${source}
|
|
console.log(JSON.stringify([captured(5),assigned(5),between(5),intermediate(5),nested(5),alias(5),defaulted(5),destructured({x:5}),local()]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual([
|
|
{ type: "ok", value: 5 },
|
|
{ value: 2 },
|
|
{ before: 5, after: 2 },
|
|
{ value: 1 },
|
|
{ value: 5 },
|
|
{ value: 5 },
|
|
{ value: 5 },
|
|
{ value: 5 },
|
|
{ value: "after" },
|
|
]);
|
|
await verifyJavaScriptReturnShapes(cli, source, (fields) => {
|
|
for (const name of [
|
|
"captured",
|
|
"nested",
|
|
"alias",
|
|
"defaulted",
|
|
"destructured",
|
|
])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "unknown" }),
|
|
);
|
|
expect(fields("captured")).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/type",
|
|
state: "literal",
|
|
value: "ok",
|
|
}),
|
|
);
|
|
expect(fields("assigned")).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "literal", value: 2 }),
|
|
);
|
|
expect(fields("local")).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: "after",
|
|
}),
|
|
);
|
|
expect(fields("intermediate")).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "literal", value: 1 }),
|
|
);
|
|
expect(fields("between")).toContainEqual(
|
|
expect.objectContaining({ path: "/before", state: "unknown" }),
|
|
);
|
|
expect(fields("between")).toContainEqual(
|
|
expect.objectContaining({ path: "/after", state: "literal", value: 2 }),
|
|
);
|
|
});
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"distinguishes delayed instance values from immediate static fields and computed keys",
|
|
async ({ cli }) => {
|
|
const fieldsSource = `
|
|
export function delayed() { let x=0; class C { field=(x=1) } return {value:x}; }
|
|
export function incoming(x) { class C { field=(x=1) } return {value:x}; }
|
|
export function delayedPrivate() { let x=0; class C { #field=(x=1) } return {value:x}; }
|
|
export function staticField() { let x=0; class C { static field=(x=1) } return {value:x}; }
|
|
export function staticPrivate() { let x=0; class C { static #field=(x=1) } return {value:x}; }
|
|
export function computedKey() { let x=0; class C { [x=1]=7 } return {value:x}; }
|
|
`;
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`${fieldsSource}
|
|
console.log(JSON.stringify([delayed(),incoming(5),delayedPrivate(),staticField(),staticPrivate(),computedKey()]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual([
|
|
{ value: 0 },
|
|
{ value: 5 },
|
|
{ value: 0 },
|
|
{ value: 1 },
|
|
{ value: 1 },
|
|
{ value: 1 },
|
|
]);
|
|
await verifyJavaScriptReturnShapes(cli, fieldsSource, (fields) => {
|
|
for (const name of ["delayed", "incoming", "delayedPrivate"])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "unknown" }),
|
|
);
|
|
for (const name of ["staticField", "staticPrivate", "computedKey"])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
);
|
|
});
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"retains possible aliases through skipped optional-chain and logical assignments",
|
|
async ({ cli }) => {
|
|
const cases = [
|
|
["optionalCall", "receiver?.method(alias=replacement)"],
|
|
["optionalCallee", "receiver?.method?.(alias=replacement)"],
|
|
["optionalKey", "receiver?.[alias=replacement]"],
|
|
["continuedKey", "receiver?.field[alias=replacement]"],
|
|
["logicalWrite", "flag ||= (alias=replacement)"],
|
|
["optionalTypedCall", "receiver?.method!(alias=replacement)"],
|
|
["optionalTypedKey", "receiver?.field![alias=replacement]"],
|
|
["groupedCall", "(receiver?.method)(alias=replacement)"],
|
|
["ordinaryCall", "receiver.method(alias=replacement)"],
|
|
["groupedTypedCall", "(receiver?.method!)(alias=replacement)"],
|
|
[
|
|
"assertedGroupedCall",
|
|
"(receiver?.method as ((value: unknown) => void))!(alias=replacement)",
|
|
],
|
|
] as const;
|
|
const aliasesSource = cases
|
|
.map(
|
|
([name, expression]) => `export function ${name}(receiver,flag) {
|
|
const original={member:1}; let alias=original; const replacement={member:2};
|
|
${expression}; alias.member=3; return {value:original.member};
|
|
}`,
|
|
)
|
|
.join("\n");
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module-typescript",
|
|
"-e",
|
|
`${aliasesSource}
|
|
const receiver={method() {},field:{}};
|
|
console.log(JSON.stringify([optionalCall(null),optionalCall(receiver),optionalCallee(null),optionalCallee(receiver),optionalKey(null),optionalKey(receiver),continuedKey(null),continuedKey(receiver),logicalWrite(null,true),logicalWrite(null,false),groupedCall(receiver),ordinaryCall(receiver),optionalTypedCall(null),optionalTypedCall(receiver),optionalTypedKey(null),optionalTypedKey(receiver),groupedTypedCall(receiver),assertedGroupedCall(receiver)]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual(
|
|
[3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 1, 1, 3, 1, 3, 1, 1, 1].map((value) => ({
|
|
value,
|
|
})),
|
|
);
|
|
await verifyJavaScriptReturnShapes(
|
|
cli,
|
|
aliasesSource,
|
|
(fields) => {
|
|
for (const [name] of cases.slice(0, 7))
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "unknown" }),
|
|
);
|
|
for (const [name] of cases.slice(7))
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
);
|
|
},
|
|
"app.ts",
|
|
);
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"orders object and array reads while keeping unsupported expression order unknown",
|
|
async ({ cli }) => {
|
|
const orderedSource = `
|
|
export function objectOrder() { let x=0; return {set:(x=1),value:x}; }
|
|
export function bothSides() { let x=0; return {before:x,set:(x=1),after:x}; }
|
|
export function arrayOrder() { let x=0; return [x,(x=1),x]; }
|
|
export function computedOrder() { let x=0; return {[x=1]:0,value:x}; }
|
|
export function templateOrder() { let x=0; return {value:\`\${x=1}\${x}\`}; }
|
|
`;
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`${orderedSource}
|
|
console.log(JSON.stringify([objectOrder(),bothSides(),arrayOrder(),computedOrder(),templateOrder()]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual([
|
|
{ set: 1, value: 1 },
|
|
{ before: 0, set: 1, after: 1 },
|
|
[0, 1, 1],
|
|
{ "1": 0, value: 1 },
|
|
{ value: "11" },
|
|
]);
|
|
await verifyJavaScriptReturnShapes(cli, orderedSource, (fields) => {
|
|
for (const name of ["objectOrder", "computedOrder"])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
);
|
|
expect(fields("bothSides")).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "/before",
|
|
state: "literal",
|
|
value: 0,
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/after",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
]),
|
|
);
|
|
expect(fields("arrayOrder")).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: "/0", state: "literal", value: 0 }),
|
|
expect.objectContaining({ path: "/2", state: "literal", value: 1 }),
|
|
]),
|
|
);
|
|
expect(fields("templateOrder")).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "unknown" }),
|
|
);
|
|
});
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"shares captured dependency chains while separating values at escape capture points",
|
|
async ({ cli }) => {
|
|
const depth = 30;
|
|
const source = `export function read(){let a0=1;
|
|
${Array.from({ length: depth }, (_, index) => `let a${index + 1}=a${index}+a${index};`).join("\n")}
|
|
const saved=a${depth};
|
|
${Array.from({ length: depth + 1 }, (_, index) => `a${index}=0;`).join("\n")}
|
|
return {value:saved};}
|
|
export function captured(external) {
|
|
let shared={value:1};
|
|
const before=shared.value; external(shared); const after=shared.value;
|
|
shared={value:2}; return {before,after};
|
|
}`;
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`${source}
|
|
console.log(JSON.stringify([read(),captured(object=>{object.value=3;})]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual([
|
|
{ value: 2 ** depth },
|
|
{ before: 1, after: 3 },
|
|
]);
|
|
await verifyJavaScriptReturnShapes(cli, source, (fields) => {
|
|
expect(fields("read")).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: 2 ** depth,
|
|
}),
|
|
);
|
|
expect(fields("captured")).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "/before",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
expect.objectContaining({ path: "/after", state: "unknown" }),
|
|
]),
|
|
);
|
|
});
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"keeps defaults and do-while tests conditional while preserving definite evaluation",
|
|
async ({ cli }) => {
|
|
const defaultsSource = `
|
|
export function objectPresent(){let x=0;const {a=(x=1)}={a:2};return {value:x,selected:a};}
|
|
export function objectMissing(){let x=0;const {a=(x=1)}={};return {value:x};}
|
|
export function arrayPresent(){let x=0;const [a=(x=1)]=[2];return {value:x};}
|
|
export function arrayMissing(){let x=0;const [a=(x=1)]=[];return {value:x};}
|
|
export function assignmentDefault(){let x=0,a;({a=(x=1)}={a:2});return {value:x};}
|
|
export function nestedDefault(){let x=0;const {a:{b=(x=1)}}={a:{b:2}};return {value:x};}
|
|
export function computedKey(){let x=0;const {[x=1]:a=9}={1:2};return {value:x};}
|
|
export function initializer(){let x=0;const {a}=(x=1,{a:2});return {value:x};}
|
|
export function nestedLeftKey(){let x=0;const {a:{[x=1]:b}={}}={a:{1:2}};return {value:x};}
|
|
export function doBreak(){let x=0;do{break;}while((x=1,false));return {value:x};}
|
|
export function doMaybe(flag){let x=0;do{if(flag)break;}while((x=1,false));return {value:x};}
|
|
export function doBody(){do{var x=1;}while(false);return {value:x};}
|
|
`;
|
|
const oracle = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`${defaultsSource}
|
|
console.log(JSON.stringify([objectPresent(),objectMissing(),arrayPresent(),arrayMissing(),assignmentDefault(),nestedDefault(),computedKey(),initializer(),nestedLeftKey(),doBreak(),doMaybe(true),doMaybe(false),doBody()]));`,
|
|
],
|
|
{ timeout: 10_000 },
|
|
);
|
|
expect(JSON.parse(oracle.stdout)).toEqual([
|
|
{ value: 0, selected: 2 },
|
|
...[1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1].map((value) => ({ value })),
|
|
]);
|
|
await verifyJavaScriptReturnShapes(cli, defaultsSource, (fields) => {
|
|
for (const name of [
|
|
"objectPresent",
|
|
"objectMissing",
|
|
"arrayPresent",
|
|
"arrayMissing",
|
|
"assignmentDefault",
|
|
"nestedDefault",
|
|
"doBreak",
|
|
"doMaybe",
|
|
])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({ path: "/value", state: "unknown" }),
|
|
);
|
|
expect(fields("objectPresent")).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/selected",
|
|
state: "literal",
|
|
value: 2,
|
|
}),
|
|
);
|
|
for (const name of [
|
|
"computedKey",
|
|
"initializer",
|
|
"nestedLeftKey",
|
|
"doBody",
|
|
])
|
|
expect(fields(name)).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "/value",
|
|
state: "literal",
|
|
value: 1,
|
|
}),
|
|
);
|
|
});
|
|
},
|
|
120_000,
|
|
);
|
|
|
|
cliTest(
|
|
"keeps a captured parameter unknown when comparing export shapes through CLI and MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-captured-comparison-");
|
|
const evidence = [];
|
|
for (const [side, body] of [
|
|
["left", 'const saved=x; x=2; return {type:"ok",value:saved};'],
|
|
["right", 'return {type:"ok",value:2};'],
|
|
] as const) {
|
|
const directory = join(root, side);
|
|
await mkdir(directory);
|
|
await writeFile(
|
|
join(directory, "main.mjs"),
|
|
`export function read(x) { ${body} }`,
|
|
);
|
|
const response = await cli.run({
|
|
arguments: ["analyze-javascript-application", directory, "--json"],
|
|
});
|
|
expect(response.exitCode, response.stderr).toBe(0);
|
|
evidence.push(parseEvidence(response.json));
|
|
}
|
|
const [left, right] = evidence;
|
|
if (left === undefined || right === undefined)
|
|
throw new Error("Missing source Evidence");
|
|
const input = {
|
|
left,
|
|
right,
|
|
left_module_path: "main.mjs",
|
|
left_export_name: "read",
|
|
right_module_path: "main.mjs",
|
|
right_export_name: "read",
|
|
};
|
|
const path = join(root, "comparison.json");
|
|
await writeFile(path, JSON.stringify(input));
|
|
const response = await cli.run({
|
|
arguments: ["compare-javascript-export-shapes", path, "--json"],
|
|
});
|
|
expect(response.exitCode, response.stderr).toBe(0);
|
|
const comparison = parseEvidence(response.json);
|
|
expect(comparison.evidence_links).toEqual([
|
|
left.evidence_id,
|
|
right.evidence_id,
|
|
]);
|
|
expect(comparison.normalized_result).toMatchObject({
|
|
summary: {
|
|
added: 0,
|
|
removed: 0,
|
|
changed: 0,
|
|
unknown: 1,
|
|
},
|
|
changes: [expect.objectContaining({ path: "/value", status: "unknown" })],
|
|
});
|
|
const harness = await createApplicationMcpHarness();
|
|
try {
|
|
const reply = await harness.client.callTool({
|
|
name: "compare_javascript_export_shapes",
|
|
arguments: input,
|
|
});
|
|
expect(reply.isError).not.toBe(true);
|
|
expect(parseEvidence(reply.structuredContent)).toEqual(comparison);
|
|
const retained = await harness.client.callTool({
|
|
name: "compare_javascript_export_shapes",
|
|
arguments: {
|
|
...input,
|
|
left: { kind: "retained-evidence", evidence_id: left.evidence_id },
|
|
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
|
|
},
|
|
});
|
|
expect(retained.isError).not.toBe(true);
|
|
expect(parseEvidence(retained.structuredContent)).toEqual(comparison);
|
|
} finally {
|
|
await harness.close();
|
|
}
|
|
},
|
|
120_000,
|
|
);
|