Files
podman/docs/cont_learning.md
sb-iam 65764f0a1c feat(graph): record + surface suppressed-repeat activity at repeat time (Feature A) (#43)
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>
2026-06-28 09:35:56 -07:00

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/* and frontend/src/components/GraphView.tsx files. 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:

  1. Embedded (served to the viz): the team_model document carries a graph field:

    // team_model doc (one per pod, unique index { podId: 1 })
    { podId, graph: PodGraph, updatedAt }
    

    GET /api/pods/:podId/graph returns the live materialized graph first, then team_model.graph, then a labeled demo graph when neither live nor seeded data exists.

  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):

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.tsPodGraph, PodGraphNode/Edge/Metric, GraphNodeDoc, GraphEdgeDoc
  • backend/src/graph/demo.tscreateDemoPodGraph(podId) (grounded in the demo-pod crew)
  • backend/src/graph/live.tsmaterializePodGraph(podId): builds the graph from the real collections (pods, engineer_states, observations, collisions, interventions, outcomes)
  • backend/src/graph/store.tsloadPodGraph (live → seeded → demo), seedGraph, reachFrom ($graphLookup)
  • backend/src/graph/seed.tspnpm graph:seed (writes demo into team_model + graph collections)
  • frontend/src/lib/graph.tsfetchPodGraph(podId)
  • frontend/src/components/GraphView.tsx — shadcn-themed SVG graph (theme-aware; toggle from App.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)

  1. LivematerializePodGraph from the real collections (returns null if only bare roster).
  2. Seededteam_model.graph (from pnpm graph:seed).
  3. DemocreateDemoPodGraph() (stage safety; never an empty canvas).