fix(graph): honest graph-consistent metrics + flow narrative on node click

Address review feedback on the live view:

Metrics looked fake because they counted raw DB events (test churn) instead of
the de-noised entities actually drawn — e.g. "Open risk paths: 50" for 2 files,
"Learned owners: 16" with 2 ownership edges, and the caption ("Files with 2+
editors") contradicting the number. Now derived from the final graph:
- Open risk paths = distinct files carrying a surviving collision (50 -> 4 on live).
- Learned owners  = distinct engineers retained as owners via owns/learned_from
  edges (16 -> 1 on live).
- Accept rate     = accepted vs total real outcomes.
demo.ts metrics + loop counts realigned to its own graph (3 owners / 1 risk path
/ 100%) so nothing contradicts the picture; the PREDICT/ADAPT loop stages reuse
the same de-noised counts.

Right pane now explains the flow: clicking a node renders a plain-English walk of
its path (flowNarrative) — "Karti and Yahya are both editing auth.ts before
pushing ... PodMan suggested a sync PR", "PodMan offered a sync PR for the overlap
on auth.ts. The pod accepted it, so PodMan learned Karti owns auth.ts." With no
selection the panel gives a mode-aware explainer of what the lit path means. Edge
legend rounded out with editing/touches.

Verified: lint + -r typecheck + -r build pass; Playwright confirmed the flow text
per node kind and the demo metrics (3/1/100%); the metric formula re-checked
against the real live graph (4 risk files / 1 learned owner).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb-iam
2026-06-27 23:47:14 -07:00
parent c97863e05e
commit 5e9929f8da
5 changed files with 159 additions and 35 deletions
+10 -8
View File
@@ -16,9 +16,9 @@ export function createDemoPodGraph(podId: string): PodGraph {
loop: [
{ key: 'observe', title: 'OBSERVE', value: '5', detail: '~5/s vision contexts', active: false },
{ key: 'store', title: 'STORE', value: '124', detail: 'memory vectors · Atlas', active: false },
{ key: 'predict', title: 'PREDICT', value: '2', detail: 'collisions flagged', active: true },
{ key: 'predict', title: 'PREDICT', value: '1', detail: 'open risk path', active: true },
{ key: 'outcome', title: 'OUTCOME', value: '1/0', detail: 'accepted · dismissed', active: false },
{ key: 'adapt', title: 'ADAPT', value: '5', detail: 'learned owners', active: false },
{ key: 'adapt', title: 'ADAPT', value: '3', detail: 'learned owners', active: false },
],
activity: [
{
@@ -47,21 +47,23 @@ export function createDemoPodGraph(podId: string): PodGraph {
text: 'Yahya opened auth.ts — unpushed changes',
},
],
// Kept consistent with the graph below (3 owner engineers, 1 collision file,
// 1 of 1 interventions accepted) so the numbers never contradict the picture.
metrics: [
{
label: 'Learned owners',
value: '5',
detail: 'Ownership edges retained from accepted interventions.',
value: '3',
detail: 'Distinct owners retained from accepted interventions.',
},
{
label: 'Open risk paths',
value: '2',
detail: 'auth.ts and the memory API have converging editors.',
value: '1',
detail: 'File with two or more converging editors.',
},
{
label: 'Accept rate',
value: '86%',
detail: 'Interventions accepted this session (+14%).',
value: '100%',
detail: 'Interventions accepted vs total this session.',
},
],
nodes: [
+31 -15
View File
@@ -219,7 +219,7 @@ function buildLoop(opts: {
key: 'predict',
title: 'PREDICT',
value: String(riskPaths),
detail: `${riskPaths === 1 ? 'collision' : 'collisions'} flagged`,
detail: `open risk path${riskPaths === 1 ? '' : 's'}`,
},
{ key: 'outcome', title: 'OUTCOME', value: `${accepted}/${dismissed}`, detail: 'accepted · dismissed' },
{
@@ -549,30 +549,48 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
layout(nodes);
// Metrics are derived from the FINAL de-noised graph (not raw docs) so the
// numbers match what's actually on screen. Counting raw collision signatures /
// accepted-outcome rows inflates them with test churn (e.g. 50 "risk paths" for
// 2 files), which reads as fake — these count distinct visible entities instead.
const finalEdges = [...b.edges.values()];
// Open risk paths = distinct files carrying a surviving collision (the triangles).
const riskFiles = new Set<string>();
for (const e of finalEdges) {
if (e.kind === 'touches' && b.nodes.get(e.source)?.kind === 'file') riskFiles.add(e.source);
}
const collisionNodeCount = nodes.filter((n) => n.kind === 'collision').length;
const riskPaths = riskFiles.size || collisionNodeCount;
// Learned owners = distinct engineers PodMan retained as owners from accepted
// interventions (the owns / learned_from edges actually drawn).
const ownerSet = new Set<string>();
for (const e of finalEdges) {
if (e.kind === 'learned_from') ownerSet.add(e.target);
if (e.kind === 'owns') ownerSet.add(e.source);
}
const learnedOwners = [...ownerSet].filter((id) => b.nodes.get(id)?.kind === 'engineer').length;
const acceptedReal = outcomeDocs.filter((o) => o.accepted && o.wasRealCollision).length;
const totalOutcomes = outcomeDocs.length;
const riskPaths = new Set(
collisionDocs.map(
(col) =>
(col as { memorySignature?: string }).memorySignature ??
`${normalizeFile(col.file)}#${col.symbol ?? ''}`,
),
).size;
const acceptRate = totalOutcomes ? Math.round((acceptedReal / totalOutcomes) * 100) : null;
const metrics: PodGraphMetric[] = [
{
label: 'Learned owners',
value: String(acceptedReal),
detail: 'Ownership retained from accepted interventions.',
value: String(learnedOwners),
detail: 'Distinct owners retained from accepted interventions.',
},
{
label: 'Open risk paths',
value: String(riskPaths),
detail: 'Files with two or more converging editors.',
detail: `${riskPaths === 1 ? 'File' : 'Files'} with two or more converging editors.`,
},
{
label: 'Accept rate',
value: totalOutcomes ? `${Math.round((acceptedReal / totalOutcomes) * 100)}%` : '—',
detail: 'Interventions accepted this session.',
value: acceptRate == null ? '—' : `${acceptRate}%`,
detail: 'Interventions accepted vs total this session.',
},
];
@@ -590,8 +608,6 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
).length;
if (!vectorCount) vectorCount = collisionDocs.length;
const learnedOwners = Object.keys(ownership).length || acceptedReal;
const loop = buildLoop({
now,
observations,