mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(javascript): keep exports out of write relations
Project writes only from write-role references. Export references remain distinct facts and genuine assignments still traverse writes-only traces. Validation: source semantic controls and public CLI/MCP writes-only traces. Fixes #3207
This commit is contained in:
1 parent
7e5cffa2ae
commit
81f7638203
3 files changed
+118
-3
No files matched your search
@@ -25,6 +25,43 @@ import { semanticCoverageResourceLimits } from "../../domain/javascript/javascri
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
|
||||
it.each([
|
||||
["plain export", "const answer = 42; export { answer };", 0],
|
||||
["aliased export", "const answer = 42; export { answer as result };", 0],
|
||||
["declaration only", "const answer = 42;", 0],
|
||||
["genuine assignment", "let answer = 0; answer = 1;", 1],
|
||||
])(
|
||||
"projects writes only for actual writes: %s",
|
||||
(_name, source, writeCount) => {
|
||||
const graph = graphFor(source);
|
||||
const binding = graph.nodes.find(
|
||||
({ kind, label }) => kind === "binding" && label === "answer",
|
||||
);
|
||||
if (binding === undefined) throw new Error("Expected answer binding");
|
||||
const writes = graph.relations.filter(
|
||||
({ relation, target_node_id }) =>
|
||||
relation === "writes" && target_node_id === binding.node_id,
|
||||
);
|
||||
expect(writes).toHaveLength(writeCount);
|
||||
const exportReference = graph.nodes.find(
|
||||
({ identity }) => identity.role_key === "reference:0:export:answer",
|
||||
);
|
||||
if (source.includes("export {")) expect(exportReference).toBeDefined();
|
||||
|
||||
const trace = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: binding.node_id },
|
||||
direction: "backward-provenance",
|
||||
allowed_relations: ["writes"],
|
||||
});
|
||||
expect(trace.status).toBe("found");
|
||||
expect(trace.summary).toMatchObject({
|
||||
total_seed_matches: 1,
|
||||
traversed_relations: writeCount,
|
||||
});
|
||||
expect(trace.relations).toHaveLength(writeCount);
|
||||
},
|
||||
);
|
||||
|
||||
it("projects closure and direct interprocedural flow without execution", () => {
|
||||
const graph = graphFor(`
|
||||
const outer = 2;
|
||||
|
||||
@@ -648,7 +648,7 @@ function* projectDefinitionsAndReferences(
|
||||
resolution: "resolved",
|
||||
evidence: inferredSemanticEvidenceAt(context.file, reference.location),
|
||||
});
|
||||
else
|
||||
else if (reference.role === "write")
|
||||
addRelation(context.state, {
|
||||
source: expression,
|
||||
target: binding,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
|
||||
import type { JsonValue } from "../../src/domain/jsonValue.js";
|
||||
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { javaScriptSemanticTraceResultSchema } from "../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
|
||||
import { resolveJavaScriptSemanticEvidence } from "../../src/domain/javascript/javascriptSemanticGraph.js";
|
||||
|
||||
@@ -124,12 +125,88 @@ it("reports invalid nested semantic graph Evidence as caller input through CLI a
|
||||
}
|
||||
});
|
||||
|
||||
it("excludes local exports from writes-only traces through CLI and MCP", async () => {
|
||||
const root = await createTestTempDirectory("rea-semantic-export-trace-");
|
||||
await mkdir(join(root, "src"));
|
||||
await writeFile(
|
||||
join(root, "src", "main.js"),
|
||||
[
|
||||
"const answer = 42;",
|
||||
"export { answer, answer as result };",
|
||||
"let mutable = 0;",
|
||||
"mutable = 1;",
|
||||
"export { mutable };",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const cli = createCli({});
|
||||
const analyzed = await serveCli(cli, [
|
||||
"analyze-javascript-application",
|
||||
root,
|
||||
"--json",
|
||||
]);
|
||||
expect(analyzed.exitCode).toBe(0);
|
||||
const application = parseEvidence(JSON.parse(analyzed.stdout));
|
||||
const analysisResult = javascriptApplicationAnalysisResultSchema.parse(
|
||||
application.normalized_result,
|
||||
);
|
||||
const bindingId = (label: string): string => {
|
||||
const binding = analysisResult.semantic_graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === label,
|
||||
);
|
||||
if (binding === undefined)
|
||||
throw new Error(`CLI semantic graph must include the ${label} binding`);
|
||||
return binding.node_id;
|
||||
};
|
||||
|
||||
const harness = await createApplicationMcpHarness();
|
||||
try {
|
||||
const exportTrace = await assertSuccessfulTraceParity(
|
||||
cli,
|
||||
harness,
|
||||
application,
|
||||
{
|
||||
seed: { kind: "semantic-node", node_id: bindingId("answer") },
|
||||
direction: "backward-provenance",
|
||||
allowed_relations: ["writes"],
|
||||
},
|
||||
);
|
||||
expect(exportTrace.summary).toMatchObject({
|
||||
total_seed_matches: 1,
|
||||
traversed_relations: 0,
|
||||
});
|
||||
expect(exportTrace.relations).toEqual([]);
|
||||
|
||||
const assignmentTrace = await assertSuccessfulTraceParity(
|
||||
cli,
|
||||
harness,
|
||||
application,
|
||||
{
|
||||
seed: { kind: "semantic-node", node_id: bindingId("mutable") },
|
||||
direction: "backward-provenance",
|
||||
allowed_relations: ["writes"],
|
||||
},
|
||||
);
|
||||
expect(assignmentTrace.summary).toMatchObject({
|
||||
total_seed_matches: 1,
|
||||
traversed_relations: 1,
|
||||
});
|
||||
expect(assignmentTrace.relations).toHaveLength(1);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
|
||||
const assertSuccessfulTraceParity = async (
|
||||
cli: ReturnType<typeof createCli>,
|
||||
harness: Awaited<ReturnType<typeof createApplicationMcpHarness>>,
|
||||
application: ReturnType<typeof parseEvidence>,
|
||||
query: { readonly seed: unknown; readonly direction: string },
|
||||
): Promise<void> => {
|
||||
query: {
|
||||
readonly seed: unknown;
|
||||
readonly direction: string;
|
||||
readonly allowed_relations?: readonly string[];
|
||||
},
|
||||
): Promise<ReturnType<typeof javaScriptSemanticTraceResultSchema.parse>> => {
|
||||
const arguments_ = { application, query };
|
||||
const cliResponse = await serveCli(cli, [
|
||||
"trace-javascript-semantics",
|
||||
@@ -162,6 +239,7 @@ const assertSuccessfulTraceParity = async (
|
||||
).outputSchema.parse(mcpResponse.structuredContent);
|
||||
expect(mcpEvidence.normalized_result).toEqual(cliResult);
|
||||
expect(mcpEvidence).toEqual(cliEvidence);
|
||||
return cliResult;
|
||||
};
|
||||
|
||||
const assertTraceContextsResolve = (
|
||||
|
||||
Reference in new issue
Block a user