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:
morluto committed 2026-10-14 16:54:08 +08:00
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;