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:
77
src/parser.ts
Normal file
77
src/parser.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user