mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
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:
@@ -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,
|
||||
);
|
||||
Reference in New Issue
Block a user