Files
rea/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts
T
morluto b58adafa4f fix(javascript): align module lookup and source evidence
Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts.

Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036.

Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
2026-10-14 08:46:50 +08:00

466 lines
15 KiB
TypeScript

import { createJavaScriptApplicationEdge } from "./javascriptApplicationGraph.js";
import type { ApplicationEdge } from "./javascriptApplicationGraphSchemas.js";
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import type { JavaScriptRuntimeReconciliationItem } from "./javascriptRuntimeReconciliationSchemas.js";
import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js";
import {
mappedRuntimePaths,
type MappedRuntimePath,
type StaticRuntimeCandidate,
} from "./javascriptRuntimeStaticCandidates.js";
export interface RuntimeMatchingProjection {
readonly items: readonly JavaScriptRuntimeReconciliationItem[];
readonly edges: readonly ApplicationEdge[];
readonly omittedItems: number;
}
/** Match runtime entities conservatively and emit only inferred observed_as edges. */
export const reconcileRuntimeEntities = (input: {
readonly entities: readonly RuntimeReconciliationEntity[];
readonly candidates: readonly StaticRuntimeCandidate[];
readonly layers: readonly ParsedStaticLayer[];
}): RuntimeMatchingProjection => {
const index = indexCandidates(input.candidates);
const evaluated = input.entities.map((entity) =>
reconcileEntity(entity, index, input.layers),
);
const retained = evaluated.sort((left, right) =>
compareUnicodeCodePoints(
left.item.reconciliation_id,
right.item.reconciliation_id,
),
);
return {
items: retained.map(({ item }) => item),
edges: retained.flatMap(({ edge }) => (edge === null ? [] : [edge])),
omittedItems: 0,
};
};
interface EvaluatedMatch {
readonly item: JavaScriptRuntimeReconciliationItem;
readonly edge: ApplicationEdge | null;
}
interface CandidateIndex {
readonly byCategoryDigest: ReadonlyMap<
string,
readonly StaticRuntimeCandidate[]
>;
readonly byNativeDeclarationPath: ReadonlyMap<
string,
readonly {
readonly candidate: StaticRuntimeCandidate;
readonly sourcePath: string | null;
}[]
>;
readonly byLayerCategoryPath: ReadonlyMap<
string,
readonly StaticRuntimeCandidate[]
>;
}
const reconcileEntity = (
entity: RuntimeReconciliationEntity,
index: CandidateIndex,
layers: readonly ParsedStaticLayer[],
): EvaluatedMatch => {
if (entity.location.kind === "unresolved")
return unresolved(entity, {
status: "unknown",
basis: "none",
confidence: "unknown",
reason: "runtime-location-unresolved",
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)),
);
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) {
const located = location;
return matched(entity, located.candidate, {
basis: located.mapping.basis,
confidence: "medium",
reason: "unique-location-match",
});
}
if (locations.length > 1)
return unresolved(entity, {
status: "ambiguous",
basis: strongestLocationBasis(locations),
confidence: "unknown",
reason: "ambiguous-static-candidates",
candidates: locations.map(({ candidate }) => candidate),
});
const mapped = layers.some(
(layer) => mappedRuntimePaths(layer, entity).length > 0,
);
return unresolved(entity, {
status: mapped ? "unmatched" : "unknown",
basis: "none",
confidence: "unknown",
reason: mapped
? "no-static-content-match"
: "no-authorized-location-mapping",
candidates: [],
});
};
interface LocatedCandidate {
readonly candidate: StaticRuntimeCandidate;
readonly mapping: MappedRuntimePath;
}
const candidatesWithLocation = (
entity: RuntimeReconciliationEntity,
index: CandidateIndex,
layers: readonly ParsedStaticLayer[],
): LocatedCandidate[] => {
const located = new Map<string, LocatedCandidate>();
for (const layer of layers)
for (const mapping of mappedRuntimePaths(layer, entity))
for (const candidate of [
...(index.byLayerCategoryPath.get(
layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.path),
) ?? []),
...(mapping.fileStyle === null || mapping.nativeRelativePath === null
? []
: (
index.byNativeDeclarationPath.get(
`${mapping.fileStyle}\0${layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.nativeRelativePath)}`,
) ?? []
)
.filter(
({ sourcePath }) =>
mapping.artifactPrefix === "" ||
(sourcePath !== null &&
sourcePath.startsWith(`${mapping.artifactPrefix}/`)),
)
.map(({ candidate }) => candidate)),
]) {
const key = candidateKey(candidate);
if (!located.has(key)) located.set(key, { candidate, mapping });
}
return [...located.values()].sort((left, right) =>
compareUnicodeCodePoints(
candidateKey(left.candidate),
candidateKey(right.candidate),
),
);
};
const indexCandidates = (
candidates: readonly StaticRuntimeCandidate[],
): CandidateIndex => {
const byCategoryDigest = new Map<string, StaticRuntimeCandidate[]>();
const byLayerCategoryPath = new Map<string, StaticRuntimeCandidate[]>();
const byNativeDeclarationPath = new Map<
string,
{ candidate: StaticRuntimeCandidate; sourcePath: string | null }[]
>();
for (const candidate of candidates) {
for (const { sha256 } of candidate.digests)
appendIndex(
byCategoryDigest,
categoryDigestKey(candidate.category, sha256),
candidate,
);
for (const path of candidate.paths)
appendIndex(
byLayerCategoryPath,
layerCategoryPathKey(candidate.layer.layerId, candidate.category, path),
candidate,
);
for (const style of ["posix", "windows"] as const)
for (const { path, sourcePath } of candidate.nativeDeclarationPaths[
style
])
appendIndex(
byNativeDeclarationPath,
`${style}\0${layerCategoryPathKey(candidate.layer.layerId, candidate.category, path)}`,
{ candidate, sourcePath },
);
}
return { byCategoryDigest, byLayerCategoryPath, byNativeDeclarationPath };
};
const appendIndex = <Value>(
index: Map<string, Value[]>,
key: string,
candidate: Value,
): void => {
const values = index.get(key);
if (values === undefined) index.set(key, [candidate]);
else values.push(candidate);
};
const categoryDigestKey = (category: string, digest: string): string =>
`${category}\0${digest}`;
const layerCategoryPathKey = (
layerId: string,
category: string,
path: string,
): string => `${layerId}\0${category}\0${path}`;
const matched = (
entity: RuntimeReconciliationEntity,
candidate: StaticRuntimeCandidate,
input: Pick<ItemInput, "basis" | "confidence" | "reason">,
): EvaluatedMatch => {
const item = createItem(entity, {
layer: candidate.layer,
staticNodeId: candidate.node.node_id,
status: "matched",
basis: input.basis,
confidence: input.confidence,
reason: input.reason,
candidates: [candidate],
});
return {
item,
edge: createJavaScriptApplicationEdge({
source_node_id: candidate.node.node_id,
target_node_id: entity.node.node_id,
relation: "observed_as",
properties: {
reconciliation_id: item.reconciliation_id,
basis: input.basis,
static_layer_id: candidate.layer.layerId,
},
evidence: reconciliationEvidence(
entity,
candidate.layer,
input.basis,
input.confidence,
),
}),
};
};
const unresolved = (
entity: RuntimeReconciliationEntity,
input: Pick<ItemInput, "basis" | "confidence" | "reason" | "candidates"> & {
readonly status: Exclude<
JavaScriptRuntimeReconciliationItem["status"],
"matched"
>;
},
): EvaluatedMatch => ({
item: createItem(entity, {
layer: null,
staticNodeId: null,
status: input.status,
basis: input.basis,
confidence: input.confidence,
reason: input.reason,
candidates: input.candidates,
}),
edge: null,
});
type ItemInput = {
readonly basis: JavaScriptRuntimeReconciliationItem["basis"];
readonly confidence: JavaScriptRuntimeReconciliationItem["confidence"];
readonly reason: JavaScriptRuntimeReconciliationItem["reason"];
readonly candidates: readonly StaticRuntimeCandidate[];
} & (
| {
readonly layer: ParsedStaticLayer;
readonly staticNodeId: string;
readonly status: "matched";
}
| {
readonly layer: null;
readonly staticNodeId: null;
readonly status: Exclude<
JavaScriptRuntimeReconciliationItem["status"],
"matched"
>;
}
);
const createItem = (
entity: RuntimeReconciliationEntity,
input: ItemInput,
): JavaScriptRuntimeReconciliationItem => {
const candidateReferences = uniqueCandidateReferences(input.candidates);
const staticMatch =
input.status === "matched"
? {
static_layer_id: input.layer.layerId,
static_node_id: input.staticNodeId,
status: input.status,
}
: {
static_layer_id: null,
static_node_id: null,
status: input.status,
};
const semantic = {
entity_kind: entity.kind,
runtime_evidence_id: entity.capture.evidence.evidence_id,
runtime_node_id: entity.node.node_id,
...staticMatch,
basis: input.basis,
confidence: input.confidence,
reason: input.reason,
candidate_static_count: candidateReferences.count,
candidate_static_nodes: candidateReferences.items,
} satisfies Omit<JavaScriptRuntimeReconciliationItem, "reconciliation_id">;
return {
reconciliation_id: `jrr_item_${digestCanonicalValue(semantic, "Runtime reconciliation")}`,
...semantic,
};
};
const reconciliationEvidence = (
entity: RuntimeReconciliationEntity,
layer: ParsedStaticLayer,
basis: JavaScriptRuntimeReconciliationItem["basis"],
confidence: JavaScriptRuntimeReconciliationItem["confidence"],
): ApplicationGraphEvidence => {
const observation = entity.node.observations.at(0);
if (observation === undefined)
throw new TypeError("Runtime reconciliation entity has no observation");
return {
authority: "cross-layer-reconciliation",
state: "inferred",
confidence: confidence === "medium" ? "medium" : "high",
artifact:
entity.sourceSha256 === null
? {
available: false,
reason: "not-observed",
detail: "Runtime source bytes were not captured for this mapping.",
}
: {
available: true,
artifact_id: `art_${entity.sourceSha256}`,
sha256: entity.sourceSha256,
},
location: observation.evidence.location,
extractor: {
name: "rea-javascript-runtime-reconciliation",
version: "1",
operation: "reconcile_javascript_runtime",
executable_sha256: null,
},
coverage:
layer.graph.coverage.status === "complete" &&
entity.capture.scriptsCompleteWithinScope
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
: {
status: "partial",
truncated: false,
omitted_count: null,
limits: [],
},
limitations: [
basis === "content-sha256" ||
basis === "module-source-sha256" ||
basis === "content-and-location"
? "Equal content digests establish byte identity, not execution causality."
: "Location correspondence is inferred from an artifact root or caller-declared mapping.",
"Passive script parsing or target presence does not prove feature execution.",
],
evidence_ids: [
layer.evidence.evidence_id,
entity.capture.evidence.evidence_id,
].sort(),
};
};
const strongestLocationBasis = (
values: readonly LocatedCandidate[],
): JavaScriptRuntimeReconciliationItem["basis"] => {
const bases = new Set(values.map(({ mapping }) => mapping.basis));
if (bases.has("artifact-path")) return "artifact-path";
if (bases.has("operator-file-mapping")) return "operator-file-mapping";
return "operator-url-mapping";
};
const candidateKey = (candidate: StaticRuntimeCandidate): string =>
`${candidate.layer.layerId}:${candidate.node.node_id}`;
const uniqueCandidateReferences = (
candidates: readonly StaticRuntimeCandidate[],
): {
readonly count: number;
readonly items: JavaScriptRuntimeReconciliationItem["candidate_static_nodes"];
} => {
const seen = new Set<string>();
let count = 0;
const items: JavaScriptRuntimeReconciliationItem["candidate_static_nodes"] =
[];
for (const { layer, node } of candidates) {
const key = `${layer.layerId}:${node.node_id}`;
if (seen.has(key)) continue;
seen.add(key);
count += 1;
items.push({
static_layer_id: layer.layerId,
static_node_id: node.node_id,
});
}
return { count, items };
};