paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
576 lines (523 loc) • 20.2 kB
text/typescript
import { formatClock, formatTokens, fullTokens, roleWords } from './cost';
import type {
PaqadChief,
PaqadHighTable,
PaqadHudSnapshot,
PaqadSeat,
PaqadSeatState,
PaqadTone,
PaqadTrackRecord,
} from './types';
export const TABLE_NAME = 'High Table';
export const WHO_SHAPED = 'Who shaped the spec';
export const CREDITS_LATER =
'Shown when the spec is frozen: each spec line with the experts who shaped it.';
export const CHIEF_WORDS = 'Chief architect';
export const SKIPPED_TEXT =
'High Table skipped: the spec was written by hand while the pipeline was on.';
export interface TableSegment {
text: string;
tone?: PaqadTone;
dim?: boolean;
href?: string;
}
export type TableLine = TableSegment[];
export const SEAT_MARK: Record<PaqadSeatState, string> = {
seated: '○',
writing: '◐',
done: '●',
};
const SEAT_TONE: Record<PaqadSeatState, PaqadTone | undefined> = {
seated: undefined,
writing: 'working',
done: 'good',
};
export const SEAT_COLOR = {
done: '#2e9d5b',
writing: '#3b7dd8',
seated: '#8a8a8a',
late: '#d08b1e',
} as const;
const CONFLICT_CATEGORY = 'spec.expert_conflict';
const ms = (iso: string | null): number => (iso ? Date.parse(iso) : Number.NaN);
function plural(count: number, one: string, many = `${one}s`): string {
return `${count} ${count === 1 ? one : many}`;
}
export function showsTable(
snapshot: PaqadHudSnapshot | null,
): snapshot is PaqadHudSnapshot & { table: PaqadHighTable } {
return snapshot?.active === true && snapshot.lane !== 'fast' && snapshot.table !== null;
}
export function hasRuled(table: PaqadHighTable): boolean {
return table.chief?.state === 'done' && table.chief.verdict !== null;
}
function specFrozen(snapshot: PaqadHudSnapshot): boolean {
const spec = snapshot.stages.find((stage) => stage.stage === 'specification');
return spec?.state === 'complete' || spec?.state === 'redone';
}
export function isOpenConflict(snapshot: PaqadHudSnapshot, target: string): boolean {
return snapshot.decisions.pending.some(
(decision) => decision.category === CONFLICT_CATEGORY && decision.question.includes(target),
);
}
export function tableFlags(snapshot: PaqadHudSnapshot & { table: PaqadHighTable }): number {
const table = snapshot.table;
const late =
table.met_after_freeze || table.seats.some((seat) => seat.late) || table.chief?.late === true;
const conflicts = (table.rulings?.conflicts ?? []).filter((conflict) =>
isOpenConflict(snapshot, conflict.target),
).length;
const gaps = specFrozen(snapshot) ? 0 : table.gaps.length;
return (late ? 1 : 0) + conflicts + gaps;
}
export interface SeatDot {
role: string;
state: PaqadSeatState;
late: boolean;
}
export interface TableInstrument {
skipped: boolean;
dots: SeatDot[];
chief: SeatDot | null;
ruled: boolean;
writing: number;
done: number;
seated: number;
chiefWriting: boolean;
flags: number;
}
const dotOf = (seat: PaqadSeat): SeatDot => ({
role: seat.role,
state: seat.state,
late: seat.late,
});
function instrumentOf(table: PaqadHighTable, flags: number): TableInstrument {
const count = (state: PaqadSeatState) =>
table.seats.filter((seat) => seat.state === state).length;
return {
skipped: table.skipped,
dots: table.seats.map(dotOf),
chief: table.chief ? dotOf(table.chief) : null,
ruled: hasRuled(table),
writing: count('writing'),
done: count('done'),
seated: count('seated'),
chiefWriting: table.chief?.state === 'writing',
flags,
};
}
export function tableInstrument(snapshot: PaqadHudSnapshot | null): TableInstrument | null {
if (!showsTable(snapshot)) return null;
return instrumentOf(snapshot.table, tableFlags(snapshot));
}
export function tableSummary(instrument: TableInstrument): string {
const parts: string[] = [];
if (instrument.writing > 0) parts.push(`${instrument.writing} writing`);
if (instrument.done > 0) parts.push(`${instrument.done} done`);
if (instrument.seated > 0) parts.push(`${instrument.seated} seated`);
if (instrument.chiefWriting) parts.push('the chief architect writing');
const late = instrument.dots.filter((dot) => dot.late).length;
if (late > 0) parts.push(`${late} late`);
return parts.join(', ');
}
export function dotSegment(dot: SeatDot): TableSegment {
return {
text: SEAT_MARK[dot.state],
tone: dot.late ? 'warn' : SEAT_TONE[dot.state],
dim: dot.state === 'seated' && !dot.late,
};
}
export function seatStateWords(seat: PaqadSeat, now: number): string {
const started = ms(seat.started_at);
if (seat.state === 'writing') {
return Number.isFinite(started) ? `writing for ${formatClock(now - started)}` : 'writing';
}
if (seat.state === 'done') {
const done = ms(seat.done_at);
return Number.isFinite(started) && Number.isFinite(done)
? `done in ${formatClock(done - started)}`
: 'done';
}
return 'seated';
}
export function noteTokens(seat: PaqadSeat): string | null {
if (seat.tokens_estimate === null) return null;
const budget =
seat.budget_tokens !== null ? ` of its ${formatTokens(seat.budget_tokens)} budget` : '';
return `Note: ~${formatTokens(seat.tokens_estimate)} tokens${budget}`;
}
export function runTokens(seat: PaqadSeat): string | null {
if (!seat.tokens) return null;
return `Run: ${formatTokens(seat.tokens.total)} tokens, mostly the code it read (${seat.tokens_exact ? 'exact' : 'estimate'})`;
}
export function seatTokens(seat: PaqadSeat): string | null {
const parts = [noteTokens(seat), runTokens(seat)].filter((part): part is string => part !== null);
return parts.length > 0 ? parts.join('. ') : null;
}
const LATE_WORDS = 'late: its note landed after the spec was frozen';
function chiefStateWords(chief: PaqadChief, now: number): string {
const words = seatStateWords(chief, now);
return chief.state === 'done' && chief.verdict !== null
? `${words.replace(/^done/, 'ruled')}, verdict: ${chief.verdict}`
: words;
}
function seatLine(seat: PaqadSeat, now: number, withReason: boolean): TableLine {
const parts = [seatStateWords(seat, now)];
const tokens = seatTokens(seat);
if (tokens) parts.push(tokens);
if (seat.findings !== null) parts.push(plural(seat.findings, 'finding'));
const line: TableLine = [
{ text: ' ' },
dotSegment(dotOf(seat)),
{ text: ` ${roleWords(seat.role)}: ${parts.join(', ')}` },
];
if (seat.late) line.push({ text: `, ${LATE_WORDS}`, tone: 'warn' });
if (withReason && seat.reason) line.push({ text: `. Why: ${seat.reason}`, dim: true });
return line;
}
function chiefLine(chief: PaqadChief, now: number): TableLine {
const parts = [chiefStateWords(chief, now)];
const tokens = seatTokens(chief);
if (tokens) parts.push(tokens);
const line: TableLine = [
{ text: ' ' },
dotSegment(dotOf(chief)),
{ text: ` ${CHIEF_WORDS}: ${parts.join(', ')}` },
];
if (chief.late) line.push({ text: `, ${LATE_WORDS}`, tone: 'warn' });
return line;
}
function findingsTotal(table: PaqadHighTable): number | null {
const counted = table.seats.filter((seat) => seat.findings !== null);
return counted.length === 0 ? null : counted.reduce((sum, seat) => sum + (seat.findings ?? 0), 0);
}
export function rulingCounts(table: PaqadHighTable): string {
const rulings = table.rulings;
const total = findingsTotal(table);
const parts: string[] = [];
if (total !== null) parts.push(plural(total, 'finding'));
if (rulings) {
parts.push(`${rulings.accepted.length} accepted`, `${rulings.declined.length} declined`);
if (rulings.renames.length > 0) parts.push(`${rulings.renames.length} renamed`);
if (rulings.conflicts.length > 0) parts.push(plural(rulings.conflicts.length, 'disagreement'));
if (rulings.merged.length > 0) parts.push(`${rulings.merged.length} merged`);
if (rulings.settled.length > 0) parts.push(`${rulings.settled.length} settled before`);
}
if (table.gaps.length > 0) parts.push(plural(table.gaps.length, 'gap'));
return parts.join(', ');
}
export function isExpertsCall(tool: string, input: unknown): boolean {
const command = (input as { command?: unknown } | null | undefined)?.command;
return (
tool === 'Bash' && typeof command === 'string' && /paqad-ai spec pipeline experts/.test(command)
);
}
export function cardKey(snapshot: PaqadHudSnapshot | null): string {
return snapshot?.change?.id ?? snapshot?.change?.issue ?? 'session';
}
export function tableCardLines(table: PaqadHighTable, now: number): TableLine[] {
const ruled = hasRuled(table);
if (table.skipped) {
return [[{ text: TABLE_NAME, tone: 'warn' }, { text: SKIPPED_TEXT.slice(TABLE_NAME.length) }]];
}
const summary = ruled
? `the chief architect ruled: ${table.chief?.verdict ?? ''}`
: tableSummary(instrumentOf(table, 0)) || 'nobody seated yet';
const lines: TableLine[] = [
[
{ text: TABLE_NAME, tone: ruled ? 'good' : 'working' },
{ text: ` · ${plural(table.seats.length, 'seat')} · ${summary}` },
],
...table.seats.map((seat) => seatLine(seat, now, false)),
];
if (table.chief) lines.push(chiefLine(table.chief, now));
else lines.push([{ text: ` ${SEAT_MARK.seated} ${CHIEF_WORDS}: not seated yet`, dim: true }]);
if (ruled) {
lines.push([{ text: `Verdict: ${table.chief?.verdict ?? ''}. ${rulingCounts(table)}.` }]);
}
if (table.met_after_freeze) {
lines.push([
{
text: 'The High Table met after the spec was frozen, so no spec line is credited to it.',
tone: 'warn',
},
]);
}
return lines;
}
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export function decisionDate(id: string): string | null {
const match = /^D-([0-9A-HJKMNP-TV-Z]{26})$/i.exec(id);
if (!match) return null;
let time = 0;
for (const char of match[1]!.slice(0, 10).toUpperCase())
time = time * 32 + CROCKFORD.indexOf(char);
const date = new Date(time);
return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
}
function originWords(origin: string | null): string | null {
if (origin === 'standing') return 'a standing seat, always at the table';
if (origin === 'detector') return 'picked for this change';
return origin;
}
export interface TableSeatButton {
role: string;
key: string;
label: string;
selected: boolean;
}
export interface TablePaneView {
svg: { source: string; alt: string };
ring: TableLine[];
seats: TableLine[];
buttons: TableSeatButton[];
detail: TableLine[];
rulings: TableLine[];
}
export function seatKey(role: string): string {
return `paqad-seat-${role}`;
}
const escapeXml = (text: string): string =>
text.replace(/[<>&"']/g, (char) => `&#${char.charCodeAt(0)};`);
function seatColor(seat: PaqadSeat): string {
return seat.late ? SEAT_COLOR.late : SEAT_COLOR[seat.state];
}
function seatWords(seat: PaqadSeat): string {
return `${roleWords(seat.role)} ${seat.state}${seat.late ? ', late' : ''}`;
}
export function tableSvg(table: PaqadHighTable): string {
const size = 160;
const centre = size / 2;
const radius = 62;
const gap = 6;
const n = Math.max(1, table.seats.length);
const arcs = table.seats.map((seat, index) => {
const from = ((index * 360) / n + gap / 2 - 90) * (Math.PI / 180);
const to = (((index + 1) * 360) / n - gap / 2 - 90) * (Math.PI / 180);
const point = (angle: number) =>
`${(centre + radius * Math.cos(angle)).toFixed(2)} ${(centre + radius * Math.sin(angle)).toFixed(2)}`;
const large = 360 / n - gap > 180 ? 1 : 0;
return `<path d="M ${point(from)} A ${radius} ${radius} 0 ${large} 1 ${point(to)}" stroke="${seatColor(seat)}" stroke-width="14" fill="none"><title>${escapeXml(seatWords(seat))}</title></path>`;
});
const chief = table.chief;
const chiefColor = chief ? seatColor(chief) : SEAT_COLOR.seated;
const chiefTitle = chief ? seatWords(chief) : `${CHIEF_WORDS} not seated yet`;
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
...arcs,
`<circle cx="${centre}" cy="${centre}" r="28" fill="none" stroke="${chiefColor}" stroke-width="4"><title>${escapeXml(chiefTitle)}</title></circle>`,
`<text x="${centre}" y="${centre + 5}" text-anchor="middle" font-size="13" fill="#888">Chief</text>`,
'</svg>',
].join('');
}
export function tableAlt(table: PaqadHighTable): string {
const chief = table.chief
? table.chief.state === 'done' && table.chief.verdict !== null
? `the chief architect at the centre, ruled: ${table.chief.verdict}`
: `the chief architect at the centre, ${table.chief.state}`
: 'the chief architect not seated yet';
const seats = table.seats.map(seatWords).join(', ');
return `The High Table: ${chief}. ${plural(table.seats.length, 'seat')} around it${seats ? `: ${seats}` : ''}.`;
}
function detailLines(seat: PaqadSeat | PaqadChief, now: number): TableLine[] {
const isChief = 'verdict' in seat;
const name = isChief ? CHIEF_WORDS : roleWords(seat.role);
const origin = isChief ? 'chairs the table' : originWords(seat.origin);
const lines: TableLine[] = [[{ text: origin ? `${name}, ${origin}.` : `${name}.` }]];
if (seat.reason) lines.push([{ text: `Why: ${seat.reason}` }]);
lines.push([
{ text: `State: ${isChief ? chiefStateWords(seat, now) : seatStateWords(seat, now)}.` },
]);
const clamp =
seat.clamped === true
? ', cut to the ceiling'
: seat.clamped === false
? ', within the ceiling'
: '';
const note = noteTokens(seat);
lines.push([
{
text: note
? `${note}${clamp}.`
: seat.budget_tokens !== null
? `Note: not written yet, its budget is ${formatTokens(seat.budget_tokens)} tokens${clamp}.`
: 'Note: not written yet.',
},
]);
const run = runTokens(seat);
if (run) lines.push([{ text: `${run}.` }]);
const record = seat.track_record;
if (record) lines.push([{ text: trackRecordWords(record) }]);
if (!isChief) {
lines.push([
{
text: seat.findings === null ? 'Findings: not yet.' : `Findings: ${seat.findings}.`,
},
]);
}
if (seat.late) {
lines.push([
{
text: 'Late: its note landed after the spec was frozen, so it shaped no spec line.',
tone: 'warn',
},
]);
}
return lines;
}
export function trackRecordWords(record: PaqadTrackRecord): string {
const parts = [
`on ${plural(record.changes, 'earlier change')}`,
`changed the spec on ${record.changed_spec}`,
];
if (record.empty > 0) parts.push(`had nothing to add on ${record.empty}`);
if (record.avg_tokens > 0) parts.push(`about ${fullTokens(record.avg_tokens)} tokens each`);
return `Track record: ${parts.join(', ')}.`;
}
function settledLine(entry: { target: string; chosen: string; source: string }): TableLine {
const date = decisionDate(entry.source);
const where = entry.source ? ` in ${entry.source}` : '';
const when = date ? ` on ${date}` : '';
return [
{
text: `Settled before: ${entry.target}. Chosen: ${entry.chosen}. Decided${when}${where}, so nobody was asked again.`,
},
];
}
function rulingLines(snapshot: PaqadHudSnapshot & { table: PaqadHighTable }): TableLine[] {
const table = snapshot.table;
const lines: TableLine[] = [[{ text: "The chief architect's rulings:" }]];
const rulings = table.rulings;
if (!hasRuled(table) || !rulings) {
lines.push([{ text: ' Not yet. The chief architect rules once every seat has written.' }]);
} else {
lines.push([{ text: ` Verdict: ${table.chief?.verdict ?? ''}. ${rulingCounts(table)}.` }]);
for (const declined of rulings.declined) {
lines.push([{ text: ` Declined: ${declined.id}. Reason: ${declined.reason}` }]);
}
for (const rename of rulings.renames) {
lines.push([{ text: ` Renamed: ${rename.id} now reads "${rename.target}".` }]);
}
for (const conflict of rulings.conflicts) {
const open = isOpenConflict(snapshot, conflict.target);
lines.push([
open
? { text: ` Open disagreement: ${conflict.target}.`, tone: 'warn' }
: { text: ` Disagreement: ${conflict.target}.` },
{
text: ` The chief architect recommends: ${conflict.recommendation}${conflict.rationale ? ` Why: ${conflict.rationale}` : ''}`,
},
...(open ? [{ text: ' The choice is yours.' }] : []),
]);
}
for (const merged of rulings.merged) {
lines.push([
{
text: ` Merged: ${merged.target}. The experts agree: ${merged.merged_claim}${merged.rationale ? ` Why: ${merged.rationale}` : ''}`,
},
]);
}
for (const settled of rulings.settled) lines.push([{ text: ' ' }, ...settledLine(settled)]);
}
for (const gap of table.gaps) {
lines.push([
{ text: ` Gap no expert covered: ${gap.area}. ${gap.why_it_matters}`.trimEnd() },
...(gap.question ? [{ text: ` Question for you: ${gap.question}` }] : []),
]);
}
for (const answered of table.questions?.auto_answered ?? []) {
lines.push([
{
text: ` Answered from an earlier decision: ${answered.question} Source: ${answered.source || 'not recorded'}.`,
},
]);
}
lines.push(...creditLines(snapshot));
return lines;
}
function creditLines(snapshot: PaqadHudSnapshot & { table: PaqadHighTable }): TableLine[] {
const table = snapshot.table;
if (table.met_after_freeze) {
return [
[
{
text: 'Who shaped the spec: no spec line. The High Table met after the spec was frozen.',
tone: 'warn',
},
],
];
}
if (table.credits.length === 0) {
return [
[
{
text: specFrozen(snapshot)
? 'Who shaped the spec: no spec line came from an expert.'
: WHO_SHAPED,
},
],
...(specFrozen(snapshot) ? [] : [[{ text: CREDITS_LATER }]]),
];
}
return [
[{ text: `Who shaped the spec (${plural(table.credits.length, 'line')}):` }],
...table.credits.map((credit): TableLine => [
{ text: ` ${credit.line}: ${credit.roles.map(roleWords).join(', ')}` },
]),
];
}
export function tablePaneView(
snapshot: PaqadHudSnapshot & { table: PaqadHighTable },
pickedSeat: string | null,
now: number,
): TablePaneView {
const table = snapshot.table;
const everyone: Array<PaqadSeat | PaqadChief> = [
...table.seats,
...(table.chief ? [table.chief] : []),
];
const picked = everyone.find((seat) => seat.role === pickedSeat) ?? everyone[0] ?? null;
const ring: TableLine[] = [
[
{ text: `${TABLE_NAME}: ` },
...(table.chief
? [dotSegment(dotOf(table.chief)), { text: ` ${CHIEF_WORDS} at the centre` }]
: [{ text: `${CHIEF_WORDS} not seated yet`, dim: true }]),
{ text: `, ${plural(table.seats.length, 'seat')} around it` },
],
[
{ text: ' ' },
...table.seats.flatMap((seat, index): TableSegment[] => [
...(index > 0 ? [{ text: ' ' }] : []),
dotSegment(dotOf(seat)),
]),
...(table.seats.length > 0 ? [{ text: ` ${tableSummary(instrumentOf(table, 0))}` }] : []),
],
];
return {
svg: { source: tableSvg(table), alt: tableAlt(table) },
ring,
seats: [
...table.seats.map((seat) => seatLine(seat, now, true)),
...(table.chief ? [chiefLine(table.chief, now)] : []),
],
buttons: everyone.map((seat) => {
const label = 'verdict' in seat ? CHIEF_WORDS : roleWords(seat.role);
const selected = seat === picked;
return {
role: seat.role,
key: seatKey(seat.role),
label: selected ? `${label} (shown)` : label,
selected,
};
}),
detail: picked ? detailLines(picked, now) : [],
rulings: rulingLines(snapshot),
};
}
export function noTableText(snapshot: PaqadHudSnapshot | null): string {
if (!snapshot) return 'Not yet. paqad has no record for this session.';
if (!snapshot.active) return 'No change is in progress, so there is no High Table to show.';
if (snapshot.lane === 'fast') return 'The fast lane seats no High Table.';
return 'No High Table for this change: the experts are off or not seated yet.';
}