mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
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<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),
|
||||
) ?? []) {
|
||||
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<string, StaticRuntimeCandidate[]>();
|
||||
const byLayerCategoryPath = new Map<string, StaticRuntimeCandidate[]>();
|
||||
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<string, StaticRuntimeCandidate[]>,
|
||||
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<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_${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<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 };
|
||||
};
|
||||
Reference in New Issue
Block a user