Self-contained handoff for a fresh session: rebuild the light/real-data graph to the dark Bauhaus mock's quality + structure (metrics rail, learning-loop rail, activity stream, selected-node panel) and make the graph dynamic (force-directed + animated), in light shadcn. Captures architecture, data, the dynamic-layout options, what to feed the new panels, files, gotchas, and acceptance criteria. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
19 KiB
Team Memory Graph — Redesign Brief (fresh-session handoff)
You are a fresh Claude Code session with no prior context. Read this whole file first. Your job: rebuild the live "Team memory" graph view so the light/real-data version is as polished and functional as the original dark Bauhaus mock, and make the graph dynamic (force-directed + animated), not the current dead static-column layout. Do not rewrite the backend materializer — it is good. The problem is 100% the frontend rendering.
0. Mission (one paragraph)
PodMan's "Team memory" is a per-pod graph of who owns/edits which files, where work collides, and what PodMan learned from accepted interventions — the continual-learning loop made legible in 10 seconds. A dark Bauhaus mock of this view looks great (clean 3-panel layout, a learning-loop rail, an activity stream, a readable graph). The shipped light version on real data looks terrible (a hairball of red edges, overlapping labels, a static lifeless layout, and it's missing the learning-loop rail + activity stream entirely). Make the light version match the mock's structure/polish/functionality, in the app's light shadcn theme, and make the graph dynamic (organic force-directed layout, draggable, animated transitions). Keep using real data from the existing materializer.
1. The two reference points
A. The dark Bauhaus mock = what "good" looks like (target structure)
A single dark card titled "PODMAN — CONTINUAL-LEARNING OBSERVATORY" with a LIVE · POD demo-pod
status. Layout:
- Left rail — WORKFLOW METRICS: a vertical stack of bordered cards, each a big numeral + an
UPPERCASE tracked label + a one-line detail, with a colored left-accent bar:
03 PODS WATCHED,05 ENGINEERS LIVE,02 COLLISIONS OPEN (▲ auth.ts critical),01 INTERVENTION SENT,86% ACCEPT RATE (▲ +14% this session),124 MEMORY VECTORS. - Center — the GRAPH: sparse, geometric, readable. Node shapes encode kind
(engineer = filled square, file = outlined square, feature = circle, collision = triangle,
intervention = diamond). One risk path is lit (Karti+Yahya → auth.ts → collision → sync PR →
learned_from), everything else dimmed. Edges color-coded (collidesred,warnsamber/orange,learned_fromdashed violet,ownsblue,editingpaper,touchesgrey). - Right rail — LEARNING LOOP: a vertical 5-step stepper with the active step highlighted/pulsing:
01 OBSERVE (vision → 5 contexts/s)→02 STORE (124 vectors · Atlas)→03 PREDICT (2 collisions flagged)[active] →04 OUTCOME (1 accepted · 0 dismissed)→05 ADAPT (Karti→auth ownership +). Arrows between steps. - Bottom-left — ACTIVITY STREAM: a time-stamped feed with colored kind-tags:
15:48 EDITING Yahya opened auth.ts — unpushed changes detected,15:48 COLLISION Critical overlap on auth.ts · Karti + Yahya,15:49 WARNS PodMan spoke: "open a sync PR?" → card sent,15:49 OUTCOME Sync PR accepted by the pod,15:49 LEARNED_FROM Memory updated: Karti owns auth (confidence ↑). - Bottom-right — SELECTED NODE: click a node → kind / name / relationships count / severity / a
one-line "why" (
Two engineers editing the same file before push — the signal git can't see.). - Legend: engineer / file / feature / collision / intervention · collides / warns / learned_from.
It reads in 10 seconds because it is sparse, color-coded, and tells the loop story with the rails + stream, not just a node blob. (The full mock HTML/CSS is reproduced in Appendix A — port its structure to light shadcn.)
B. The shipped light version = what's wrong (the thing to fix)
Same data, but: a hairball — every engineer fans red collides edges to ~6 collision triangles
(all labeled sync PR); file labels overlap in a dim middle column; the graph uses a static
deterministic column layout (x by kind, y evenly spread) so it looks dead/lifeless; and it is
missing the LEARNING LOOP rail and the ACTIVITY STREAM entirely — it's just a metrics rail + the bare
graph + a selected-node panel. (Two already-fixed-on-branch items: garbage collision labels
infra/README.md### Running the git watcher and full-path bleed — see PR #7 / commit a21a289,
shortLabel in live.ts. Build on top of that, don't redo it.)
2. The gap to close (light vs mock)
| Mock has | Light version | Action |
|---|---|---|
| Workflow metrics rail | ✅ has it (3 metrics) | keep; restyle to match |
| Learning loop rail (observe→…→adapt) | ❌ missing | build it (needs live counts) |
| Activity stream feed | ❌ missing | build it (needs an event feed) |
| Selected-node panel | ✅ has it | keep |
| Dynamic / animated graph | ❌ static columns | replace the layout |
| Sparse, lit "risk path" | partial (Risk-path mode exists) | improve emphasis + spacing |
| Legend | ✅ | keep |
3. Current architecture (build on this — do NOT rewrite the materializer)
Backend (good, keep):
backend/src/graph/live.ts—materializePodGraph(podId): builds the graph from the real Mongo collections (pods,engineer_states,observations,collisions,interventions,outcomes). It already de-noises hard: collapses collisions bymemorySignature, caps to 8, collapses interventions to one per collision, filters junk files (isFilePath), prunes test-artifact engineers (ENGINEER_NOISE), caps files to 9, short labels (shortLabel). Output is ~20 clean nodes fordemo-pod. This is solid — extend it, don't replace it.backend/src/graph/store.ts—loadPodGraph(podId): live materializer → seededteam_model.graph→ demo fallback (createDemoPodGraph). PlusreachFrom($graphLookup).- Route:
GET /api/pods/:id/graphreturnsPodGraph(also/graph/reach/:node). backend/src/memory/db.ts—collections(),getGitStates(podId),getDb().- WS bus:
backend/src/server.tshostsws /api/events(the agent +/api/outcomebroadcast here).
Frontend (this is where the work is):
frontend/src/components/GraphView.tsx— the thing you redesign (~90% of the work). Currently: fetches/api/pods/:id/graph, renders a bespoke SVG with the static column layout, Risk/Learning/Whole toggles, a metrics rail, a selected-node panel, a legend. Composed from shadcn primitives (Button,Badge) + Tailwind utilities. Theme-aware via shadcn tokens.frontend/src/lib/graph.ts—fetchPodGraph(podId).- Opened from each
PodCard's⋯menu → "Team memory" (onOpenGraph(pod.id)infrontend/src/App.tsx). It is a conditional render (no route).
Data contract (shared/src/graph.ts):
PodGraph = { podId, generatedAt, nodes: PodGraphNode[], edges: PodGraphEdge[], metrics: PodGraphMetric[] }
PodGraphNode = { id, kind, label, summary, weight 0..1, status: 'stable'|'active'|'risk'|'learned', x, y }
// kind: 'engineer'|'feature'|'file'|'collision'|'intervention'
PodGraphEdge = { id, source, target, kind, label, strength 0..1 }
// kind: 'owns'|'editing'|'touches'|'collides'|'warns'|'learned_from'
PodGraphMetric = { label, value, detail }
Theme / components (HARD RULE): the app is light shadcn, built from the ruixen registry —
add primitives with npx shadcn@latest add "https://ruixen.com/r/[component]" and compose from
@/components/ui/* (Button, Badge, Card, Tabs, ToggleGroup, etc.) using the design tokens
(var(--card) / --foreground / --muted-foreground / --border, --chart-1..5). Only the SVG/canvas
graph is bespoke. Match frontend/src/App.tsx's StatPill/BriefLine utility patterns.
4. Target design (build this)
A light shadcn page with the mock's structure:
┌───────────────────────────────────────────────────────────────────────┐
│ Header: "Team memory · What PodMan learned · <pod>" [← Pods] │
├───────────────────────────────────────────────────────────────────────┤
│ Toggles: Risk path | Learning edges | Whole graph (keep) │
├──────────────┬──────────────────────────────────┬─────────────────────┤
│ WORKFLOW │ │ LEARNING LOOP │
│ METRICS │ DYNAMIC GRAPH CANVAS │ observe→store→ │
│ (cards) │ (force-directed + animated) │ predict→outcome→ │
│ │ │ adapt (active pulses)│
├──────────────┴──────────────────────────────────┴─────────────────────┤
│ ACTIVITY STREAM (time-tagged feed) │ SELECTED NODE (detail) │
└───────────────────────────────────────────────────────────────────────┘
- Light shadcn throughout (theme-aware; follows dark mode if the app ever toggles). Keep the
geometric node-shape + color encoding (it's the legible part) but on light surfaces with the
app's hues (engineer blue
#2563eb, file slate outline#475569, feature amber#d97706, collision red#dc2626, intervention violet#7c3aed; edges: collides red, warns amber, learned_from dashed violet, owns blue, editing slate, touches faint slate). - Default to "Risk path": light the collision→intervention→
learned_fromchain; dim the rest.
5. Make the graph DYNAMIC (the headline new requirement)
The static column layout (live.ts layout() sets x/y by kind) looks dead. Replace the frontend
rendering with a dynamic graph. Pick one (recommended order):
d3-forceforce-directed (recommended). Addd3-force(small). Run a force simulation on thePodGraphnodes/edges: link force (byedge.strength), charge/repulsion, center, collision radius (bynode.weight). Render nodes/edges as SVG, update positions per tick. Make nodes draggable (pin on drag). Animate new nodes/edges fading in on data refresh, and thelearned_fromdashed stroke animating. Ignore the server'sx/y(or use them as initial positions). Keep node shapes.react-force-graph/force-graph(canvas) — heavier, faster for big graphs; overkill at ~20 nodes but fine.- A custom animated layered layout (engineers → files → collisions → interventions columns, but with curved edges, eased position transitions on refresh, and gentle idle motion). Lighter-weight than d3-force; still feels alive if you animate transitions.
Realtime/dynamic data: poll GET /api/pods/:id/graph every ~5s and animate the diff between
snapshots (don't hard-replace). Optionally subscribe to ws /api/events for instant nudges. New
collisions/interventions should visibly animate in; the learned_from edge + gold node should pop on a
new accepted outcome.
De-hairball: even force-directed, ~6 collisions × 3 engineers = many collides edges. Mitigate:
bundle/curve edges, lower non-risk edge opacity, default to Risk-path emphasis, size nodes by weight,
and keep label collision-avoidance (offset labels, hide on overlap, show on hover/select).
6. Data for the new panels (extend the materializer or add endpoints)
The mock's Learning Loop and Activity Stream need data the current PodGraph doesn't carry. Two
options: (a) extend materializePodGraph to also return loop + activity, or (b) add small endpoints.
Recommended: extend the return type (additive to shared/src/graph.ts).
- Learning loop counts (
observe→store→predict→outcome→adapt):- observe = recent
observationscount (e.g. last 60s) / rate - store =
memory_vectorsorcollisions.embeddingcount (Voyage vectors) - predict = open
collisions(distinct signatures) count - outcome =
outcomesaccepted vs dismissed counts - adapt =
team_model.ownershipentries / learned owners count - mark the "active" stage = the most recent activity.
- observe = recent
- Activity stream: merge + time-sort recent events from
collisions.detectedAt,interventions.createdAt,outcomes.recordedAt,engineer_states.gitUpdatedAt→ a typed feed{ at, kind: 'editing'|'collision'|'warns'|'outcome'|'learned_from', text }. Cap to ~8 most recent.
(backend/src/memory/db.ts collections() gives you observations/collisions/interventions/outcomes;
getGitStates gives engineer_states; team_model is db.collection('team_model').)
7. Files to touch
frontend/src/components/GraphView.tsx— the redesign (force-directed graph + 3-panel layout + learning-loop rail + activity stream). May split intoGraphCanvas.tsx,LearningLoop.tsx,ActivityStream.tsx,MetricsRail.tsx.frontend/src/lib/graph.ts— add fetches for loop/activity if you add endpoints.backend/src/graph/live.ts(extend, don't rewrite) — emitloop+activityin the result; keep all the de-noise.shared/src/graph.ts— addloop/activitytypes toPodGraph(additive).- deps —
d3-force(+@types/d3-force) via pnpm infrontend. - Possibly add a ruixen primitive (e.g.
timeline,stepper) via the shadcn CLI if one fits.
8. Constraints & gotchas (READ — these will bite you)
- The materializer is good — do not rewrite it. It already de-noises (caps, collapse-by-signature, engineer/file filters, short labels). The bad UI is the frontend layout/render, not the data.
- PWA service worker caches aggressively — after any deploy, hard-refresh (Cmd-Shift-R) or test in a private window, or you'll think nothing changed.
- Deploy = merge to
main(DOdeploy_on_push: true).mainis shared by ~4 engineers and moves fast. Work on a branch, open a PR, merge. Don't push tomaindirectly. learned_from"money" edge won't render ondemo-podright now — its one accepted outcome is orphaned (points at a collision deleted by test churn). It needs one intact accept flow (real collision → intervention → someone clicks Accept) to draw. To demo, seed a clean chain or clear test docs (writes to shared Atlas — confirm scope first).- Atlas creds rotate frequently —
podman/.env'sMONGODB_URImay be stale; the deployed env has the working one. If local Mongo auth fails, that's why. - Build verification: some sandboxes can't run
pnpm/vite/shadcn deps (lucide-react,@radix-ui). Verify the frontend withpnpm buildin a real env or CI before merging. Backend typecheck excludes uninstalledws/sharp/@livekit/rtc-nodenoise. - Compose from ruixen/shadcn primitives (
npx shadcn add ruixen.com/r/[component],@/components/ui/*); only the SVG/canvas graph is bespoke. MatchStatPill/BriefLineinApp.tsx. - Light theme + tokens — never hardcode dark colors for chrome; use
var(--card)/--foreground/…. Keep fixed semantic hues only for the node/edge kind encoding.
9. Acceptance criteria
- Light Team-memory page matches the mock's structure: metrics rail + dynamic graph + learning-loop rail + activity stream + selected-node panel + legend.
- Graph is dynamic: force-directed (or animated layered), draggable, animates new nodes/edges and refresh transitions; no overlapping labels, no hairball.
- Reads in 10s; the risk/money path is obvious by default.
- Light shadcn theme, theme-aware; composed from ruixen primitives.
- Uses real data from
materializePodGraph; graceful demo fallback when empty. pnpm build+ typechecks pass; deploys; verified after a hard-refresh.
10. Suggested first moves for the new session
- Read this file +
docs/graph.md+docs/live-ui-spec.md(R1/R2 sections) +CLAUDE.md. git fetch; branch offmain(orfeat/live-graph-glue, which has the latest graph work).- Hit the live data once:
curl https://165-22-129-249.sslip.io/api/pods/demo-pod/graph— that's the realPodGraphyou'll render. - Build a
d3-forceGraphCanvasfirst (replace the static layout), get it draggable + animated. - Add
LearningLoop+ActivityStream(extend the materializer to feed them). - Polish to the mock;
pnpm build; PR → main → redeploy → hard-refresh.
Appendix A — the dark mock (reference structure to port to light)
The mock is a single dark card. Structure + the exact content to reproduce (in light shadcn):
- Header: brand glyph (blue square + amber circle + red triangle + outlined square) +
PODMAN / CONTINUAL-LEARNING OBSERVATORY+● LIVE · POD demo-pod. - Grid
180px 1fr 196px: metrics rail | graph | learning-loop rail. - Metrics cards: big
Archivo-weight numeral, uppercase tracked label, muted detail, colored left-accent (blue/red/yellow/violet/green). - Graph: SVG, geometric node shapes by kind, color-coded edges, one lit risk path, dim others; click a node → highlight its incident edges + neighbors, fill the selected-node panel.
- Learning-loop rail: 5 bordered steps with number + UPPERCASE title + muted sub; the active step has a
pulsing left bar;
↓arrows between. - Legend row (node kinds + edge kinds).
- Activity stream (time + colored tag + text) and selected-node panel below.
Palette used (port to shadcn tokens for chrome; keep these as node/edge hues):
bg #0c0c0e, panel #141417, line #2a2a31, paper/text #ECE7DA, muted #8d897e,
blue #3B5BFF, red #E2403A, amber #F6C445, violet #8b6cff, green #46c07a.
For light: chrome → var(--card)/--foreground/--border/--muted-foreground; node/edge hues →
blue #2563eb, slate #475569, amber #d97706, red #dc2626, violet #7c3aed (light-readable).
The dark mock was a
show_widgetdemo (not a saved file). If you want the literal HTML/CSS, ask the user to paste it, or reconstruct from this appendix — the structure + content above is the spec. Goal: same structure, same legibility, light theme + dynamic graph.