Codex review fix. The earlier approach synthesized a 'suppressed' beat from every dismissed OUTCOME, which is wrong: a dismissal is not a later suppressed repeat, and the old dismissal timestamp sank below the 12-row activity cap (invisible). Now the negative-feedback loop is recorded WHEN IT HAPPENS. When shouldIntervene() returns false specifically because a signature was dismissed before (agent/podman.ts), the agent writes a durable SuppressionDoc to a new `suppressions` collection, timestamped at the repeat. graph/live.ts materializes those into kind:'suppressed' activity (recent -> surfaces at the top). The suppressed collision is never written to `collisions` (agent returns before recordCollision), so this is its own record. Verified end-to-end: a transient demo-pod suppression renders at the TOP of the activity stream; cleaned up after. shared build + backend/frontend typecheck + eslint pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
5.4 KiB
Continual-Learning Graph Spec
Owner: graph data + visualization. Status: demo-backed / active. Satisfies the documentation-first gate for the
backend/src/graph/*andfrontend/src/components/GraphView.tsxfiles. This file is the canonical graph spec.
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:
-
Embedded (served to the viz): the
team_modeldocument carries agraphfield:// team_model doc (one per pod, unique index { podId: 1 }) { podId, graph: PodGraph, updatedAt }GET /api/pods/:podId/graphreturns the live materialized graph first, thenteam_model.graph, then a labeled demo graph when neither live nor seeded data exists. -
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_nodesGraphNodeDoc=PodGraphNode+podId{ podId: 1, id: 1 }uniquegraph_edgesGraphEdgeDoc=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):
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,GraphEdgeDocbackend/src/graph/demo.ts—createDemoPodGraph(podId)(grounded in the demo-pod crew)backend/src/graph/live.ts—materializePodGraph(podId): builds the graph from the real collections (pods, engineer_states, observations, collisions, interventions, outcomes)backend/src/graph/store.ts—loadPodGraph(live → seeded → demo),seedGraph,reachFrom($graphLookup)backend/src/graph/seed.ts—pnpm graph:seed(writes demo intoteam_model+ graph collections)frontend/src/lib/graph.ts—fetchPodGraph(podId)frontend/src/components/GraphView.tsx— shadcn-themed SVG graph (theme-aware; toggle fromApp.tsx)
Live data → graph mapping
materializePodGraph reads the 5 real collections per pod and emits a PodGraph:
| Collection | Produces |
|---|---|
pods.members |
baseline engineer nodes |
engineer_states |
engineer risk if unpushed; file nodes (git paths parsed); editing |
observations |
engineer active; file from currentFile; editing (strength=conf.) |
collisions |
collision nodes; collides (eng→col) + touches (file→col) |
interventions |
intervention nodes; warns (col→intervention) |
outcomes |
learned_from (intervention→owner) on accepted; flips nodes to learned |
suppressions |
suppressed activity beats — a dismissed signature recurred and PodMan stayed quiet (negative-feedback made visible; written at repeat time by the agent) |
Metrics (learned owners / open risk paths / accept rate) are live counts.
Fallback order (loadPodGraph)
- Live —
materializePodGraphfrom the real collections (returnsnullif only bare roster). - Seeded —
team_model.graph(frompnpm graph:seed). - Demo —
createDemoPodGraph()(stage safety; never an empty canvas).