mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
765e809e36
Remove forwarding wrappers and shadow aliases, migrate callers and verification scripts to their owners, and share one exhaustive semantic resource-bound projection.
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import type {
|
|
ApplicationEdge,
|
|
ApplicationNode,
|
|
} from "./javascriptApplicationGraphSchemas.js";
|
|
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
|
|
interface AdjacencyEntry {
|
|
readonly edge: ApplicationEdge;
|
|
readonly nextNodeId: string;
|
|
}
|
|
|
|
interface Predecessor {
|
|
readonly previousNodeId: string;
|
|
readonly edgeId: string;
|
|
}
|
|
|
|
/** Complete reachable graph projection plus the first deterministic path to each node. */
|
|
export interface ApplicationFeatureTraversal {
|
|
readonly nodes: ApplicationNode[];
|
|
readonly edges: ApplicationEdge[];
|
|
readonly predecessors: ReadonlyMap<string, Predecessor>;
|
|
}
|
|
|
|
/** Traverse a JAG without treating edge inference as observed reachability. */
|
|
export const traverseApplicationFeature = (
|
|
graph: JavaScriptApplicationGraph,
|
|
seedNodeIds: readonly string[],
|
|
direction: "outgoing" | "incoming" | "both",
|
|
): ApplicationFeatureTraversal => {
|
|
const nodeById = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
const adjacency = buildAdjacency(graph.edges, direction);
|
|
const visited = new Set(seedNodeIds);
|
|
const retainedEdges = new Map<string, ApplicationEdge>();
|
|
const predecessors = new Map<string, Predecessor>();
|
|
const queue = [...seedNodeIds];
|
|
for (let offset = 0; offset < queue.length; offset += 1) {
|
|
const current = queue[offset];
|
|
if (current === undefined) continue;
|
|
for (const entry of adjacency.get(current) ?? []) {
|
|
retainedEdges.set(entry.edge.edge_id, entry.edge);
|
|
if (visited.has(entry.nextNodeId)) continue;
|
|
if (!nodeById.has(entry.nextNodeId)) continue;
|
|
visited.add(entry.nextNodeId);
|
|
predecessors.set(entry.nextNodeId, {
|
|
previousNodeId: current,
|
|
edgeId: entry.edge.edge_id,
|
|
});
|
|
queue.push(entry.nextNodeId);
|
|
}
|
|
}
|
|
return {
|
|
nodes: [...visited]
|
|
.map((nodeId) => nodeById.get(nodeId))
|
|
.filter((node): node is ApplicationNode => node !== undefined)
|
|
.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.node_id, right.node_id),
|
|
),
|
|
edges: [...retainedEdges.values()].sort((left, right) =>
|
|
compareUnicodeCodePoints(left.edge_id, right.edge_id),
|
|
),
|
|
predecessors,
|
|
};
|
|
};
|
|
|
|
const buildAdjacency = (
|
|
edges: readonly ApplicationEdge[],
|
|
direction: "outgoing" | "incoming" | "both",
|
|
): ReadonlyMap<string, AdjacencyEntry[]> => {
|
|
const adjacency = new Map<string, AdjacencyEntry[]>();
|
|
for (const edge of edges) {
|
|
if (direction !== "incoming")
|
|
addAdjacency(adjacency, edge.source_node_id, {
|
|
edge,
|
|
nextNodeId: edge.target_node_id,
|
|
});
|
|
if (direction !== "outgoing")
|
|
addAdjacency(adjacency, edge.target_node_id, {
|
|
edge,
|
|
nextNodeId: edge.source_node_id,
|
|
});
|
|
}
|
|
for (const entries of adjacency.values())
|
|
entries.sort((left, right) =>
|
|
compareUnicodeCodePoints(
|
|
`${left.edge.edge_id}\0${left.nextNodeId}`,
|
|
`${right.edge.edge_id}\0${right.nextNodeId}`,
|
|
),
|
|
);
|
|
return adjacency;
|
|
};
|
|
|
|
const addAdjacency = (
|
|
adjacency: Map<string, AdjacencyEntry[]>,
|
|
nodeId: string,
|
|
entry: AdjacencyEntry,
|
|
): void => {
|
|
const entries = adjacency.get(nodeId);
|
|
if (entries === undefined) adjacency.set(nodeId, [entry]);
|
|
else entries.push(entry);
|
|
};
|