fix(javascript): preserve module identity and boundary fields
This commit is contained in:
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",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
+17
@@ -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, {
|
||||
|
||||
Reference in new issue
Block a user