import type { PodGraph, GraphNodeDoc, GraphEdgeDoc } from '@podman/shared'; import { getDb } from '../memory/db.js'; import { createDemoPodGraph } from './demo.js'; import { materializePodGraph } from './live.js'; interface TeamModelDoc { podId: string; graph?: PodGraph; updatedAt?: string; } /** * Load a pod's continual-learning graph. Reads the embedded `team_model.graph`; * falls back to a grounded demo graph when none exists yet or Mongo is * unreachable — so the demo path never depends on a populated DB. */ export async function loadPodGraph(podId: string): Promise { // 1. Live: materialize from the real collections (observations/collisions/…). try { const live = await materializePodGraph(podId); if (live) return live; } catch (err) { console.warn(`[graph] live materialize failed, falling back: ${(err as Error).message}`); } // 2. Seeded snapshot embedded in team_model. try { const db = await getDb(); const doc = await db.collection('team_model').findOne({ podId }); if (doc?.graph) return doc.graph; } catch (err) { console.warn(`[graph] loadPodGraph fell back to demo: ${(err as Error).message}`); } // 3. Demo (stage safety — never an empty canvas). return createDemoPodGraph(podId); } /** * Seed a pod's graph into Mongo: embed it in `team_model` and mirror nodes/edges * into `graph_nodes` / `graph_edges` so `$graphLookup` traversal is real, not a * mock. Idempotent — safe to run repeatedly. */ export async function seedGraph(podId: string): Promise { const db = await getDb(); const graph = createDemoPodGraph(podId); const nodes = db.collection('graph_nodes'); const edges = db.collection('graph_edges'); await Promise.all([ nodes.createIndex({ podId: 1, id: 1 }, { unique: true }), edges.createIndex({ podId: 1, source: 1 }), db.collection('team_model').createIndex({ podId: 1 }, { unique: true }), ]); await db .collection('team_model') .updateOne( { podId }, { $set: { podId, graph, updatedAt: new Date().toISOString() } }, { upsert: true }, ); await Promise.all([nodes.deleteMany({ podId }), edges.deleteMany({ podId })]); if (graph.nodes.length) await nodes.insertMany(graph.nodes.map((n) => ({ ...n, podId }))); if (graph.edges.length) await edges.insertMany(graph.edges.map((e) => ({ ...e, podId }))); return graph; } export interface ReachResult { start: string; reaches: GraphEdgeDoc[]; } /** * Walk the directed edge chain from a node with MongoDB `$graphLookup` — answers * "what does this node's work reach?" (engineer -> file -> collision -> * intervention). This is the graph-database traversal that powers the risk path. */ export async function reachFrom(podId: string, startNodeId: string): Promise { const db = await getDb(); const rows = await db .collection('graph_edges') .aggregate([ { $match: { podId, source: startNodeId } }, { $graphLookup: { from: 'graph_edges', startWith: '$target', connectFromField: 'target', connectToField: 'source', as: 'reaches', restrictSearchWithMatch: { podId }, maxDepth: 6, }, }, ]) .toArray(); // Include the direct outbound edges (the $match rows) plus everything reachable // from them, de-duped by edge id. Without the direct rows, edges straight off // the start node go missing unless a cycle happens to re-discover them. const seen = new Set(); const reaches: GraphEdgeDoc[] = []; for (const row of rows) { const { reaches: recursive, ...direct } = row; for (const edge of [direct as GraphEdgeDoc, ...(recursive ?? [])]) { if (edge?.id && !seen.has(edge.id)) { seen.add(edge.id); reaches.push(edge); } } } return { start: startNodeId, reaches }; }