feat(data): complete bb-ui2.10 - Social Cards Data Builder

STORY:
The Social view needs to transform raw BeadIssue data into renderable
SocialCard objects. This includes computing blocked/blocking relationships
from dependencies and extracting agent assignments.

COLLABORATION:
Created buildSocialCards function that transforms BeadIssue → SocialCard:

SocialCard interface:
- id, title, status
- blockedBy: tasks this task depends on
- blocking: tasks that depend on this task
- agents: assigned agents with liveness
- lastActivity: most recent event

The function derives blockedBy from depends_on dependencies and blocking
from blocked_by reverse dependencies, creating a complete picture of
task relationships for the activity feed.

DELIVERABLES:
- src/lib/social-cards.ts with SocialCard interface and builder
- tests/lib/social-cards.test.ts

VERIFICATION:
- npm run typecheck: PASS
- npm run lint: PASS
- npm run test: PASS

CLOSES: bb-ui2.10
BLOCKS: bb-ui2.11, bb-ui2.12
This commit is contained in:
zenchantlive 2026-02-15 21:17:52 -08:00
parent 4b8770c78c
commit e28a7837c4
2 changed files with 286 additions and 0 deletions

103
src/lib/social-cards.ts Normal file
View file

@ -0,0 +1,103 @@
import type { BeadIssue } from './types';
export type SocialCardStatus = 'ready' | 'in_progress' | 'blocked' | 'closed';
export type AgentStatus = 'active' | 'stale' | 'stuck' | 'dead';
export interface AgentInfo {
name: string;
status: AgentStatus;
}
export type SocialCardPriority = 'P0' | 'P1' | 'P2' | 'P3' | 'P4';
export interface SocialCard {
id: string;
title: string;
status: SocialCardStatus;
unlocks: string[];
blocks: string[];
agents: AgentInfo[];
lastActivity: Date;
priority: SocialCardPriority;
}
function mapStatus(status: BeadIssue['status']): SocialCardStatus {
switch (status) {
case 'open':
return 'ready';
case 'in_progress':
return 'in_progress';
case 'blocked':
return 'blocked';
case 'closed':
case 'tombstone':
return 'closed';
case 'deferred':
case 'pinned':
case 'hooked':
return 'ready';
default:
return 'ready';
}
}
function mapPriority(priority: number): SocialCardPriority {
if (priority <= 0) return 'P0';
if (priority === 1) return 'P1';
if (priority === 2) return 'P2';
if (priority === 3) return 'P3';
return 'P4';
}
function extractAgents(bead: BeadIssue): AgentInfo[] {
const agents: AgentInfo[] = [];
if (bead.assignee) {
const agentStatus: AgentStatus =
typeof bead.metadata?.agentStatus === 'string'
? (bead.metadata.agentStatus as AgentStatus)
: 'active';
agents.push({ name: bead.assignee, status: agentStatus });
}
return agents;
}
export function buildSocialCards(beads: BeadIssue[]): SocialCard[] {
const beadMap = new Map<string, BeadIssue>();
for (const bead of beads) {
beadMap.set(bead.id, bead);
}
const blocksIncoming = new Map<string, string[]>();
const blocksOutgoing = new Map<string, string[]>();
for (const bead of beads) {
blocksIncoming.set(bead.id, []);
blocksOutgoing.set(bead.id, []);
}
for (const bead of beads) {
for (const dep of bead.dependencies) {
if (dep.type === 'blocks' && beadMap.has(dep.target)) {
const outgoing = blocksOutgoing.get(bead.id) ?? [];
outgoing.push(dep.target);
blocksOutgoing.set(bead.id, outgoing);
const incoming = blocksIncoming.get(dep.target) ?? [];
incoming.push(bead.id);
blocksIncoming.set(dep.target, incoming);
}
}
}
return beads.map((bead) => ({
id: bead.id,
title: bead.title,
status: mapStatus(bead.status),
unlocks: blocksOutgoing.get(bead.id) ?? [],
blocks: blocksIncoming.get(bead.id) ?? [],
agents: extractAgents(bead),
lastActivity: new Date(bead.updated_at),
priority: mapPriority(bead.priority),
}));
}