import { CONTAINS_RELATION, EVIDENCE_RELATION_ID } from "@banes-lab/web/constants/graph.constants"; import type { EdgeIndex, Graph, Merged } from "#types/graph.types"; import type { GraphEdge, GraphNode, RelationPair } from "@banes-lab/web/types/graph.types.js"; import type { EdgeRef } from "@banes-lab/web/types/link.types.js"; import type { ReferenceRelation } from "@banes-lab/web/types/reference.types.js"; import { noReverse } from "#configuration/strings/catalog.strings"; export const inboundRelations = function inboundRelations( graph: Graph, pairs: readonly RelationPair[], ): ReadonlyMap { const titles = new Map(graph.nodes.map((held) => [held.ref, held.title])); const reverses = new Map(pairs.map((pair) => [pair.forward, pair.reverse])); const inbound = new Map>>(); for (const edge of graph.edges) { const reverse = reverses.get(edge.relation); if (reverse === undefined) { throw new Error(noReverse(edge.relation, edge.from)); } const byRelation = inbound.get(edge.to) ?? new Map>(); const sources = byRelation.get(reverse) ?? new Map(); sources.set(edge.from, titles.get(edge.from) ?? edge.from); byRelation.set(reverse, sources); inbound.set(edge.to, byRelation); } return new Map( [...inbound.entries()].map(([ref, byRelation]) => [ ref, [...byRelation.entries()].map(([relation, sources]) => ({ edges: [...sources.entries()].map(([source, label]) => ({ label, ref: source })), relation, })), ]), ); }; export const outboundRelations = function outboundRelations( graph: Graph, ): ReadonlyMap { const titles = new Map(graph.nodes.map((held) => [held.ref, held.title])); const outbound = new Map>>(); for (const edge of graph.edges) { const byRelation = outbound.get(edge.from) ?? new Map>(); const targets = byRelation.get(edge.relation) ?? new Map(); targets.set(edge.to, titles.get(edge.to) ?? edge.to); byRelation.set(edge.relation, targets); outbound.set(edge.from, byRelation); } return new Map( [...outbound.entries()].map(([ref, byRelation]) => [ ref, [...byRelation.entries()].map(([relation, targets]) => ({ edges: [...targets.entries()].map(([target, label]) => ({ label, ref: target })), relation, })), ]), ); }; const indexKey = function indexKey(ref: string, relation: string): string { return `${ref}\n${relation}`; }; export const edgeIndexOf = function edgeIndexOf(graph: Graph): EdgeIndex { const titles = new Map(graph.nodes.map((held) => [held.ref, held.title])); const outgoing = new Map(); const incoming = new Map(); const add = function add(into: Map, key: string, ref: string): void { into.set(key, [...(into.get(key) ?? []), { label: titles.get(ref) ?? ref, ref }]); }; for (const edge of graph.edges) { add(outgoing, indexKey(edge.from, edge.relation), edge.to); add(incoming, indexKey(edge.to, edge.relation), edge.from); } return { incoming: (ref, relation) => incoming.get(indexKey(ref, relation)) ?? [], outgoing: (ref, relation) => outgoing.get(indexKey(ref, relation)) ?? [], }; }; export const groundingSources = function groundingSources( graph: Graph, edges: EdgeIndex, hrefOf: (ref: string) => string | null, ): (ref: string) => readonly EdgeRef[] { const sharingHref = new Map(); for (const node of graph.nodes) { if (node.href !== null) { sharingHref.set(node.href, [...(sharingHref.get(node.href) ?? []), node.ref]); } } return (ref) => { const href = hrefOf(ref); const aliases = href === null ? [] : (sharingHref.get(href) ?? []).filter((alias) => alias !== ref); const held = [ref, ...aliases, ...edges.outgoing(ref, CONTAINS_RELATION).map((edge) => edge.ref ?? "")]; const sources = new Map( held.flatMap((part) => edges.incoming(part, EVIDENCE_RELATION_ID)).map((edge) => [edge.ref, edge.label]), ); return [...sources].map(([source, label]) => ({ label, ref: source })); }; }; const edgeKey = function edgeKey(edge: GraphEdge): string { return `${edge.from}\n${edge.relation}\n${edge.to}`; }; const isDuplicate = function isDuplicate(kept: GraphNode, held: GraphNode): boolean { return kept.kind !== held.kind || kept.layer !== held.layer; }; export const mergeGraphs = function mergeGraphs(parts: readonly Graph[]): Merged { const nodes = new Map(); const edges = new Map(); const duplicates: (readonly [GraphNode, GraphNode])[] = []; for (const part of parts) { for (const held of part.nodes) { const first = nodes.get(held.ref); if (first === undefined) { nodes.set(held.ref, held); continue; } if (isDuplicate(first, held)) { duplicates.push([first, held]); } } for (const edge of part.edges) { edges.set(edgeKey(edge), edge); } } return { duplicates, edges: [...edges.values()], nodes: [...nodes.values()] }; }; export const danglingEdges = function danglingEdges(graph: Graph): readonly GraphEdge[] { const refs = new Set(graph.nodes.map((held) => held.ref)); return graph.edges.filter((edge) => !refs.has(edge.from) || !refs.has(edge.to)); };