Files
podman/backend/src/graph/store.ts
T
sb-iam 85ab8da598 feat(graph): live materializer + light shadcn theme (real-data glue)
- backend/src/graph/live.ts: materializePodGraph builds the graph from the real
  collections (pods, engineer_states, observations, collisions, interventions,
  outcomes) instead of the demo seed. Parses git-status paths, fuses git+vision,
  draws collides/warns/learned_from, computes live metrics + a column layout.
- store.ts: loadPodGraph now prefers live → seeded team_model.graph → demo.
- GraphView.tsx: light/shadcn theme (from the team-memory-theme work), composed
  from the ruixen primitives; node-shape encoding unchanged.
- docs/graph.md: live data→graph mapping + fallback order.

Typechecks clean. Not yet runtime-verified end-to-end (Atlas creds rotated again).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:44:50 -07:00

115 lines
3.9 KiB
TypeScript

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<PodGraph> {
// 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<TeamModelDoc>('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<PodGraph> {
const db = await getDb();
const graph = createDemoPodGraph(podId);
const nodes = db.collection<GraphNodeDoc>('graph_nodes');
const edges = db.collection<GraphEdgeDoc>('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<TeamModelDoc>('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<ReachResult> {
const db = await getDb();
const rows = await db
.collection<GraphEdgeDoc>('graph_edges')
.aggregate<GraphEdgeDoc & { reaches: GraphEdgeDoc[] }>([
{ $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<string>();
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 };
}