fix(javascript): preserve module identity and boundary fields

This commit is contained in:
morluto committed 2026-10-15 03:06:26 +08:00
1 parent 9392590f2f
commit 92e17ccfce
7 files changed
+185

No files matched your search

@@ -105,6 +105,7 @@ export const addJavaScriptSourceModules = (
label: file.path,
properties: {
semantic_role: "source-module",
source_sha256: file.sha256,
path: file.path,
container_sha256: file.container_sha256,
logical_module_key: file.path,
@@ -421,6 +421,33 @@ it("keeps duplicate function fingerprints ambiguous", () => {
expect(query.summary.total_seed_matches).toBe(2);
});
it("selects analyzer-produced boundaries by their method field", () => {
const graph = graphFor("const port = Number(process.env.PORT);");
const boundary = graph.nodes.find(({ kind }) => kind === "boundary");
if (boundary === undefined) throw new Error("Expected a boundary node");
expect(boundary.properties).toMatchObject({
boundary_kind: "coerce",
field: "Number",
method: "Number",
});
expect(
queryJavaScriptSemanticGraph(graph, {
seed: { kind: "boundary-field", field: "Number" },
direction: "forward-influence",
}),
).toMatchObject({
seed_node_ids: [boundary.node_id],
summary: { total_seed_matches: 1 },
});
expect(
queryJavaScriptSemanticGraph(graph, {
seed: { kind: "boundary-field", field: "missing-field" },
direction: "forward-influence",
}).summary.total_seed_matches,
).toBe(0);
});
const semanticNodeFor = (roleKey: string) => {
const file: JavaScriptArtifactFile = {
path: "budget.js",
@@ -263,6 +263,7 @@ const addBoundaryNode = (
null),
properties: {
boundary_kind: operation.kind,
field: operation.method,
method: operation.method,
},
});
@@ -30,6 +30,58 @@ const HASH = {
const EVIDENCE_ID = `ev_${"a".repeat(64)}`;
describe("historical source to bundle comparison", () => {
it("matches an unchanged JSON source artifact by its exact content digest", () => {
const path = "src/data.json";
const artifact = createJavaScriptApplicationNode({
kind: "artifact",
identity: {
strategy: "content-digest",
stability: "global-exact",
sha256: HASH.unchanged,
},
observations: [
{
label: path,
properties: { path, file_kind: "json" },
evidence: artifactEvidence(HASH.unchanged, path),
},
],
});
const result = compareSourceToBundle({
reference: historicalGraph("complete", [path]),
application: {
evidenceId: EVIDENCE_ID,
rootArtifactSha256: HASH.artifact,
graph: createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [artifact.node_id],
nodes: [artifact],
edges: [],
coverage: {
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
},
limitations: [],
}),
},
});
expect(item(result, path)).toMatchObject({
status: "unchanged",
confidence: "exact",
current_node_ids: [artifact.node_id],
candidates: [
expect.objectContaining({
signals: expect.arrayContaining([
expect.objectContaining({ kind: "exact-source-digest" }),
]),
}),
],
});
});
it("classifies exact, changed, absent, split, and merged mappings stably", () => {
const reference = historicalGraph("complete");
const graph = applicationGraph("complete");
@@ -120,7 +172,9 @@ describe("historical source to bundle comparison", () => {
});
expect(result.coverage.status).toBe("partial");
});
});
describe("source to bundle candidate evaluation", () => {
it("evaluates all indexed candidates for each source file", () => {
const result = compareSourceToBundle({
reference: historicalGraph("complete", ["src/split.ts"]),
@@ -45,6 +45,9 @@ export interface SourceToBundleCandidateIndex {
}
const RELEVANT_NODE_KINDS = new Set([
// Non-JavaScript source files such as JSON are projected as artifacts.
// Their content digests and paths still identify historical source files.
"artifact",
"javascript-asset",
"javascript-module",
"source-module",
@@ -96,3 +96,85 @@ cliTest(
}
},
);
cliTest(
"matches an unchanged source module after a sibling file changes",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-source-module-version-");
const versions = await Promise.all(
["left", "right"].map(async (name) => {
const application = join(root, name);
await mkdir(join(application, "src"), { recursive: true });
await writeFile(
join(application, "package.json"),
JSON.stringify({
name: "source-module-fixture",
version: "1.0.0",
main: "src/main.js",
}),
);
await writeFile(
join(application, "src/main.js"),
'require("./util.js");\n',
);
await writeFile(
join(application, "src/util.js"),
`module.exports = ${JSON.stringify(name)};\n`,
);
const response = await cli.run({
arguments: ["analyze-javascript-application", application, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
return parseEvidence(response.json);
}),
);
const [left, right] = versions;
if (left === undefined || right === undefined)
throw new Error("Missing source module Evidence");
const analyses = versions.map(({ normalized_result }) =>
javascriptApplicationAnalysisResultSchema.parse(normalized_result),
);
const sourceModules = analyses.map(({ graph }) => {
const node = graph.nodes.find(
({ kind, observations }) =>
kind === "javascript-module" &&
observations.some(
({ properties }) =>
properties.semantic_role === "source-module" &&
properties.path === "src/main.js",
),
);
if (node === undefined) throw new Error("Missing main source module");
expect(
node.observations.some(
({ properties }) => typeof properties.source_sha256 === "string",
),
).toBe(true);
return node;
});
const inputPath = join(root, "comparison.json");
await writeFile(inputPath, JSON.stringify({ left, right }));
const response = await cli.run({
arguments: ["compare-application-versions", inputPath, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
const result = applicationVersionComparisonResultSchema.parse(
parseEvidence(response.json).normalized_result,
);
expect(result.items).toContainEqual(
expect.objectContaining({
left_node_id: sourceModules[0]?.node_id,
right_node_id: sourceModules[1]?.node_id,
status: "unchanged",
match: expect.objectContaining({
status: "matched",
basis: "exact-module-source-digest",
confidence: "exact",
}),
}),
);
},
);
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -15,6 +16,22 @@ import {
} from "../../../support/javascriptApplicationFixture.js";
describe("TypeScript source module loader semantics", () => {
it("publishes each source module's own content digest", async () => {
const root = await createTestTempDirectory("rea-source-module-digest-");
const source = "module.exports = 42;\n";
await writeFixtureFiles(root, { "main.js": source });
const { graph } = await reconstructJavaScriptFixture(root);
const sourceModule = findSourceModule(graph, "main.js");
const digest = createHash("sha256").update(source).digest("hex");
expect(
sourceModule?.observations.find(
({ properties }) => properties.semantic_role === "source-module",
)?.properties,
).toMatchObject({ source_sha256: digest });
});
it("keeps whole type-only declarations out of runtime module relationships", async () => {
const root = await createTestTempDirectory("rea-type-module-links-");
await writeFixtureFiles(root, {