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
.trim()
.replace(/^["']|["']$/g, '')
.replace(/^[ACDMRTU?!]{1,2}\s+/, '')
.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> = {
stable: 0,
active: 1,
@@ -166,32 +181,67 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
label: o.engineerId,
status: recent ? 'active' : undefined,
});
if (o.currentFile) {
const file = normalizeFile(o.currentFile);
const file = o.currentFile ? normalizeFile(o.currentFile) : '';
if (isFilePath(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));
}
}
// 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 colNodeFor = new Map<string, string | null>();
const sigToNode = new Map<string, string>();
let distinctCollisions = 0;
for (const col of collisionDocs) {
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, {
label: col.symbol ? `${col.file}#${col.symbol}` : col.file,
label: col.symbol ? `${file}#${col.symbol}` : file,
status: 'risk',
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' : ''
}`,
});
const file = normalizeFile(col.file);
const fNode = upsertNode(b, 'file', file, { label: file, status: 'risk' });
upsertEdge(b, fNode, cNode, 'touches', 'hot', 0.6);
for (const name of col.engineers) {
const eng = upsertNode(b, 'engineer', name, { label: name });
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
@@ -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]>();
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);
const colNode = colNodeFor.get(iv.collisionId);
if (!colNode || ivNodeForCol.has(colNode)) continue;
const ivNode = upsertNode(b, 'intervention', iv.id, {
label:
iv.suggestedAction?.kind === 'open_sync_pr'
@@ -225,9 +281,8 @@ export async function materializePodGraph(podId: string): Promise<PodGraph | nul
: 'watch',
summary: iv.message,
});
if (b.nodes.has(nodeKey('collision', iv.collisionId))) {
upsertEdge(b, nodeKey('collision', iv.collisionId), ivNode, 'warns', 'nudges', 0.85);
}
upsertEdge(b, colNode, ivNode, 'warns', 'nudges', 0.85);
ivNodeForCol.set(colNode, ivNode);
}
// 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);
if (!col) continue;
const file = normalizeFile(col.file);
if (!isFilePath(file)) continue;
const owner =
(out as { learnedOwner?: string }).learnedOwner ?? ownership[file] ?? col.engineers[0];
if (!owner) continue;
const engNode = upsertNode(b, 'engineer', owner, { label: owner, status: 'learned' });
const fNode = upsertNode(b, 'file', file, { label: file });
upsertEdge(b, engNode, fNode, 'owns', 'owns', 0.85);
const ivKey = iv ? nodeKey('intervention', iv.id) : null;
if (ivKey && b.nodes.has(ivKey)) {
upsertNode(b, 'intervention', iv!.id, { status: 'learned' });
upsertEdge(b, ivKey, engNode, 'learned_from', `learned: owns ${file}`, 0.6);
const cNode = colNodeFor.get(col.id);
const ivNode = cNode ? ivNodeForCol.get(cNode) : undefined;
if (ivNode) {
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 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[] = [
{
label: 'Learned owners',