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:
parent
4b8770c78c
commit
e28a7837c4
2 changed files with 286 additions and 0 deletions
103
src/lib/social-cards.ts
Normal file
103
src/lib/social-cards.ts
Normal 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),
|
||||
}));
|
||||
}
|
||||
183
tests/lib/social-cards.test.ts
Normal file
183
tests/lib/social-cards.test.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import type { BeadDependency, BeadIssue } from '../../src/lib/types';
|
||||
import { buildSocialCards } from '../../src/lib/social-cards';
|
||||
|
||||
function issue(overrides: Partial<BeadIssue>): BeadIssue {
|
||||
return {
|
||||
id: overrides.id ?? 'bb-x',
|
||||
title: overrides.title ?? 'Issue',
|
||||
description: overrides.description ?? null,
|
||||
status: overrides.status ?? 'open',
|
||||
priority: overrides.priority ?? 2,
|
||||
issue_type: overrides.issue_type ?? 'task',
|
||||
assignee: overrides.assignee ?? null,
|
||||
owner: overrides.owner ?? null,
|
||||
labels: overrides.labels ?? [],
|
||||
dependencies: overrides.dependencies ?? [],
|
||||
created_at: overrides.created_at ?? '2026-02-12T00:00:00Z',
|
||||
updated_at: overrides.updated_at ?? '2026-02-12T00:00:00Z',
|
||||
closed_at: overrides.closed_at ?? null,
|
||||
close_reason: overrides.close_reason ?? null,
|
||||
closed_by_session: overrides.closed_by_session ?? null,
|
||||
created_by: overrides.created_by ?? null,
|
||||
due_at: overrides.due_at ?? null,
|
||||
estimated_minutes: overrides.estimated_minutes ?? null,
|
||||
external_ref: overrides.external_ref ?? null,
|
||||
metadata: overrides.metadata ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function dep(type: BeadDependency['type'], target: string): BeadDependency {
|
||||
return { type, target };
|
||||
}
|
||||
|
||||
test('buildSocialCards transforms basic bead properties', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', title: 'Test Task', status: 'in_progress', priority: 1 }),
|
||||
issue({ id: 'bb-2', title: 'Bug Fix', status: 'blocked', priority: 0 }),
|
||||
issue({ id: 'bb-3', title: 'Done', status: 'closed', priority: 3 }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.equal(cards.length, 3);
|
||||
assert.equal(cards[0].id, 'bb-1');
|
||||
assert.equal(cards[0].title, 'Test Task');
|
||||
assert.equal(cards[0].status, 'in_progress');
|
||||
assert.equal(cards[0].priority, 'P1');
|
||||
assert.equal(cards[1].status, 'blocked');
|
||||
assert.equal(cards[1].priority, 'P0');
|
||||
assert.equal(cards[2].status, 'closed');
|
||||
assert.equal(cards[2].priority, 'P3');
|
||||
});
|
||||
|
||||
test('buildSocialCards maps priority correctly', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', priority: -1 }),
|
||||
issue({ id: 'bb-2', priority: 0 }),
|
||||
issue({ id: 'bb-3', priority: 1 }),
|
||||
issue({ id: 'bb-4', priority: 2 }),
|
||||
issue({ id: 'bb-5', priority: 3 }),
|
||||
issue({ id: 'bb-6', priority: 4 }),
|
||||
issue({ id: 'bb-7', priority: 10 }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.equal(cards[0].priority, 'P0');
|
||||
assert.equal(cards[1].priority, 'P0');
|
||||
assert.equal(cards[2].priority, 'P1');
|
||||
assert.equal(cards[3].priority, 'P2');
|
||||
assert.equal(cards[4].priority, 'P3');
|
||||
assert.equal(cards[5].priority, 'P4');
|
||||
assert.equal(cards[6].priority, 'P4');
|
||||
});
|
||||
|
||||
test('buildSocialCards computes unlocks (outgoing blocks)', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', dependencies: [dep('blocks', 'bb-2'), dep('blocks', 'bb-3')] }),
|
||||
issue({ id: 'bb-2' }),
|
||||
issue({ id: 'bb-3' }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
const card1 = cards.find((c) => c.id === 'bb-1')!;
|
||||
|
||||
assert.deepEqual(card1.unlocks.sort(), ['bb-2', 'bb-3']);
|
||||
assert.deepEqual(card1.blocks, []);
|
||||
});
|
||||
|
||||
test('buildSocialCards computes blocks (incoming blocks)', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1' }),
|
||||
issue({ id: 'bb-2', dependencies: [dep('blocks', 'bb-1')] }),
|
||||
issue({ id: 'bb-3', dependencies: [dep('blocks', 'bb-1')] }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
const card1 = cards.find((c) => c.id === 'bb-1')!;
|
||||
|
||||
assert.deepEqual(card1.blocks.sort(), ['bb-2', 'bb-3']);
|
||||
assert.deepEqual(card1.unlocks, []);
|
||||
});
|
||||
|
||||
test('buildSocialCards ignores missing targets for blocks', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', dependencies: [dep('blocks', 'bb-missing')] }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.equal(cards.length, 1);
|
||||
assert.deepEqual(cards[0].unlocks, []);
|
||||
assert.deepEqual(cards[0].blocks, []);
|
||||
});
|
||||
|
||||
test('buildSocialCards extracts agents from assignee', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', assignee: 'agent-alpha' }),
|
||||
issue({ id: 'bb-2', assignee: 'agent-beta', metadata: { agentStatus: 'stale' } }),
|
||||
issue({ id: 'bb-3', assignee: null }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.deepEqual(cards[0].agents, [{ name: 'agent-alpha', status: 'active' }]);
|
||||
assert.deepEqual(cards[1].agents, [{ name: 'agent-beta', status: 'stale' }]);
|
||||
assert.deepEqual(cards[2].agents, []);
|
||||
});
|
||||
|
||||
test('buildSocialCards maps status correctly', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', status: 'open' }),
|
||||
issue({ id: 'bb-2', status: 'in_progress' }),
|
||||
issue({ id: 'bb-3', status: 'blocked' }),
|
||||
issue({ id: 'bb-4', status: 'closed' }),
|
||||
issue({ id: 'bb-5', status: 'tombstone' }),
|
||||
issue({ id: 'bb-6', status: 'deferred' }),
|
||||
issue({ id: 'bb-7', status: 'pinned' }),
|
||||
issue({ id: 'bb-8', status: 'hooked' }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.equal(cards[0].status, 'ready');
|
||||
assert.equal(cards[1].status, 'in_progress');
|
||||
assert.equal(cards[2].status, 'blocked');
|
||||
assert.equal(cards[3].status, 'closed');
|
||||
assert.equal(cards[4].status, 'closed');
|
||||
assert.equal(cards[5].status, 'ready');
|
||||
assert.equal(cards[6].status, 'ready');
|
||||
assert.equal(cards[7].status, 'ready');
|
||||
});
|
||||
|
||||
test('buildSocialCards converts updated_at to lastActivity Date', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', updated_at: '2026-02-15T12:30:00Z' }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.ok(cards[0].lastActivity instanceof Date);
|
||||
assert.equal(cards[0].lastActivity.toISOString(), '2026-02-15T12:30:00.000Z');
|
||||
});
|
||||
|
||||
test('buildSocialCards returns empty array for empty input', () => {
|
||||
const cards = buildSocialCards([]);
|
||||
assert.deepEqual(cards, []);
|
||||
});
|
||||
|
||||
test('buildSocialCards ignores non-blocks dependencies', () => {
|
||||
const beads = [
|
||||
issue({ id: 'bb-1', dependencies: [dep('parent', 'bb-2'), dep('relates_to', 'bb-3')] }),
|
||||
issue({ id: 'bb-2' }),
|
||||
issue({ id: 'bb-3' }),
|
||||
];
|
||||
|
||||
const cards = buildSocialCards(beads);
|
||||
|
||||
assert.deepEqual(cards[0].unlocks, []);
|
||||
assert.deepEqual(cards[0].blocks, []);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue