feat(reader): STATUS.md parser + TS project scaffold [skip-tdd: visual for configs]

- vitest+TS scaffold (package.json/tsconfig/vitest.config = config artifacts)
- src/parser.ts: parses STATUS.md blocks → StatusBlock[]
- tests/parser.test.ts: 6 cases (single, multi, blocker, null-blocker, empty, unknown emoji)
- fix emoji contract: align with using-tasks reality ( open / 🔴 in_progress / 🟡 paused / 🔵 blocked / 🟢 done); reader-task acceptance + design doc updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vitya
2026-05-22 12:44:11 +03:00
parent ba33f8cf27
commit 1c7e372abf
10 changed files with 1688 additions and 3 deletions

77
src/parser.ts Normal file
View File

@@ -0,0 +1,77 @@
export type Status = 'open' | 'in_progress' | 'paused' | 'blocked' | 'done';
export type StatusEmoji = '⚪' | '🔴' | '🟡' | '🔵' | '🟢';
export interface StatusBlock {
slug: string;
title: string;
status: Status;
status_emoji: StatusEmoji;
where_stopped: string | null;
next_action: string | null;
blocker: string | null;
branch: string | null;
}
const EMOJI_TO_STATUS: Record<StatusEmoji, Status> = {
'⚪': 'open',
'🔴': 'in_progress',
'🟡': 'paused',
'🔵': 'blocked',
'🟢': 'done',
};
const KNOWN_EMOJIS = Object.keys(EMOJI_TO_STATUS) as StatusEmoji[];
const H2_RE = /^##\s+(\S+)\s+\[([^\]]+)\]\s+\s+(.+?)\s*$/u;
const FIELD_RE = /^\*\*([^:*]+):\*\*\s*(.*)$/;
export function parseStatus(text: string): StatusBlock[] {
const blocks: StatusBlock[] = [];
for (const rawBlock of splitBlocks(text)) {
const block = parseOneBlock(rawBlock);
if (block !== null) blocks.push(block);
}
return blocks;
}
function splitBlocks(text: string): string[] {
return text.split(/^---\s*$/m).map((s) => s.trim()).filter(Boolean);
}
function parseOneBlock(raw: string): StatusBlock | null {
const lines = raw.split(/\r?\n/);
let header: { emoji: StatusEmoji; slug: string; title: string } | null = null;
const fields: Record<string, string> = {};
for (const line of lines) {
if (header === null) {
const m = H2_RE.exec(line);
if (m === null) continue;
const [, emoji, slug, title] = m;
if (emoji === undefined || slug === undefined || title === undefined) continue;
if (!KNOWN_EMOJIS.includes(emoji as StatusEmoji)) continue;
header = { emoji: emoji as StatusEmoji, slug, title };
continue;
}
const fm = FIELD_RE.exec(line);
if (fm !== null) {
const [, key, value] = fm;
if (key !== undefined && value !== undefined) fields[key.trim()] = value.trim();
}
}
if (header === null) return null;
return {
slug: header.slug,
title: header.title,
status: EMOJI_TO_STATUS[header.emoji],
status_emoji: header.emoji,
where_stopped: fields['Where I stopped'] ?? null,
next_action: fields['Next action'] ?? null,
blocker: fields['Blocker'] ?? null,
branch: fields['Branch'] ?? null,
};
}