fix(javascript): preserve mapped runtime digest disagreements

Fixes #3316

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-15 17:04:49 +08:00
1 parent e18fccc548
commit d7651b6a0d
5 files changed
+316 -84

No files matched your search

+7 -4
View File
@@ -91,17 +91,20 @@ nodes. REA evaluates compatible static candidates conservatively:
1. A captured-source SHA-256 plus one matching mapped location yields
a content-and-location match.
2. One unique exact content digest can match even when no location mapping is
available. Whole-asset digests and reconstructed module-factory source
2. When no static candidate exists at a mapped location, one unique exact
content digest can match. Whole-asset digests and reconstructed module-factory source
digests remain distinguishable in the reported basis.
3. Without captured bytes, one unique mapped artifact path can produce a
medium-confidence location match.
4. Multiple candidates remain `ambiguous`; no `observed_as` edge is emitted.
With captured source and mapped candidates, only mapped content matches
qualify; unrelated content matches elsewhere are excluded.
`candidate_static_count` reports the full qualified layer/node count, and
`candidate_static_nodes` includes every unique candidate reference.
5. If captured bytes disagree with the static candidate at the mapped path,
the result is an explicit mismatch. A path never overrides a digest
disagreement.
the result is an explicit mismatch. An equal digest elsewhere cannot hide
that disagreement. If a mapped candidate's digest is unavailable and no
mapped content match is known, the comparison remains `unknown`.
6. Missing authorization mappings and incomplete or truncated input coverage
remain unknown or truncated rather than becoming absence claims.
@@ -47,6 +47,7 @@ const classifyLayer = (
const nodes = layer.graph.nodes.filter(isLoadStateNode);
const direct = new Map<string, string[]>();
const ambiguous = new Set<string>();
const incomplete = new Set<string>();
for (const match of matches)
if (
match.status === "matched" &&
@@ -54,10 +55,12 @@ const classifyLayer = (
match.static_node_id !== null
)
append(direct, match.static_node_id, match.runtime_node_id);
else if (match.status === "ambiguous")
else if (match.status === "ambiguous" || match.status === "unknown") {
const uncertain = match.status === "ambiguous" ? ambiguous : incomplete;
for (const candidate of match.candidate_static_nodes)
if (candidate.static_layer_id === layer.layerId)
ambiguous.add(candidate.static_node_id);
uncertain.add(candidate.static_node_id);
}
const resident = residentNodes(layer, new Set(direct.keys()));
const runtimeScope = createStaticRuntimeScope(layer, runtimeEntities);
const scopedEntities = runtimeEntities.filter(
@@ -82,11 +85,13 @@ const classifyLayer = (
runtimeNodeIds: [],
reason: "containing-asset-was-loaded",
});
if (ambiguous.has(node.node_id))
if (incomplete.has(node.node_id) || ambiguous.has(node.node_id))
return state(layer, node, {
status: "unknown",
runtimeNodeIds: [],
reason: "ambiguous-runtime-candidate",
reason: incomplete.has(node.node_id)
? "static-or-runtime-coverage-incomplete"
: "ambiguous-runtime-candidate",
});
const nodeWithinScope = staticNodeWithinRuntimeScope(node, runtimeScope);
if (layerComplete && nodeWithinScope)
@@ -78,60 +78,13 @@ const reconcileEntity = (
candidates: [],
});
const locations = candidatesWithLocation(entity, index, layers);
if (entity.sourceSha256 !== null) {
const content =
index.byCategoryDigest.get(
categoryDigestKey(entity.staticKind, entity.sourceSha256),
) ?? [];
const locatedKeys = new Set(
locations.map(({ candidate }) => candidateKey(candidate)),
if (entity.sourceSha256 !== null)
return reconcileCapturedSource(
entity,
entity.sourceSha256,
locations,
index,
);
const combined = content.filter((candidate) =>
locatedKeys.has(candidateKey(candidate)),
);
const combinedCandidate = combined.at(0);
if (combined.length === 1 && combinedCandidate !== undefined)
return matched(entity, combinedCandidate, {
basis: "content-and-location",
confidence: "high",
reason: "content-and-location-match",
});
const contentCandidate = content.at(0);
if (content.length === 1 && contentCandidate !== undefined) {
const candidate = contentCandidate;
const digestBasis = candidate.digests.find(
({ sha256 }) => sha256 === entity.sourceSha256,
)?.basis;
return matched(entity, candidate, {
basis: digestBasis ?? "content-sha256",
confidence: "high",
reason: "unique-content-match",
});
}
if (content.length > 1)
return unresolved(entity, {
status: "ambiguous",
basis: "none",
confidence: "unknown",
reason: "ambiguous-static-candidates",
candidates: content,
});
if (locations.length > 0)
return unresolved(entity, {
status: "unmatched",
basis: strongestLocationBasis(locations),
confidence: "high",
reason: "captured-content-disagrees-with-static-location",
candidates: locations.map(({ candidate }) => candidate),
});
return unresolved(entity, {
status: "unmatched",
basis: "none",
confidence: "high",
reason: "no-static-content-match",
candidates: [],
});
}
const location = locations.at(0);
if (locations.length === 1 && location !== undefined) {
@@ -169,6 +122,69 @@ interface LocatedCandidate {
readonly mapping: MappedRuntimePath;
}
const reconcileCapturedSource = (
entity: RuntimeReconciliationEntity,
sourceSha256: string,
locations: readonly LocatedCandidate[],
index: CandidateIndex,
): EvaluatedMatch => {
// A known mapped candidate owns the comparison. Unrelated equal bytes cannot
// repair a disagreement or establish the content of a candidate without a digest.
const content =
locations.length > 0
? locations
.map(({ candidate }) => candidate)
.filter(({ digests }) =>
digests.some(({ sha256 }) => sha256 === sourceSha256),
)
: (index.byCategoryDigest.get(
categoryDigestKey(entity.staticKind, sourceSha256),
) ?? []);
const candidate = content.at(0);
if (content.length === 1 && candidate !== undefined)
return matched(entity, candidate, {
basis:
locations.length > 0
? "content-and-location"
: (candidate.digests.find(({ sha256 }) => sha256 === sourceSha256)
?.basis ?? "content-sha256"),
confidence: "high",
reason:
locations.length > 0
? "content-and-location-match"
: "unique-content-match",
});
if (content.length > 1)
return unresolved(entity, {
status: "ambiguous",
basis: locations.length > 0 ? "content-and-location" : "none",
confidence: "unknown",
reason: "ambiguous-static-candidates",
candidates: content,
});
if (locations.length > 0) {
const digestUnavailable = locations.some(
({ candidate: { digests } }) => digests.length === 0,
);
return unresolved(entity, {
status: digestUnavailable ? "unknown" : "unmatched",
basis: strongestLocationBasis(locations),
confidence: digestUnavailable ? "unknown" : "high",
reason: digestUnavailable
? "static-or-runtime-coverage-incomplete"
: "captured-content-disagrees-with-static-location",
candidates: locations.map(({ candidate: located }) => located),
});
}
return unresolved(entity, {
status: "unmatched",
basis: "none",
confidence: "high",
reason: "no-static-content-match",
candidates: [],
});
};
const candidatesWithLocation = (
entity: RuntimeReconciliationEntity,
index: CandidateIndex,
@@ -0,0 +1,231 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
parseRuntimeReconciliationInput,
reconcileJavaScriptRuntime,
} from "../../../../src/domain/javascript/javascriptRuntimeReconciliation.js";
import { reconcileRuntimeEntities } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationMatching.js";
import { classifyStaticLoadStates } from "../../../../src/domain/javascript/javascriptRuntimeLoadState.js";
import { projectRuntimeCaptures } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationRuntime.js";
import { reconcileJavaScriptRuntimeInputSchema } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js";
import { collectStaticRuntimeCandidates } from "../../../../src/domain/javascript/javascriptRuntimeStaticCandidates.js";
import {
SOURCE,
analyzeFixture,
applicationFixture,
electronRuntimeEvidence,
} from "../../../fixtures/javascriptRuntimeReconciliation.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const CAPTURED = "export const observed = 'different';\n";
const collisionInput = async (
alternatives: number,
explicitMapping: boolean,
) => {
const root = await applicationFixture();
if (alternatives > 0) await writeFile(join(root, "other.js"), CAPTURED);
const live = explicitMapping
? await createTestTempDirectory("rea-runtime-content-live-")
: root;
const extra = await createTestTempDirectory("rea-runtime-content-extra-");
await writeFile(join(extra, "app.js"), CAPTURED);
return parseRuntimeReconciliationInput(
reconcileJavaScriptRuntimeInputSchema.parse({
static_layers: [
{
role: "application",
analysis: await analyzeFixture(root),
runtime_mappings: explicitMapping
? [{ kind: "file-root", root: live, artifact_prefix: "" }]
: [],
},
...(alternatives > 1
? [{ role: "assets", analysis: await analyzeFixture(extra) }]
: []),
],
runtime_observations: [
electronRuntimeEvidence(live, CAPTURED, { includeWorker: false }),
],
}),
);
};
it.each([
[0, false],
[1, false],
[2, false],
[0, true],
[1, true],
[2, true],
] as const)(
"preserves mapped disagreement with %i other content matches (explicit mapping: %s)",
async (alternatives, explicitMapping) => {
const parsed = await collisionInput(alternatives, explicitMapping);
const result = reconcileJavaScriptRuntime(parsed);
const script = result.reconciliations.find(
({ entity_kind }) => entity_kind === "script",
);
expect(script).toMatchObject({
status: "unmatched",
basis: explicitMapping ? "operator-file-mapping" : "artifact-path",
confidence: "high",
reason: "captured-content-disagrees-with-static-location",
static_layer_id: null,
static_node_id: null,
candidate_static_count: 1,
});
const candidate = script?.candidate_static_nodes[0];
const selected = result.graph.nodes.find(
({ node_id }) => node_id === candidate?.static_node_id,
);
expect(selected?.observations).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({ path: "app.js" }),
}),
]),
);
expect(
result.graph.edges.some(
(edge) =>
edge.relation === "observed_as" &&
edge.target_node_id === script?.runtime_node_id,
),
).toBe(false);
},
);
it("restricts content-and-location ambiguity to mapped candidates", async () => {
const application = await applicationFixture();
const cache = await applicationFixture();
const other = await applicationFixture();
const live = await createTestTempDirectory("rea-runtime-content-mapped-");
const analysis = await analyzeFixture(application);
const mapping = [{ kind: "file-root", root: live, artifact_prefix: "" }];
const parsed = parseRuntimeReconciliationInput(
reconcileJavaScriptRuntimeInputSchema.parse({
static_layers: [
{ role: "application", analysis, runtime_mappings: mapping },
{
role: "cache",
analysis: await analyzeFixture(cache),
runtime_mappings: mapping,
},
{ role: "assets", analysis: await analyzeFixture(other) },
],
runtime_observations: [
electronRuntimeEvidence(live, SOURCE, { includeWorker: false }),
],
}),
);
const result = reconcileJavaScriptRuntime(parsed);
const script = result.reconciliations.find(
({ entity_kind }) => entity_kind === "script",
);
expect(script).toMatchObject({
status: "ambiguous",
reason: "ambiguous-static-candidates",
candidate_static_count: 2,
});
const unmapped = result.static_layers.find(({ role }) => role === "assets");
expect(
script?.candidate_static_nodes.map(
({ static_layer_id }) => static_layer_id,
),
).not.toContain(unmapped?.layer_id);
});
it.each([false, true])(
"retains digest-only matching without a mapped candidate (mapping: %s)",
async (mapped) => {
const root = await applicationFixture();
await writeFile(join(root, "other.js"), CAPTURED);
const live = await createTestTempDirectory(
"rea-runtime-content-unlocated-",
);
const result = reconcileJavaScriptRuntime(
parseRuntimeReconciliationInput(
reconcileJavaScriptRuntimeInputSchema.parse({
static_layers: [
{
role: "application",
analysis: await analyzeFixture(root),
runtime_mappings: mapped
? [{ kind: "file-root", root: live, artifact_prefix: "" }]
: [],
},
],
runtime_observations: [
electronRuntimeEvidence(live, CAPTURED, {
scriptFile: "absent.js",
includeWorker: false,
}),
],
}),
),
);
expect(
result.reconciliations.find(
({ entity_kind }) => entity_kind === "script",
),
).toMatchObject({
status: "matched",
basis: "content-sha256",
confidence: "high",
reason: "unique-content-match",
candidate_static_count: 1,
});
},
);
it("keeps unavailable mapped digests unknown instead of borrowing unrelated matches", async () => {
const parsed = await collisionInput(1, true);
const candidates = collectStaticRuntimeCandidates(parsed.layers).map(
(candidate) =>
candidate.paths.includes("app.js") && candidate.category === "javascript"
? { ...candidate, digests: [] }
: candidate,
);
// Model the matcher's supported missing-digest boundary; do not falsify Evidence.
const runtime = projectRuntimeCaptures(parsed.captures);
const result = reconcileRuntimeEntities({
entities: runtime.entities,
candidates,
layers: parsed.layers,
});
expect(
result.items.find(({ entity_kind }) => entity_kind === "script"),
).toMatchObject({
status: "unknown",
basis: "operator-file-mapping",
confidence: "unknown",
reason: "static-or-runtime-coverage-incomplete",
candidate_static_count: 1,
});
const script = result.items.find(
({ entity_kind }) => entity_kind === "script",
);
const candidate = script?.candidate_static_nodes[0];
const states = classifyStaticLoadStates(
parsed.layers,
runtime.entities,
result.items,
{
reconciliationComplete: true,
},
);
expect(
states.states.find(
(state) =>
state.static_layer_id === candidate?.static_layer_id &&
state.static_node_id === candidate?.static_node_id,
),
).toMatchObject({
status: "unknown",
reason: "static-or-runtime-coverage-incomplete",
});
});
@@ -224,29 +224,6 @@ it("keeps static load states unknown when one scoped capture omits scripts", asy
);
});
it("reports a captured digest disagreement instead of accepting a path match", async () => {
const fixture = await applicationFixture();
const staticEvidence = await analyzeFixture(fixture);
const runtimeEvidence = electronRuntimeEvidence(
fixture,
"export const observed = 'different';\n",
);
const result = reconcileInput({
static_layers: [{ role: "application", analysis: staticEvidence }],
runtime_observations: [runtimeEvidence],
});
const script = result.reconciliations.find(
({ entity_kind: kind }) => kind === "script",
);
expect(script).toMatchObject({
status: "unmatched",
reason: "captured-content-disagrees-with-static-location",
});
expect(script?.candidate_static_nodes).toHaveLength(1);
});
it("reconciles active Electron as a partial target-only runtime capture", async () => {
const fixture = await applicationFixture();
const staticEvidence = await analyzeFixture(fixture);