fix(javascript)!: stop copying literals into semantic metadata (#1407)

Commit literal values with canonical JSON SHA-256 in semantic role keys
and use a type label for strings. Preserve complete values, provenance and
literal queries while avoiding two payload copies in each string node.

Verify real compiled CLI/default stdio MCP, typed union seeds, empty
strings and trace continuation with a large source-owned fixture.

Refs #723

BREAKING CHANGE: Newly produced semantic literal node IDs use
value-sha256 role keys, and string labels read "string literal". Read exact
values from properties.value and obtain node IDs from the selected parent
graph. Previously stored Evidence and its original IDs remain valid.
This commit is contained in:
Ysopwnable
2026-10-09 20:00:53 -07:00
committed by GitHub
parent 1f0f728c63
commit 8550913cda
3 changed files with 171 additions and 2 deletions
+12
View File
@@ -90,6 +90,18 @@ keep the result ambiguous. Runtime Evidence can later corroborate an exact
mapped candidate, but structural reachability, semantic influence, runtime
observation, and causal proof remain separate claims.
Semantic literal nodes keep their complete value in `properties.value`; literal
queries match that field. String labels read `string literal` rather than
repeating the payload. A literal's `identity.role_key` commits its canonical
JSON value as `value-sha256:<digest>`, together with its binding and property
role. This keeps large values out of identity and display metadata without
truncating their Evidence.
New analyses therefore produce different literal node IDs from the earlier
payload-bearing role keys. Use IDs returned by the selected parent graph and
read literal values from `properties.value`, rather than decoding role keys or
labels. Previously stored graphs and their original IDs remain valid inputs.
## Version comparison
REA pairs entities only when a tier produces one unique candidate on each side.
@@ -2,6 +2,7 @@ import {
semanticContainer,
semanticPropertyPointer,
} from "../../domain/javascript/javascriptSemanticSlots.js";
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import type { JavaScriptSemanticBinding } from "../../domain/javascript/javascriptSemanticIr.js";
import type {
JavaScriptSemanticProperty,
@@ -156,9 +157,11 @@ const addLiteralNode = (
) =>
retainSemanticGraphNode(context.state, context.file, {
kind: "literal",
roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`,
// Identity needs an exact value commitment, not another payload copy.
// Keep the complete literal in properties for queries and Evidence export.
roleKey: `literal:${binding.bindingId}:${role}:value-sha256:${digestCanonicalValue(value, "Semantic literal")}`,
location: binding.definitions[0]?.location ?? null,
label: JSON.stringify(value),
label: typeof value === "string" ? "string literal" : JSON.stringify(value),
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
@@ -0,0 +1,154 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"delivers complete large literals through CLI and default stdio MCP without metadata copies",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-literal-payload-");
const value = 'rea_literal_payload_ğ\\"\0'.padEnd(24, "x").repeat(65_536);
const source = [
`export const payload = ${JSON.stringify(value)};`,
'export const choice = flag ? 1 : "1";',
'export const empty = "";',
].join("\n");
await writeFile(join(root, "main.js"), source);
const cliResponse = await cli.run({
arguments: [
"analyze-javascript-application",
root,
"--artifact-format",
"directory",
"--json",
],
timeoutMs: 60_000,
});
expect(cliResponse.exitCode).toBe(0);
const cliEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(cliResponse.json);
const client = new Client({ name: "literal-payload-e2e", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
transport.stderr?.on("data", () => undefined);
try {
await client.connect(transport);
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
});
// The old payload-bearing role key/label exceed the default wire budget.
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
const text = response.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP text result");
const textEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(JSON.parse(text.text));
expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
const graph = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
).semantic_graph;
const literal = graph.nodes.find(
(node) => node.kind === "literal" && node.properties.value === value,
);
if (literal === undefined) throw new Error("Complete literal was lost");
const sourceSha256 = createHash("sha256").update(source).digest("hex");
expect(literal.identity).toMatchObject({
module_path: "main.js",
artifact_sha256: sourceSha256,
});
expect(literal.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "main.js" },
});
const choices = graph.nodes.filter(
(node) =>
node.kind === "literal" &&
(node.properties.value === 1 || node.properties.value === "1"),
);
expect(choices.length).toBe(2);
expect(new Set(choices.map((node) => node.node_id)).size).toBe(2);
expect(
graph.nodes.some(
(node) => node.kind === "literal" && node.properties.value === "",
),
).toBe(true);
const traceResponse = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
query: {
seed: { kind: "semantic-node", node_id: literal.node_id },
direction: "backward-provenance",
},
},
});
expect(traceResponse.isError).not.toBe(true);
const traceEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(traceResponse.structuredContent);
const trace = javaScriptSemanticTraceResultSchema.parse(
traceEvidence.normalized_result,
);
expect(
trace.nodes.some(
(node) =>
node.node_id === literal.node_id && node.properties.value === value,
),
).toBe(true);
const valueTraceResponse = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
query: {
seed: { kind: "literal", value: "1" },
direction: "backward-provenance",
},
},
});
expect(valueTraceResponse.isError).not.toBe(true);
const valueTraceEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(valueTraceResponse.structuredContent);
const valueTrace = javaScriptSemanticTraceResultSchema.parse(
valueTraceEvidence.normalized_result,
);
expect(valueTrace.seed_node_ids).toEqual(
choices
.filter((node) => node.properties.value === "1")
.map((node) => node.node_id),
);
await client.ping();
} finally {
await client.close();
await transport.close();
}
},
60_000,
);