From 546aeeed70ba27516b9ff356a3528fc7bdf2f38b Mon Sep 17 00:00:00 2001 From: sb-iam <59984144+sb-iam@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:34:02 -0700 Subject: [PATCH] feat(graph): continual-learning graph data model + dark-Bauhaus viz Adds the team_model graph layer (the "it learned" view) and its visualization, per docs/graph.md. Demo-backed first; built on origin/main conventions. - shared: PodGraph / node / edge / metric types + GraphNodeDoc / GraphEdgeDoc - backend/src/graph: demo graph data; Mongo store (loadPodGraph w/ demo fallback, seedGraph, reachFrom via $graphLookup); graph:seed script - backend/src/server.ts: additive GET /api/pods/:id/graph + /graph/reach/:node - frontend: dark-Bauhaus GraphView + fetch helper, reachable from a "Team memory" toggle in App.tsx - docs/graph.md: spec (data model, $graphLookup, API, demo-first plan) Demo-backed: the route serves a grounded demo graph when team_model has no graph yet; graph:seed writes team_model + graph_nodes/graph_edges so the $graphLookup traversal is real. Swap loadPodGraph to live team_model later. Known follow-ups before merge: branch is based on 65a0791; origin/main is now at 1294b84, so this needs a rebase + App.tsx conflict resolution (teammate rewrote App.tsx with live room view / one-click join). Co-Authored-By: Claude Opus 4.8 --- backend/package.json | 1 + backend/src/graph/demo.ts | 250 ++++++++++++++++++++ backend/src/graph/seed.ts | 19 ++ backend/src/graph/store.ts | 88 +++++++ backend/src/server.ts | 18 ++ docs/graph.md | 88 +++++++ frontend/src/App.tsx | 26 ++- frontend/src/components/GraphView.tsx | 319 ++++++++++++++++++++++++++ frontend/src/lib/graph.ts | 10 + shared/src/graph.ts | 69 ++++++ shared/src/index.ts | 11 + 11 files changed, 892 insertions(+), 7 deletions(-) create mode 100644 backend/src/graph/demo.ts create mode 100644 backend/src/graph/seed.ts create mode 100644 backend/src/graph/store.ts create mode 100644 docs/graph.md create mode 100644 frontend/src/components/GraphView.tsx create mode 100644 frontend/src/lib/graph.ts create mode 100644 shared/src/graph.ts diff --git a/backend/package.json b/backend/package.json index 5c34139..48c511d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,6 +8,7 @@ "dev": "tsx watch src/server.ts", "dev:server": "tsx watch src/server.ts", "dev:agent": "tsx watch src/agent.ts", + "graph:seed": "tsx src/graph/seed.ts", "start": "node dist/server.js", "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit" diff --git a/backend/src/graph/demo.ts b/backend/src/graph/demo.ts new file mode 100644 index 0000000..57a0e32 --- /dev/null +++ b/backend/src/graph/demo.ts @@ -0,0 +1,250 @@ +import type { PodGraph } from '@podman/shared'; + +/** + * Demo continual-learning graph, grounded in the demo-pod crew. Used as the + * served graph until the ingest pipeline populates `team_model.graph`, and as + * the seed for the `graph_nodes` / `graph_edges` collections. The hero path — + * Karti + Yahya editing auth.ts -> collision -> sync PR -> *learned: Karti owns + * auth* — is the continual-learning story the demo lights up. + */ +export function createDemoPodGraph(podId: string): PodGraph { + return { + podId, + generatedAt: new Date().toISOString(), + metrics: [ + { + label: 'Learned owners', + value: '5', + detail: 'Ownership edges retained from accepted interventions.', + }, + { + label: 'Open risk paths', + value: '2', + detail: 'auth.ts and the memory API have converging editors.', + }, + { + label: 'Accept rate', + value: '86%', + detail: 'Interventions accepted this session (+14%).', + }, + ], + nodes: [ + { + id: 'engineer:shakthi', + kind: 'engineer', + label: 'Shakthi', + summary: 'Owns the team-memory graph + visualization.', + weight: 0.8, + status: 'active', + x: 78, + y: 70, + }, + { + id: 'engineer:karti', + kind: 'engineer', + label: 'Karti', + summary: 'Learned owner of auth; backend + DB wiring.', + weight: 0.9, + status: 'learned', + x: 78, + y: 188, + }, + { + id: 'engineer:yahya', + kind: 'engineer', + label: 'Yahya', + summary: 'Editing auth.ts with unpushed changes.', + weight: 0.82, + status: 'risk', + x: 78, + y: 300, + }, + { + id: 'engineer:ramis', + kind: 'engineer', + label: 'Ramis', + summary: 'On the memory store + agent pipeline.', + weight: 0.66, + status: 'stable', + x: 78, + y: 404, + }, + { + id: 'engineer:zander', + kind: 'engineer', + label: 'Zander', + summary: 'Owns the realtime pod + capture.', + weight: 0.6, + status: 'stable', + x: 214, + y: 440, + }, + { + id: 'file:auth.ts', + kind: 'file', + label: 'auth.ts', + summary: 'Hot file — two live editors before push.', + weight: 0.92, + status: 'risk', + x: 300, + y: 150, + }, + { + id: 'file:memory-store', + kind: 'file', + label: 'memory/store', + summary: 'Shared memory API surface.', + weight: 0.72, + status: 'active', + x: 250, + y: 330, + }, + { + id: 'feature:continual-memory', + kind: 'feature', + label: 'continual memory', + summary: 'Atlas team_model + Voyage recall.', + weight: 1, + status: 'active', + x: 442, + y: 300, + }, + { + id: 'feature:realtime-pod', + kind: 'feature', + label: 'realtime pod', + summary: 'LiveKit screen stream in.', + weight: 0.8, + status: 'active', + x: 360, + y: 418, + }, + { + id: 'collision:auth', + kind: 'collision', + label: 'auth.ts overlap', + summary: 'Karti + Yahya editing auth.ts, unpushed — git cannot see it.', + weight: 0.95, + status: 'risk', + x: 440, + y: 118, + }, + { + id: 'intervention:sync-pr', + kind: 'intervention', + label: 'sync PR', + summary: 'PodMan offered to open a draft sync PR.', + weight: 0.7, + status: 'learned', + x: 622, + y: 118, + }, + ], + edges: [ + { + id: 'e1', + source: 'engineer:karti', + target: 'file:auth.ts', + kind: 'owns', + label: 'owns', + strength: 0.9, + }, + { + id: 'e2', + source: 'engineer:yahya', + target: 'file:auth.ts', + kind: 'editing', + label: 'edits', + strength: 0.8, + }, + { + id: 'e3', + source: 'engineer:karti', + target: 'collision:auth', + kind: 'collides', + label: 'in', + strength: 0.85, + }, + { + id: 'e4', + source: 'engineer:yahya', + target: 'collision:auth', + kind: 'collides', + label: 'in', + strength: 0.85, + }, + { + id: 'e5', + source: 'file:auth.ts', + target: 'collision:auth', + kind: 'touches', + label: 'hot', + strength: 0.7, + }, + { + id: 'e6', + source: 'collision:auth', + target: 'intervention:sync-pr', + kind: 'warns', + label: 'nudges', + strength: 0.9, + }, + { + id: 'e7', + source: 'intervention:sync-pr', + target: 'engineer:karti', + kind: 'learned_from', + label: 'learned: owns auth', + strength: 0.6, + }, + { + id: 'e8', + source: 'engineer:ramis', + target: 'file:memory-store', + kind: 'editing', + label: 'edits', + strength: 0.7, + }, + { + id: 'e9', + source: 'engineer:karti', + target: 'file:memory-store', + kind: 'editing', + label: 'edits', + strength: 0.5, + }, + { + id: 'e10', + source: 'file:memory-store', + target: 'feature:continual-memory', + kind: 'touches', + label: 'persists', + strength: 0.8, + }, + { + id: 'e11', + source: 'engineer:shakthi', + target: 'feature:continual-memory', + kind: 'owns', + label: 'models', + strength: 0.85, + }, + { + id: 'e12', + source: 'engineer:zander', + target: 'feature:realtime-pod', + kind: 'owns', + label: 'owns', + strength: 0.75, + }, + { + id: 'e13', + source: 'feature:realtime-pod', + target: 'file:memory-store', + kind: 'touches', + label: 'feeds', + strength: 0.55, + }, + ], + }; +} diff --git a/backend/src/graph/seed.ts b/backend/src/graph/seed.ts new file mode 100644 index 0000000..9fb0244 --- /dev/null +++ b/backend/src/graph/seed.ts @@ -0,0 +1,19 @@ +import { seedGraph } from './store.js'; + +/** + * Seed a pod's continual-learning graph into Mongo (team_model + graph_nodes + + * graph_edges). Usage: `pnpm graph:seed [podId]` (defaults to demo-pod). + */ +const podId = process.argv[2] ?? 'demo-pod'; + +seedGraph(podId) + .then((graph) => { + console.log( + `[graph] seeded ${graph.nodes.length} nodes / ${graph.edges.length} edges for pod "${podId}"`, + ); + process.exit(0); + }) + .catch((err: unknown) => { + console.error(`[graph] seed failed: ${(err as Error).message}`); + process.exit(1); + }); diff --git a/backend/src/graph/store.ts b/backend/src/graph/store.ts new file mode 100644 index 0000000..a8fe7a7 --- /dev/null +++ b/backend/src/graph/store.ts @@ -0,0 +1,88 @@ +import type { PodGraph, GraphNodeDoc, GraphEdgeDoc } from '@podman/shared'; +import { getDb } from '../memory/db.js'; +import { createDemoPodGraph } from './demo.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 { + 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}`); + } + 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<{ reaches: GraphEdgeDoc[] }>([ + { $match: { podId, source: startNodeId } }, + { + $graphLookup: { + from: 'graph_edges', + startWith: '$target', + connectFromField: 'target', + connectToField: 'source', + as: 'reaches', + restrictSearchWithMatch: { podId }, + }, + }, + ]) + .toArray(); + return { start: startNodeId, reaches: rows.flatMap((r) => r.reaches) }; +} diff --git a/backend/src/server.ts b/backend/src/server.ts index a594b6a..ba60d2d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -18,6 +18,7 @@ import { seedDefaultPods, } from './pods/store.js'; import { getPresence } from './livekit/rooms.js'; +import { loadPodGraph, reachFrom } from './graph/store.js'; import type { InterventionOutcome } from '@podman/shared'; const app = express(); @@ -137,6 +138,23 @@ app.delete('/api/pods/:id/members/:name', async (req, res) => { res.json(pod); }); +// --- Continual-learning graph (team_model view) --- +app.get('/api/pods/:id/graph', async (req, res) => { + try { + res.json(await loadPodGraph(req.params.id)); + } catch (e) { + res.status(500).json({ error: (e as Error).message }); + } +}); + +app.get('/api/pods/:id/graph/reach/:node', async (req, res) => { + try { + res.json(await reachFrom(req.params.id, req.params.node)); + } catch (e) { + res.status(500).json({ error: (e as Error).message }); + } +}); + const http = createServer(app); // ws relay: the agent pushes collision/intervention JSON here; PWAs subscribed by pod receive it. diff --git a/docs/graph.md b/docs/graph.md new file mode 100644 index 0000000..c0f7956 --- /dev/null +++ b/docs/graph.md @@ -0,0 +1,88 @@ +# Continual-Learning Graph Spec + +> Owner: graph data + visualization. Status: demo-backed (live `team_model` reads land later). +> Satisfies the documentation-first gate for the `backend/src/graph/*` and +> `frontend/src/components/GraphView.tsx` files. + +## What this is (and is NOT) + +PodMan's **visible "it learned" surface**. The graph is a render of the per-pod +`team_model` — who **owns / edits** which files, where work **collides**, and what +PodMan **learned** from accepted interventions (the `learned_from` edges). It is the +continual-learning loop made legible in 10 seconds: _"PodMan now knows Karti owns auth."_ + +It is **not** a generic analytics dashboard (judges down-rank dashboard-as-product). It is a +**secondary view** behind the pods list — opened from a header toggle — that exists to make +the self-improving loop _visible_ during the demo. The landing surface stays the pods list. + +## Data model — graph as a view of `team_model` + +The graph lives in two places, both keyed by `podId`: + +1. **Embedded (served to the viz):** the `team_model` document carries a `graph` field: + + ```ts + // team_model doc (one per pod, unique index { podId: 1 }) + { podId, graph: PodGraph, updatedAt } + ``` + + `GET /api/pods/:podId/graph` returns `team_model.graph`, or a demo graph when none exists yet. + +2. **Normalized (for traversal):** the same nodes/edges are mirrored into two collections so + the model can be walked with MongoDB `$graphLookup` (the graph-database pattern): + + | Collection | Doc shape (`shared/src/graph.ts`) | Index | + | ------------- | ----------------------------------------- | ---------------------------- | + | `graph_nodes` | `GraphNodeDoc` = `PodGraphNode` + `podId` | `{ podId: 1, id: 1 }` unique | + | `graph_edges` | `GraphEdgeDoc` = `PodGraphEdge` + `podId` | `{ podId: 1, source: 1 }` | + +Node kinds: `engineer · feature · file · collision · intervention`. +Edge kinds: `owns · editing · touches · collides · warns · learned_from`. +The `learned_from` edges are the continual-learning signal — derived from accepted +`interventions` / `outcomes` (ownership PodMan retains across sessions). + +### Traversal (`$graphLookup`) + +Walk the directed edge chain from any node (e.g. engineer → file → collision → intervention): + +```ts +db.collection('graph_edges').aggregate([ + { $match: { podId, source: startNodeId } }, + { + $graphLookup: { + from: 'graph_edges', + startWith: '$target', + connectFromField: 'target', + connectToField: 'source', + as: 'reaches', + restrictSearchWithMatch: { podId }, + }, + }, +]); +``` + +This answers "what does this engineer's edit reach?" — the risk path PodMan lights up. + +## API + +| Method | Route | Returns | +| ------ | ---------------------------------- | --------------------------------------------- | +| `GET` | `/api/pods/:podId/graph` | `PodGraph` (live `team_model`, demo fallback) | +| `GET` | `/api/pods/:podId/graph/reach/:id` | `$graphLookup` reachability from node `:id` | + +Additive routes in `backend/src/server.ts` (shared file — additive only). + +## Files + +- `shared/src/graph.ts` — `PodGraph`, `PodGraphNode/Edge/Metric`, `GraphNodeDoc`, `GraphEdgeDoc` +- `backend/src/graph/demo.ts` — `createDemoPodGraph(podId)` (grounded in the demo-pod crew) +- `backend/src/graph/store.ts` — `loadPodGraph`, `seedGraph`, `reachFrom` (`$graphLookup`) +- `backend/src/graph/seed.ts` — `pnpm graph:seed` (writes demo into `team_model` + graph collections) +- `frontend/src/lib/graph.ts` — `fetchPodGraph(podId)` +- `frontend/src/components/GraphView.tsx` — dark-Bauhaus SVG graph (toggle from `App.tsx`) + +## Demo-first plan + +1. Serve `createDemoPodGraph()` from the route (demo-stable, no DB dependency on the demo path). +2. `pnpm graph:seed` writes the same graph into Mongo so `$graphLookup` is real, not a mock. +3. Swap `loadPodGraph` to read live `team_model.graph` once the ingest pipeline populates it. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3ca1614..6f98e4f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import * as api from './lib/api.js'; import { PodCard } from './components/PodCard.js'; import { CreatePodForm } from './components/CreatePodForm.js'; import { PodView } from './components/PodView.js'; +import { GraphView } from './components/GraphView.js'; const SESSION_KEY = 'podman.session'; @@ -22,6 +23,7 @@ export default function App() { const [devMode, setDevMode] = useState(false); const [room, setRoom] = useState(null); const [restoring, setRestoring] = useState(false); + const [graphPodId, setGraphPodId] = useState(null); const joinedPod = joinedPodId ? (pods.find((p) => p.id === joinedPodId) ?? null) : null; @@ -209,19 +211,29 @@ export default function App() { Cancel + ) : graphPodId ? ( + setGraphPodId(null)} /> ) : (

Pods {loading ? '…' : `(${pods.length})`}

- +
+ + +
{error && ( diff --git a/frontend/src/components/GraphView.tsx b/frontend/src/components/GraphView.tsx new file mode 100644 index 0000000..66254fc --- /dev/null +++ b/frontend/src/components/GraphView.tsx @@ -0,0 +1,319 @@ +import { useEffect, useMemo, useState, type CSSProperties } from 'react'; +import type { PodGraph, PodGraphNode, PodGraphEdge, PodGraphNodeKind } from '@podman/shared'; +import { fetchPodGraph } from '../lib/graph.js'; + +type Mode = 'risk' | 'learn' | 'all'; + +const KIND_COLOR: Record = { + engineer: '#3B5BFF', + file: '#ECE7DA', + feature: '#F6C445', + collision: '#E2403A', + intervention: '#8b6cff', +}; + +const EDGE: Record = { + owns: { c: '#3B5BFF', w: 2.6 }, + editing: { c: '#ECE7DA', w: 2 }, + touches: { c: '#5d5d66', w: 1.6 }, + collides: { c: '#E2403A', w: 3.2 }, + warns: { c: '#F6C445', w: 3.2 }, + learned_from: { c: '#8b6cff', w: 2.4, dash: true }, +}; + +function NodeShape({ node }: { node: PodGraphNode }) { + const c = KIND_COLOR[node.kind]; + const { x, y } = node; + switch (node.kind) { + case 'engineer': + return ; + case 'file': + return ( + + ); + case 'feature': + return ; + case 'collision': + return ; + case 'intervention': + return ( + + ); + default: + return null; + } +} + +interface Highlight { + nodes: Set; + edges: Set; +} + +function highlightFor(graph: PodGraph, mode: Mode, selected: string | null): Highlight | null { + if (selected) { + const es = graph.edges.filter((e) => e.source === selected || e.target === selected); + return { + nodes: new Set([selected, ...es.flatMap((e) => [e.source, e.target])]), + edges: new Set(es.map((e) => e.id)), + }; + } + if (mode === 'all') return null; + const kinds: PodGraphEdge['kind'][] = + mode === 'risk' ? ['collides', 'warns', 'learned_from'] : ['learned_from', 'warns']; + const collisions = new Set(graph.nodes.filter((n) => n.kind === 'collision').map((n) => n.id)); + const es = graph.edges.filter( + (e) => + kinds.includes(e.kind) || + (mode === 'risk' && (collisions.has(e.target) || collisions.has(e.source))), + ); + return { + nodes: new Set(es.flatMap((e) => [e.source, e.target])), + edges: new Set(es.map((e) => e.id)), + }; +} + +const LEGEND: Array<{ label: string; swatch: CSSProperties }> = [ + { label: 'engineer', swatch: { background: '#3B5BFF' } }, + { label: 'file', swatch: { border: '2px solid #ECE7DA' } }, + { label: 'feature', swatch: { background: '#F6C445', borderRadius: '50%' } }, + { + label: 'collision', + swatch: { background: '#E2403A', clipPath: 'polygon(50% 0,100% 100%,0 100%)' }, + }, + { label: 'intervention', swatch: { background: '#8b6cff', transform: 'rotate(45deg)' } }, +]; + +export function GraphView({ podId, onClose }: { podId: string; onClose: () => void }) { + const [graph, setGraph] = useState(null); + const [error, setError] = useState(null); + const [mode, setMode] = useState('risk'); + const [selected, setSelected] = useState(null); + + useEffect(() => { + let alive = true; + setGraph(null); + setError(null); + fetchPodGraph(podId) + .then((g) => alive && setGraph(g)) + .catch((e: unknown) => alive && setError(e instanceof Error ? e.message : String(e))); + return () => { + alive = false; + }; + }, [podId]); + + const hi = useMemo( + () => (graph ? highlightFor(graph, mode, selected) : null), + [graph, mode, selected], + ); + const nodeById = useMemo(() => new Map((graph?.nodes ?? []).map((n) => [n.id, n])), [graph]); + const sel = selected ? nodeById.get(selected) : undefined; + const relCount = selected + ? (graph?.edges ?? []).filter((e) => e.source === selected || e.target === selected).length + : 0; + + const dimNode = (id: string) => (hi ? !hi.nodes.has(id) : false); + const dimEdge = (id: string) => (hi ? !hi.edges.has(id) : false); + const hotEdge = (id: string) => (hi ? hi.edges.has(id) : false); + + function pick(next: Mode) { + setMode(next); + setSelected(null); + } + + return ( +
+ + +
+
+
Team memory
+
What PodMan learned · {podId}
+
+ +
+ +
+ + + +
+ + {error && ( +

Graph error: {error}

+ )} + {!graph && !error && ( +

Loading graph…

+ )} + + {graph && ( + <> +
+
+
Workflow metrics
+ {graph.metrics.map((m) => ( +
+
{m.value}
+
{m.label}
+
{m.detail}
+
+ ))} +
+ +
+ + {graph.edges.map((e) => { + const a = nodeById.get(e.source); + const b = nodeById.get(e.target); + if (!a || !b) return null; + const s = EDGE[e.kind]; + return ( + + ); + })} + {graph.nodes.map((n) => ( + setSelected((cur) => (cur === n.id ? null : n.id))} + > + + + {n.label.toUpperCase()} + + + ))} + +
+ +
+ {sel ? ( + <> +
{sel.kind}
+
{sel.label}
+
+ Status + + {sel.status} + +
+
+ Relationships + {relCount} +
+
{sel.summary}
+ + ) : ( + <> +
Continual learning
+
It learned
+
+ Violet learned_from edges are ownership + PodMan retained from accepted interventions — the graph gets sharper every + session. Click any node to trace its relationships. +
+ + )} +
+
+ +
+ {LEGEND.map((l) => ( + + + {l.label} + + ))} + + + collides + + + + learned_from + +
+ + )} +
+ ); +} diff --git a/frontend/src/lib/graph.ts b/frontend/src/lib/graph.ts new file mode 100644 index 0000000..b3a57c7 --- /dev/null +++ b/frontend/src/lib/graph.ts @@ -0,0 +1,10 @@ +import type { PodGraph } from '@podman/shared'; + +const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787'; + +/** Fetch a pod's continual-learning graph (team_model view, demo fallback). */ +export async function fetchPodGraph(podId: string): Promise { + const res = await fetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(podId)}/graph`); + if (!res.ok) throw new Error(`graph request failed: ${res.status}`); + return res.json() as Promise; +} diff --git a/shared/src/graph.ts b/shared/src/graph.ts new file mode 100644 index 0000000..c8175fb --- /dev/null +++ b/shared/src/graph.ts @@ -0,0 +1,69 @@ +/** + * Continual-learning graph: PodMan's visible "it learned" surface. A render of + * the per-pod team_model — who owns/edits which files, where work collides, and + * what PodMan learned from accepted interventions (the `learned_from` edges). + * + * Served embedded in the `team_model` document; mirrored into `graph_nodes` / + * `graph_edges` collections so the model can be walked with MongoDB + * `$graphLookup`. See docs/graph.md. + */ + +export type PodGraphNodeKind = 'engineer' | 'feature' | 'file' | 'collision' | 'intervention'; + +export type PodGraphEdgeKind = + 'owns' | 'editing' | 'touches' | 'collides' | 'warns' | 'learned_from'; + +/** `learned` marks a node PodMan retained from a past accepted intervention. */ +export type PodGraphNodeStatus = 'stable' | 'active' | 'risk' | 'learned'; + +export interface PodGraphNode { + /** Stable graph id, e.g. "engineer:karti" or "file:auth.ts". */ + id: string; + kind: PodGraphNodeKind; + label: string; + summary: string; + /** 0–1 relative importance — drives node size in the viz. */ + weight: number; + status: PodGraphNodeStatus; + /** Layout position on the SVG canvas (viewBox 0..720 x 0..472). */ + x: number; + y: number; +} + +export interface PodGraphEdge { + id: string; + /** Source node id. */ + source: string; + /** Target node id. */ + target: string; + kind: PodGraphEdgeKind; + label: string; + /** 0–1 confidence/strength — drives edge thickness. */ + strength: number; +} + +export interface PodGraphMetric { + label: string; + value: string; + detail: string; +} + +/** A point-in-time render of a pod's team_model. */ +export interface PodGraph { + podId: string; + /** ISO timestamp the snapshot was generated. */ + generatedAt: string; + nodes: PodGraphNode[]; + edges: PodGraphEdge[]; + metrics: PodGraphMetric[]; +} + +/** One node as a standalone document in the `graph_nodes` collection. */ +export interface GraphNodeDoc extends PodGraphNode { + podId: string; +} + +/** One edge as a standalone document in the `graph_edges` collection (for $graphLookup). */ +export interface GraphEdgeDoc extends PodGraphEdge { + podId: string; +} diff --git a/shared/src/index.ts b/shared/src/index.ts index 9353955..1e32a9f 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -9,3 +9,14 @@ export type { SuggestedActionKind, } from './intervention.js'; export * from './messages.js'; +export type { + PodGraph, + PodGraphNode, + PodGraphEdge, + PodGraphMetric, + PodGraphNodeKind, + PodGraphEdgeKind, + PodGraphNodeStatus, + GraphNodeDoc, + GraphEdgeDoc, +} from './graph.js';