import { createJavaScriptApplicationEdge, type ApplicationEdge, type ApplicationGraphEvidence, } from "./javascriptApplicationGraph.js"; import { canonicalDigest } from "../comparisonSemantics.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; import { compareCodePoints } from "../canonicalOrdering.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) => compareCodePoints( 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 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(); 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), ) ?? []) { const key = candidateKey(candidate); if (!located.has(key)) located.set(key, { candidate, mapping }); } return [...located.values()].sort((left, right) => compareCodePoints( candidateKey(left.candidate), candidateKey(right.candidate), ), ); }; const indexCandidates = ( candidates: readonly StaticRuntimeCandidate[], ): CandidateIndex => { const byCategoryDigest = new Map(); const byLayerCategoryPath = new Map(); 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, ); } return { byCategoryDigest, byLayerCategoryPath }; }; const appendIndex = ( index: Map, key: string, candidate: StaticRuntimeCandidate, ): 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, ): 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 & { 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; return { reconciliation_id: `jrr_item_${canonicalDigest(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(); 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 }; };