mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
cf346368c4
Append overlapping observations to node buckets instead of recopying groups, and share one node index across semantic seed admission and traversal. Preserve merged provenance and absent-seed behavior.
255 lines
8.7 KiB
TypeScript
255 lines
8.7 KiB
TypeScript
import {
|
|
createJavaScriptApplicationGraph,
|
|
createJavaScriptApplicationNode,
|
|
type JavaScriptApplicationGraph,
|
|
} from "./javascriptApplicationGraph.js";
|
|
import type {
|
|
ApplicationEdge,
|
|
ApplicationNode,
|
|
} from "./javascriptApplicationGraphSchemas.js";
|
|
import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js";
|
|
import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationCaptureParsing.js";
|
|
import { uniqueSorted } from "../canonicalOrdering.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import type { RuntimeProjection } from "./javascriptRuntimeReconciliationRuntime.js";
|
|
|
|
interface ReconciledGraphInput {
|
|
readonly layers: readonly ParsedStaticLayer[];
|
|
readonly captures: readonly ParsedRuntimeCapture[];
|
|
readonly runtime: RuntimeProjection;
|
|
readonly reconciliationEdges: readonly ApplicationEdge[];
|
|
readonly omittedReconciliationItems: number;
|
|
}
|
|
|
|
interface GraphCoverageFacts {
|
|
readonly projectionOmissions: number;
|
|
readonly knownOmissions: number;
|
|
readonly omissionsKnown: boolean;
|
|
readonly inputStatus: "complete" | "partial" | "truncated";
|
|
readonly outputTruncated: boolean;
|
|
}
|
|
|
|
/** Combined graph plus omissions introduced while merging its observations. */
|
|
export interface ReconciledApplicationGraphProjection {
|
|
readonly graph: JavaScriptApplicationGraph;
|
|
readonly omittedGraphItems: number;
|
|
}
|
|
|
|
/** Merge source graphs, runtime nodes, and inferred mappings into one valid JAG. */
|
|
export const buildReconciledApplicationGraph = (
|
|
input: ReconciledGraphInput,
|
|
): ReconciledApplicationGraphProjection => {
|
|
const merged = mergeNodes([
|
|
...input.layers.flatMap(({ graph }) => graph.nodes),
|
|
...input.runtime.nodes,
|
|
]);
|
|
const allEdges = uniqueEdges([
|
|
...input.layers.flatMap(({ graph }) => graph.edges),
|
|
...input.runtime.edges,
|
|
...input.reconciliationEdges,
|
|
]);
|
|
const bounded = boundGraphContent(input, merged, allEdges);
|
|
const facts = graphCoverageFacts(input, bounded.omittedItems);
|
|
const graph = createJavaScriptApplicationGraph({
|
|
schema: "JavaScriptApplicationGraph",
|
|
root_node_ids: bounded.rootNodeIds,
|
|
nodes: bounded.nodes,
|
|
edges: bounded.edges,
|
|
coverage: graphCoverage(input, facts, bounded.nodes.length),
|
|
limitations: uniqueSorted([
|
|
...input.layers.flatMap(({ graph }) => graph.limitations),
|
|
"Runtime presence is bounded to passive capture windows and does not prove feature execution.",
|
|
"Cross-layer observed_as edges are inferences; digest equality proves byte identity only.",
|
|
"Source-map relationships retain their original static authority and are not promoted into runtime matches.",
|
|
...(facts.inputStatus === "truncated"
|
|
? [
|
|
"One or more source graphs or passive captures were incomplete or truncated.",
|
|
]
|
|
: []),
|
|
...(facts.outputTruncated
|
|
? [
|
|
"The reconciled graph omitted entities or classifications during projection.",
|
|
]
|
|
: []),
|
|
]),
|
|
});
|
|
return {
|
|
graph,
|
|
omittedGraphItems: bounded.omittedItems,
|
|
};
|
|
};
|
|
|
|
const boundGraphContent = (
|
|
input: ReconciledGraphInput,
|
|
nodes: readonly ApplicationNode[],
|
|
edges: readonly ApplicationEdge[],
|
|
): {
|
|
readonly rootNodeIds: string[];
|
|
readonly nodes: ApplicationNode[];
|
|
readonly edges: ApplicationEdge[];
|
|
readonly omittedItems: number;
|
|
} => {
|
|
const allRoots = preferredRootIds(input);
|
|
const rootNodeIds = allRoots;
|
|
const sortedNodes = [...nodes].sort((left, right) =>
|
|
compareUnicodeCodePoints(left.node_id, right.node_id),
|
|
);
|
|
const retainedNodes = sortedNodes;
|
|
const retainedIds = new Set(retainedNodes.map(({ node_id: id }) => id));
|
|
const retainedEdges = [...edges]
|
|
.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.edge_id, right.edge_id),
|
|
)
|
|
.filter(
|
|
({ source_node_id: source, target_node_id: target }) =>
|
|
retainedIds.has(source) && retainedIds.has(target),
|
|
);
|
|
return {
|
|
rootNodeIds,
|
|
nodes: retainedNodes,
|
|
edges: retainedEdges,
|
|
omittedItems: 0,
|
|
};
|
|
};
|
|
|
|
const preferredRootIds = (input: ReconciledGraphInput): string[] => {
|
|
const runtime = [...input.runtime.targetNodeByEvidenceId.values()].map(
|
|
({ node_id: id }) => id,
|
|
);
|
|
const application = input.layers
|
|
.filter(({ role }) => role === "application")
|
|
.flatMap(({ graph }) => graph.root_node_ids);
|
|
const supplemental = input.layers
|
|
.filter(({ role }) => role !== "application")
|
|
.flatMap(({ graph }) => graph.root_node_ids);
|
|
return [...new Set([...runtime, ...application, ...supplemental])];
|
|
};
|
|
|
|
const graphCoverageFacts = (
|
|
input: ReconciledGraphInput,
|
|
omittedNodeObservations: number,
|
|
): GraphCoverageFacts => {
|
|
const projectionOmissions =
|
|
input.runtime.omittedEntities +
|
|
input.omittedReconciliationItems +
|
|
omittedNodeObservations;
|
|
const captureTruncated = input.captures.some(({ inspection }) =>
|
|
inspection.completeness.conditions.includes("truncated"),
|
|
);
|
|
const captureHasUnknownOmissions = input.captures.some(
|
|
({ inspection }) => inspection.completeness.unavailable_sections.length > 0,
|
|
);
|
|
const inputTruncated =
|
|
input.layers.some(({ graph }) => graph.coverage.truncated) ||
|
|
captureTruncated;
|
|
const inputComplete =
|
|
input.layers.every(({ graph }) => graph.coverage.status === "complete") &&
|
|
input.captures.every(
|
|
({ inspection }) =>
|
|
inspection.completeness.status === "complete_within_window",
|
|
);
|
|
const outputTruncated = projectionOmissions > 0;
|
|
const sourceOmissions = input.layers.reduce(
|
|
(total, { graph }) => total + (graph.coverage.omitted_count ?? 0),
|
|
0,
|
|
);
|
|
const omissionsKnown =
|
|
!captureTruncated &&
|
|
!captureHasUnknownOmissions &&
|
|
input.layers.every(({ graph }) => graph.coverage.omitted_count !== null);
|
|
return {
|
|
projectionOmissions,
|
|
knownOmissions: projectionOmissions + sourceOmissions,
|
|
omissionsKnown,
|
|
inputStatus: inputTruncated
|
|
? "truncated"
|
|
: inputComplete
|
|
? "complete"
|
|
: "partial",
|
|
outputTruncated,
|
|
};
|
|
};
|
|
|
|
const graphCoverage = (
|
|
input: ReconciledGraphInput,
|
|
facts: GraphCoverageFacts,
|
|
nodeCount: number,
|
|
): JavaScriptApplicationGraph["coverage"] =>
|
|
facts.inputStatus === "complete" && !facts.outputTruncated
|
|
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
|
|
: facts.inputStatus === "truncated" || facts.outputTruncated
|
|
? {
|
|
status: "partial",
|
|
truncated: true,
|
|
omitted_count: facts.omissionsKnown ? facts.knownOmissions : null,
|
|
limits: [
|
|
...(facts.inputStatus === "truncated"
|
|
? [
|
|
{
|
|
name: "runtime-reconciliation-input-coverage",
|
|
value: input.layers.length + input.captures.length,
|
|
unit: "items" as const,
|
|
},
|
|
]
|
|
: []),
|
|
...(facts.outputTruncated
|
|
? [
|
|
{
|
|
name: "runtime-reconciliation-output-limits",
|
|
value: nodeCount,
|
|
unit: "items" as const,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
}
|
|
: {
|
|
status: "partial",
|
|
truncated: false,
|
|
omitted_count: facts.omissionsKnown ? facts.knownOmissions : null,
|
|
limits: [],
|
|
};
|
|
|
|
const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => {
|
|
const grouped = new Map<string, ApplicationNode[]>();
|
|
for (const node of nodes) {
|
|
const group = grouped.get(node.node_id);
|
|
if (group === undefined) grouped.set(node.node_id, [node]);
|
|
else group.push(node);
|
|
}
|
|
const merged = [...grouped.values()].map((group) => {
|
|
const first = group[0];
|
|
if (first === undefined) throw new TypeError("Empty JAG node group");
|
|
if (
|
|
group.some(
|
|
({ kind, identity }) =>
|
|
kind !== first.kind ||
|
|
JSON.stringify(identity) !== JSON.stringify(first.identity),
|
|
)
|
|
)
|
|
throw new TypeError("Colliding JAG node identifiers disagree");
|
|
const observations = [
|
|
...new Map(
|
|
group
|
|
.flatMap(({ observations: values }) => values)
|
|
.map((observation) => [observation.observation_id, observation]),
|
|
).values(),
|
|
].sort((left, right) =>
|
|
compareUnicodeCodePoints(left.observation_id, right.observation_id),
|
|
);
|
|
return createJavaScriptApplicationNode({
|
|
kind: first.kind,
|
|
identity: first.identity,
|
|
observations: observations.map(
|
|
({ observation_id: _id, identifier_strategy: _strategy, ...value }) =>
|
|
value,
|
|
),
|
|
});
|
|
});
|
|
return merged;
|
|
};
|
|
|
|
const uniqueEdges = (edges: readonly ApplicationEdge[]): ApplicationEdge[] => [
|
|
...new Map(edges.map((edge) => [edge.edge_id, edge])).values(),
|
|
];
|