mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 19:05:45 +00:00
fix(javascript): distinguish relocation from application changes
Keep location-only matched pairs unchanged under complete coverage while retaining both coordinates and location dimensions. Preserve incomplete coverage as unknown and retain definite content, property, and relationship changes. Validated with focused domain, compiled CLI and MCP workflows, static checks and docs checks. A tiny public CLI bundle fixture reproduces the issue; the reported Bitwarden releases were not rerun. Refs #4520
This commit is contained in:
1 parent
1e36e707b1
commit
556504edcb
4 files changed
+332
-4
No files matched your search
@@ -0,0 +1,220 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
artifactEvidence,
|
||||
completeCoverage,
|
||||
fixtureNode,
|
||||
} from "./javascriptApplicationGraph.fixture.js";
|
||||
import {
|
||||
createJavaScriptApplicationEdge,
|
||||
createJavaScriptApplicationGraph,
|
||||
type JavaScriptApplicationGraph,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
|
||||
import { applicationVersionComparisonResultSchema } from "./javascriptApplicationVersionComparisonSchemas.js";
|
||||
|
||||
const graph = (
|
||||
sha: string,
|
||||
offset: number,
|
||||
coverage: JavaScriptApplicationGraph["coverage"],
|
||||
options: {
|
||||
content?: string;
|
||||
permission?: string;
|
||||
relation?: "handles" | "invokes";
|
||||
} = {},
|
||||
) => {
|
||||
const evidence = {
|
||||
...artifactEvidence(sha, "main.js"),
|
||||
location: {
|
||||
available: true as const,
|
||||
value: {
|
||||
kind: "file-offset-range" as const,
|
||||
start: offset,
|
||||
end: offset + 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
const module = fixtureNode(
|
||||
"javascript-module",
|
||||
{
|
||||
strategy: "artifact-local-key",
|
||||
stability: "artifact-version",
|
||||
artifact_sha256: sha,
|
||||
namespace: "webpack-module",
|
||||
key: "11",
|
||||
},
|
||||
"module",
|
||||
{
|
||||
source_sha256: options.content ?? "c".repeat(64),
|
||||
structural_fingerprint_sha256: "d".repeat(64),
|
||||
structural_fingerprint_algorithm: "fixture",
|
||||
},
|
||||
evidence,
|
||||
);
|
||||
const channel = fixtureNode(
|
||||
"ipc-channel",
|
||||
{
|
||||
strategy: "artifact-local-key",
|
||||
stability: "artifact-version",
|
||||
artifact_sha256: sha,
|
||||
namespace: "ipc",
|
||||
key: "unchanged-channel",
|
||||
},
|
||||
"unchanged-channel",
|
||||
{
|
||||
channel: "unchanged-channel",
|
||||
...(options.permission === undefined
|
||||
? {}
|
||||
: { permission: options.permission }),
|
||||
},
|
||||
evidence,
|
||||
);
|
||||
return createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [module.node_id],
|
||||
nodes: [module, channel],
|
||||
edges: [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: module.node_id,
|
||||
target_node_id: channel.node_id,
|
||||
relation: options.relation ?? "handles",
|
||||
properties: {},
|
||||
evidence,
|
||||
}),
|
||||
],
|
||||
coverage,
|
||||
limitations:
|
||||
coverage.status === "complete"
|
||||
? []
|
||||
: [
|
||||
"Controlled incomplete inventory; unobserved relationships remain unknown.",
|
||||
],
|
||||
});
|
||||
};
|
||||
const compare = (
|
||||
left: JavaScriptApplicationGraph,
|
||||
right: JavaScriptApplicationGraph,
|
||||
) =>
|
||||
compareJavaScriptApplicationVersions({
|
||||
left: {
|
||||
evidenceId: `ev_${"1".repeat(64)}`,
|
||||
rootArtifactSha256: "a".repeat(64),
|
||||
graph: left,
|
||||
},
|
||||
right: {
|
||||
evidenceId: `ev_${"2".repeat(64)}`,
|
||||
rootArtifactSha256: "b".repeat(64),
|
||||
graph: right,
|
||||
},
|
||||
leftNativeEvidence: [],
|
||||
rightNativeEvidence: [],
|
||||
});
|
||||
const partialCoverage: JavaScriptApplicationGraph["coverage"] = {
|
||||
...completeCoverage,
|
||||
status: "partial",
|
||||
omitted_count: null,
|
||||
};
|
||||
|
||||
it("keeps relocated exact modules and inferred channels unchanged while retaining both coordinates", () => {
|
||||
const left = graph("a".repeat(64), 10, completeCoverage),
|
||||
right = graph("b".repeat(64), 50, completeCoverage);
|
||||
const result = applicationVersionComparisonResultSchema.parse(
|
||||
JSON.parse(JSON.stringify(compare(left, right))),
|
||||
);
|
||||
expect(result.summary).toEqual({
|
||||
unchanged: 2,
|
||||
changed: 0,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 0,
|
||||
});
|
||||
expect(result.items).toHaveLength(2);
|
||||
for (const item of result.items)
|
||||
expect(item).toMatchObject({
|
||||
status: "unchanged",
|
||||
dimensions: ["location"],
|
||||
});
|
||||
expect(result.matching).toMatchObject({
|
||||
exact_module_source_digest: 1,
|
||||
semantic_key: 1,
|
||||
});
|
||||
expect(result.graph.nodes).toHaveLength(4);
|
||||
expect(
|
||||
result.graph.nodes.map((node) => node.observations[0]?.evidence.location),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
left.nodes[0]?.observations[0]?.evidence.location,
|
||||
right.nodes[0]?.observations[0]?.evidence.location,
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
result.graph.edges.filter((edge) => edge.relation === "changed_from"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps relocation and uncertain relationships unknown under incomplete coverage (relationship change: %s)",
|
||||
(changeRelation) => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, completeCoverage),
|
||||
graph("b".repeat(64), 50, partialCoverage, {
|
||||
relation: changeRelation ? "invokes" : "handles",
|
||||
}),
|
||||
);
|
||||
expect(result.summary).toEqual({
|
||||
unchanged: 0,
|
||||
changed: 0,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 2,
|
||||
});
|
||||
for (const item of result.items)
|
||||
expect(item).toMatchObject({
|
||||
status: "unknown",
|
||||
dimensions: changeRelation
|
||||
? ["location", "relationships", "coverage"]
|
||||
: ["location", "coverage"],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("still classifies complete relationship changes beside relocation as changed", () => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, completeCoverage),
|
||||
graph("b".repeat(64), 50, completeCoverage, { relation: "invokes" }),
|
||||
);
|
||||
expect(result.summary.changed).toBe(2);
|
||||
for (const item of result.items)
|
||||
expect(item.dimensions).toEqual(["location", "relationships"]);
|
||||
});
|
||||
|
||||
it("retains definite content changes under partial coverage alongside relocation", () => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, completeCoverage),
|
||||
graph("b".repeat(64), 50, partialCoverage, { content: "e".repeat(64) }),
|
||||
);
|
||||
expect(
|
||||
result.items.find((item) => item.node_kind === "javascript-module"),
|
||||
).toMatchObject({
|
||||
status: "changed",
|
||||
dimensions: ["content", "location", "properties"],
|
||||
});
|
||||
expect(
|
||||
result.items.find((item) => item.node_kind === "ipc-channel")?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
|
||||
it("retains definite property changes beside relocation", () => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, completeCoverage),
|
||||
graph("b".repeat(64), 50, partialCoverage, { permission: "changed" }),
|
||||
);
|
||||
expect(
|
||||
result.items.find((item) => item.node_kind === "ipc-channel"),
|
||||
).toMatchObject({
|
||||
status: "changed",
|
||||
dimensions: ["location", "properties"],
|
||||
});
|
||||
expect(
|
||||
result.items.find((item) => item.node_kind === "javascript-module")?.status,
|
||||
).toBe("unknown");
|
||||
});
|
||||
Reference in new issue
Block a user