feat(graph): demo-ready materializer + per-pod Team memory action

Materializer (live.ts) — cut demo-pod from 110 -> 22 nodes:
- cap to 8 recent collisions; collapse repeats by memorySignature
- collapse interventions to one (most recent) per collision
- filter junk 'files' (URLs, env vars, browser/app names, scratch/test) to
  real source paths only
- prune test-artifact engineers (a/b/verify/codex-check/-testrepo) + any node
  orphaned by that
- collisions referenced by accepted outcomes bypass the cap so the learned_from
  money path never drops; risk-paths metric counts distinct signatures

UI: per-pod 'Team memory' action on each PodCard's menu (replaces the header
button that always opened pods[0]).

Verified against live demo-pod data (joins connect); typechecks clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb-iam
2026-06-27 21:35:14 -07:00
parent 412d3f7df6
commit 2006096cda
3 changed files with 117 additions and 21 deletions
+102 -16
View File
@@ -35,9 +35,24 @@ export function normalizeFile(f: string): string {
return f return f
.trim() .trim()
.replace(/^["']|["']$/g, '') .replace(/^["']|["']$/g, '')
.replace(/^[ACDMRTU?!]{1,2}\s+/, '')
.replace(/^\.\//, ''); .replace(/^\.\//, '');
} }
const MAX_COLLISIONS = 8;
/** Reject "file" values that aren't real source paths — vision/git noise such as
* URLs, env vars, browser/app names, and scratch/test artifacts. */
const FILE_NOISE =
/(:\/\/|^[#~]|\s|\.env\b|\btett\b|test-change|demo-scratch|podman-test|scratch|sslip)/i;
export function isFilePath(f: string): boolean {
if (!f || FILE_NOISE.test(f)) return false;
return /\.[a-z0-9]{1,6}$/i.test(f); // must end in a real file extension
}
/** Engineer names that are test/verification artifacts, not real teammates. */
const ENGINEER_NOISE = /(^verify\b|^.$|testrepo|-?check\b|\d{4,})/i;
const STATUS_RANK: Record<PodGraphNodeStatus, number> = { const STATUS_RANK: Record<PodGraphNodeStatus, number> = {
stable: 0, stable: 0,
active: 1, active: 1,
@@ -166,32 +181,67 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
label: o.engineerId, label: o.engineerId,
status: recent ? 'active' : undefined, status: recent ? 'active' : undefined,
}); });
if (o.currentFile) { const file = o.currentFile ? normalizeFile(o.currentFile) : '';
const file = normalizeFile(o.currentFile); if (isFilePath(file)) {
const f = upsertNode(b, 'file', file, { label: file }); const f = upsertNode(b, 'file', file, { label: file });
upsertEdge(b, eng, f, 'editing', o.activity ?? 'edits', Math.max(0.4, o.confidence ?? 0.5)); upsertEdge(b, eng, f, 'editing', o.activity ?? 'edits', Math.max(0.4, o.confidence ?? 0.5));
} }
} }
// 3. Collisions: the detected overlaps (fused git + vision). // Collisions referenced by accepted outcomes are the "learned" money path — they
// always survive the cap so the learned_from beat is never dropped.
const priorityCol = new Set<string>();
for (const out of outcomeDocs) {
if (!out.accepted || !out.wasRealCollision) continue;
if (out.collisionId) priorityCol.add(out.collisionId);
const iv = interventionDocs.find((i) => i.id === out.interventionId);
if (iv?.collisionId) priorityCol.add(iv.collisionId);
}
// 3. Collisions: collapse repeats by signature, keep the most recent, cap to
// MAX_COLLISIONS, skip junk-file collisions. `collisionById` keeps every doc
// (for the outcome join); `colNodeFor` maps each collisionId to its surviving
// collision node (or null when collapsed / capped / filtered out).
const collisionById = new Map<string, (typeof collisionDocs)[number]>(); const collisionById = new Map<string, (typeof collisionDocs)[number]>();
const colNodeFor = new Map<string, string | null>();
const sigToNode = new Map<string, string>();
let distinctCollisions = 0;
for (const col of collisionDocs) { for (const col of collisionDocs) {
collisionById.set(col.id, col); collisionById.set(col.id, col);
const file = normalizeFile(col.file);
const sig =
(col as { memorySignature?: string }).memorySignature ?? `${file}#${col.symbol ?? ''}`;
const existing = sigToNode.get(sig);
if (existing) {
colNodeFor.set(col.id, existing);
continue;
}
if (!isFilePath(file)) {
colNodeFor.set(col.id, null);
continue;
}
const isPriority = priorityCol.has(col.id);
if (!isPriority && distinctCollisions >= MAX_COLLISIONS) {
colNodeFor.set(col.id, null);
continue;
}
const cNode = upsertNode(b, 'collision', col.id, { const cNode = upsertNode(b, 'collision', col.id, {
label: col.symbol ? `${col.file}#${col.symbol}` : col.file, label: col.symbol ? `${file}#${col.symbol}` : file,
status: 'risk', status: 'risk',
weight: SEVERITY_WEIGHT[col.severity] ?? 0.7, weight: SEVERITY_WEIGHT[col.severity] ?? 0.7,
summary: `${col.engineers.join(' + ')} on ${col.file}${ summary: `${col.engineers.join(' + ')} on ${file}${
(col as { memorySignature?: string }).memorySignature ? ' · seen before' : '' (col as { memorySignature?: string }).memorySignature ? ' · seen before' : ''
}`, }`,
}); });
const file = normalizeFile(col.file);
const fNode = upsertNode(b, 'file', file, { label: file, status: 'risk' }); const fNode = upsertNode(b, 'file', file, { label: file, status: 'risk' });
upsertEdge(b, fNode, cNode, 'touches', 'hot', 0.6); upsertEdge(b, fNode, cNode, 'touches', 'hot', 0.6);
for (const name of col.engineers) { for (const name of col.engineers) {
const eng = upsertNode(b, 'engineer', name, { label: name }); const eng = upsertNode(b, 'engineer', name, { label: name });
upsertEdge(b, eng, cNode, 'collides', 'in', SEVERITY_WEIGHT[col.severity] ?? 0.7); upsertEdge(b, eng, cNode, 'collides', 'in', SEVERITY_WEIGHT[col.severity] ?? 0.7);
} }
sigToNode.set(sig, cNode);
colNodeFor.set(col.id, cNode);
if (!isPriority) distinctCollisions++;
} }
// 4. Git truth (engineer_states): mark unpushed work and confirm editing on // 4. Git truth (engineer_states): mark unpushed work and confirm editing on
@@ -212,10 +262,16 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
} }
} }
// 5. Interventions: what PodMan offered for each collision. // 5. Interventions: collapse to one (most recent) per surviving collision.
const interventionById = new Map<string, (typeof interventionDocs)[number]>(); const interventionById = new Map<string, (typeof interventionDocs)[number]>();
for (const iv of interventionDocs) { const ivNodeForCol = new Map<string, string>();
const sortedIvs = [...interventionDocs].sort((a, b) =>
String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')),
);
for (const iv of sortedIvs) {
interventionById.set(iv.id, iv); interventionById.set(iv.id, iv);
const colNode = colNodeFor.get(iv.collisionId);
if (!colNode || ivNodeForCol.has(colNode)) continue;
const ivNode = upsertNode(b, 'intervention', iv.id, { const ivNode = upsertNode(b, 'intervention', iv.id, {
label: label:
iv.suggestedAction?.kind === 'open_sync_pr' iv.suggestedAction?.kind === 'open_sync_pr'
@@ -225,9 +281,8 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
: 'watch', : 'watch',
summary: iv.message, summary: iv.message,
}); });
if (b.nodes.has(nodeKey('collision', iv.collisionId))) { upsertEdge(b, colNode, ivNode, 'warns', 'nudges', 0.85);
upsertEdge(b, nodeKey('collision', iv.collisionId), ivNode, 'warns', 'nudges', 0.85); ivNodeForCol.set(colNode, ivNode);
}
} }
// 6. Outcomes: the supervised learning signal -> learned_from edges + owns. // 6. Outcomes: the supervised learning signal -> learned_from edges + owns.
@@ -237,16 +292,41 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
const col = iv ? collisionById.get(iv.collisionId) : collisionById.get(out.collisionId); const col = iv ? collisionById.get(iv.collisionId) : collisionById.get(out.collisionId);
if (!col) continue; if (!col) continue;
const file = normalizeFile(col.file); const file = normalizeFile(col.file);
if (!isFilePath(file)) continue;
const owner = const owner =
(out as { learnedOwner?: string }).learnedOwner ?? ownership[file] ?? col.engineers[0]; (out as { learnedOwner?: string }).learnedOwner ?? ownership[file] ?? col.engineers[0];
if (!owner) continue; if (!owner) continue;
const engNode = upsertNode(b, 'engineer', owner, { label: owner, status: 'learned' }); const engNode = upsertNode(b, 'engineer', owner, { label: owner, status: 'learned' });
const fNode = upsertNode(b, 'file', file, { label: file }); const fNode = upsertNode(b, 'file', file, { label: file });
upsertEdge(b, engNode, fNode, 'owns', 'owns', 0.85); upsertEdge(b, engNode, fNode, 'owns', 'owns', 0.85);
const ivKey = iv ? nodeKey('intervention', iv.id) : null; const cNode = colNodeFor.get(col.id);
if (ivKey && b.nodes.has(ivKey)) { const ivNode = cNode ? ivNodeForCol.get(cNode) : undefined;
upsertNode(b, 'intervention', iv!.id, { status: 'learned' }); if (ivNode) {
upsertEdge(b, ivKey, engNode, 'learned_from', `learned: owns ${file}`, 0.6); const ivObj = b.nodes.get(ivNode);
if (ivObj) ivObj.status = 'learned';
upsertEdge(b, ivNode, engNode, 'learned_from', `learned: owns ${file}`, 0.6);
}
}
// Prune test-artifact engineers, then anything left orphaned by that.
const dropNode = (id: string) => {
b.nodes.delete(id);
for (const [eid, e] of [...b.edges])
if (e.source === id || e.target === id) b.edges.delete(eid);
};
for (const [id, n] of [...b.nodes]) {
if (n.kind === 'engineer' && ENGINEER_NOISE.test(n.label)) dropNode(id);
}
// Collisions with no remaining engineer = test/orphan -> drop.
for (const [id, n] of [...b.nodes]) {
if (n.kind !== 'collision') continue;
if (![...b.edges.values()].some((e) => e.kind === 'collides' && e.target === id)) dropNode(id);
}
// Files / interventions left with no edges -> drop.
for (const [id, n] of [...b.nodes]) {
if (n.kind === 'file' || n.kind === 'intervention') {
if (![...b.edges.values()].some((e) => e.source === id || e.target === id))
b.nodes.delete(id);
} }
} }
@@ -259,7 +339,13 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
const acceptedReal = outcomeDocs.filter((o) => o.accepted && o.wasRealCollision).length; const acceptedReal = outcomeDocs.filter((o) => o.accepted && o.wasRealCollision).length;
const totalOutcomes = outcomeDocs.length; const totalOutcomes = outcomeDocs.length;
const riskPaths = collisionDocs.filter((col) => new Set(col.engineers).size >= 2).length; const riskPaths = new Set(
collisionDocs.map(
(col) =>
(col as { memorySignature?: string }).memorySignature ??
`${normalizeFile(col.file)}#${col.symbol ?? ''}`,
),
).size;
const metrics: PodGraphMetric[] = [ const metrics: PodGraphMetric[] = [
{ {
label: 'Learned owners', label: 'Learned owners',
+1 -4
View File
@@ -268,10 +268,6 @@ export default function App() {
<ShieldCheckIcon data-icon="inline-start" /> <ShieldCheckIcon data-icon="inline-start" />
Privacy-limited Privacy-limited
</Badge> </Badge>
<Button variant="outline" onClick={() => setGraphPodId(pods[0]?.id ?? 'demo-pod')}>
<BrainCircuitIcon data-icon="inline-start" />
Team memory
</Button>
<Button variant="outline" onClick={() => void refresh()} disabled={loading}> <Button variant="outline" onClick={() => void refresh()} disabled={loading}>
<RefreshCwIcon data-icon="inline-start" /> <RefreshCwIcon data-icon="inline-start" />
Refresh Refresh
@@ -342,6 +338,7 @@ export default function App() {
onRemoveMember={handleRemoveMember} onRemoveMember={handleRemoveMember}
onUpdate={handleUpdate} onUpdate={handleUpdate}
onDelete={handleDelete} onDelete={handleDelete}
onOpenGraph={setGraphPodId}
/> />
))} ))}
</div> </div>
+14 -1
View File
@@ -1,5 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { MoreHorizontalIcon, PlusIcon, Trash2Icon, UserRoundIcon, VideoIcon } from 'lucide-react'; import {
BrainCircuitIcon,
MoreHorizontalIcon,
PlusIcon,
Trash2Icon,
UserRoundIcon,
VideoIcon,
} from 'lucide-react';
import type { Pod, PodInput } from '@podman/shared'; import type { Pod, PodInput } from '@podman/shared';
import { Avatar, AvatarBadge, AvatarFallback, AvatarGroup } from '@/components/ui/avatar'; import { Avatar, AvatarBadge, AvatarFallback, AvatarGroup } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -42,6 +49,7 @@ export function PodCard({
onRemoveMember: _onRemoveMember, onRemoveMember: _onRemoveMember,
onUpdate, onUpdate,
onDelete, onDelete,
onOpenGraph,
}: { }: {
pod: Pod; pod: Pod;
busy: boolean; busy: boolean;
@@ -52,6 +60,7 @@ export function PodCard({
onRemoveMember: (id: string, name: string) => void; onRemoveMember: (id: string, name: string) => void;
onUpdate: (id: string, patch: PodInput) => void; onUpdate: (id: string, patch: PodInput) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
onOpenGraph: (id: string) => void;
}) { }) {
const [newMember, setNewMember] = useState(''); const [newMember, setNewMember] = useState('');
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
@@ -100,6 +109,10 @@ export function PodCard({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem onSelect={() => onOpenGraph(pod.id)}>
<BrainCircuitIcon />
Team memory
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setEditing(true)}>Edit pod</DropdownMenuItem> <DropdownMenuItem onSelect={() => setEditing(true)}>Edit pod</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
variant="destructive" variant="destructive"