From a5f81fce700f33ee33cd175edaf6202f5c108b5e Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 17:39:11 +0200 Subject: [PATCH] feat(ST-004): emit one task per backlog sub-bullet, not per story Co-Authored-By: Claude Opus 4.7 (1M context) --- prisma/seed-data/parse-backlog.ts | 90 ++++++++++++++++++++++++------- prisma/seed.ts | 27 +++++----- 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/prisma/seed-data/parse-backlog.ts b/prisma/seed-data/parse-backlog.ts index 757d991..f8fbeff 100644 --- a/prisma/seed-data/parse-backlog.ts +++ b/prisma/seed-data/parse-backlog.ts @@ -1,13 +1,19 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' +export type ParsedTask = { + title: string + description: string + sort_order: number +} + export type ParsedStory = { ref: string title: string - description: string acceptance_criteria: string status: 'DONE' | 'OPEN' sort_order: number + tasks: ParsedTask[] } export type ParsedMilestone = { @@ -25,6 +31,7 @@ const TASK_BULLET = /^- \[(x| )\] \*\*(ST-\d+)\*\*\s+(.+?)\s*$/ const SUB_BULLET = /^ {2}- (.+?)\s*$/ const NESTED_LINE = /^ {4,}\S/ const SECTION_BREAK = /^---\s*$/ +const BOLD_PREFIX = /^\*\*([^*]+?)\*\*\s*:?\s*(.*)$/ const MILESTONE_PRIORITY: Record = { M0: 1, @@ -61,17 +68,59 @@ const MILESTONE_SPRINT_STATUS: Record const KNOWN_KEYS = Object.keys(MILESTONE_PRIORITY) +type SubBullet = { + headLine: string + nestedLines: string[] + isAcceptance: boolean +} + +function buildTask(b: SubBullet, sort_order: number): ParsedTask { + const nested = b.nestedLines.join('\n') + const m = b.headLine.match(BOLD_PREFIX) + if (m) { + const title = m[1].replace(/`/g, '').replace(/:$/, '').trim() + const description = [m[2].trim(), nested].filter(Boolean).join('\n').trim() + return { title, description, sort_order } + } + // No bold prefix: derive a short title from the first clause; full bullet stays in description. + const head = b.headLine.replace(/`/g, '').trim() + const firstClause = head.split(/[;.]\s/, 1)[0].trim() + const title = firstClause.length > 80 ? firstClause.slice(0, 79).trim() + '…' : firstClause + const description = [b.headLine, nested].filter(Boolean).join('\n').trim() + return { title, description, sort_order } +} + +function buildAcceptance(b: SubBullet): string { + const head = b.headLine.replace(/^Done when:\s*/i, '').trim() + const nested = b.nestedLines.join('\n').trim() + return [head, nested].filter(Boolean).join('\n').trim() +} + export async function loadBacklog(repoRoot: string): Promise { const file = path.join(repoRoot, 'docs/scrum4me-backlog.md') const md = await readFile(file, 'utf8') const milestones: ParsedMilestone[] = [] let current: ParsedMilestone | null = null - let pending: { story: ParsedStory; bodyLines: string[] } | null = null + let pending: { + story: ParsedStory + bullets: SubBullet[] + activeBullet: SubBullet | null + } | null = null const flushPending = () => { if (!pending) return - pending.story.description = pending.bodyLines.join('\n').trim() + if (pending.activeBullet) { + pending.bullets.push(pending.activeBullet) + pending.activeBullet = null + } + const taskBullets = pending.bullets.filter((b) => !b.isAcceptance) + const acceptanceBullets = pending.bullets.filter((b) => b.isAcceptance) + pending.story.tasks = taskBullets.map((b, i) => buildTask(b, i + 1)) + pending.story.acceptance_criteria = acceptanceBullets + .map(buildAcceptance) + .filter(Boolean) + .join('\n') pending = null } @@ -106,13 +155,13 @@ export async function loadBacklog(repoRoot: string): Promise const story: ParsedStory = { ref: taskMatch[2], title: `${taskMatch[2]}: ${taskMatch[3]}`, - description: '', acceptance_criteria: '', status: taskMatch[1] === 'x' ? 'DONE' : 'OPEN', sort_order: current.stories.length + 1, + tasks: [], } current.stories.push(story) - pending = { story, bodyLines: [] } + pending = { story, bullets: [], activeBullet: null } continue } @@ -120,22 +169,20 @@ export async function loadBacklog(repoRoot: string): Promise const subMatch = raw.match(SUB_BULLET) if (subMatch) { + if (pending.activeBullet) { + pending.bullets.push(pending.activeBullet) + } const content = subMatch[1] - if (/^Done when:/i.test(content)) { - pending.story.acceptance_criteria = content.replace(/^Done when:\s*/i, '').trim() - } else { - pending.bodyLines.push(content) + pending.activeBullet = { + headLine: content.replace(/^Done when:\s*/i, ''), + nestedLines: [], + isAcceptance: /^Done when:/i.test(content), } continue } - if (NESTED_LINE.test(raw)) { - const tail = raw.trim() - if (pending.bodyLines.length > 0) { - pending.bodyLines[pending.bodyLines.length - 1] += '\n' + tail - } else { - pending.bodyLines.push(tail) - } + if (NESTED_LINE.test(raw) && pending.activeBullet) { + pending.activeBullet.nestedLines.push(raw.trim()) continue } @@ -172,10 +219,17 @@ if (import.meta.url === `file://${process.argv[1]}`) { 0, ) const open = total - done - console.log(`Parsed ${milestones.length} milestones, ${total} stories (${done} DONE, ${open} OPEN)`) + const totalTasks = milestones.reduce( + (acc, m) => acc + m.stories.reduce((a, s) => a + s.tasks.length, 0), + 0, + ) + console.log( + `Parsed ${milestones.length} milestones, ${total} stories (${done} DONE, ${open} OPEN), ${totalTasks} tasks`, + ) for (const m of milestones) { + const ms = m.stories.reduce((a, s) => a + s.tasks.length, 0) console.log( - ` ${m.key.padEnd(5)} ${m.title.padEnd(36)} priority=${m.priority} sprint=${m.sprint_status} stories=${m.stories.length}`, + ` ${m.key.padEnd(5)} ${m.title.padEnd(36)} priority=${m.priority} sprint=${m.sprint_status} stories=${m.stories.length} tasks=${ms}`, ) } }) diff --git a/prisma/seed.ts b/prisma/seed.ts index 19324c1..e5a56a8 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -150,6 +150,7 @@ async function main() { const inSprint = isActive || s.status === 'DONE' const storyStatus = s.status === 'DONE' ? 'DONE' : isActive ? 'IN_SPRINT' : 'OPEN' + const storySummary = s.tasks.map((t) => t.title).join('; ') const story = await prisma.story.create({ data: { @@ -157,7 +158,7 @@ async function main() { product_id: product.id, sprint_id: inSprint ? sprint.id : null, title: s.title, - description: s.description, + description: storySummary, acceptance_criteria: s.acceptance_criteria, priority: ms.priority, sort_order: s.sort_order, @@ -165,17 +166,19 @@ async function main() { }, }) - await prisma.task.create({ - data: { - story_id: story.id, - sprint_id: inSprint ? sprint.id : null, - title: 'Implementatie', - description: s.description, - priority: ms.priority, - sort_order: 1.0, - status: s.status === 'DONE' ? 'DONE' : 'TO_DO', - }, - }) + for (const t of s.tasks) { + await prisma.task.create({ + data: { + story_id: story.id, + sprint_id: inSprint ? sprint.id : null, + title: t.title, + description: t.description, + priority: ms.priority, + sort_order: t.sort_order, + status: s.status === 'DONE' ? 'DONE' : 'TO_DO', + }, + }) + } } }