feat(tooling): add db:insert-milestone for idempotent backlog inserts (#7)
Een herbruikbaar script dat één milestone (PBI + stories + tasks) uit
docs/scrum4me-backlog.md leest en idempotent toevoegt aan de DB,
zonder bestaande data of sprint-toewijzingen te raken.
npm run db:insert-milestone -- M8 # standaard SCRUM4ME
npm run db:insert-milestone -- M8 --product CODE # ander product
npm run db:insert-milestone -- M8 --dry-run # niets schrijven
Parser-uitbreiding (parse-backlog.ts):
- M7 en M8 toegevoegd aan priority/goal/sprint_status maps
- Onbekende milestones krijgen defaults (priority 4, sprint COMPLETED,
goal = header-titel) zodat M9..M∞ vanzelf werken
- Optionele { strict: false } skipt de "≥ 8 milestones / ≥ 60 stories"
asserts; de bestaande seed-flow behoudt strict: true (default)
Idempotency:
- Pbi upsert by (product_id, code = milestone.key)
- Story upsert by (product_id, code = ST-XXX)
- Tasks alleen aangemaakt als de story op dit moment 0 tasks heeft
Sprint-toewijzing wordt expliciet niet meegenomen — stories landen in
de backlog en moeten via Sprint Planning in een sprint geplaatst worden.
Geverifieerd: M8 inserted (PBI + 6 stories + 6 tasks), tweede run laat
zien "PBI reused, 0 created" — geen duplicaten.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6c6a3e928
commit
f6132960a5
3 changed files with 230 additions and 19 deletions
|
|
@ -42,6 +42,8 @@ const MILESTONE_PRIORITY: Record<string, 1 | 2 | 3 | 4> = {
|
|||
M4: 2,
|
||||
M5: 3,
|
||||
M6: 4,
|
||||
M7: 4,
|
||||
M8: 4,
|
||||
}
|
||||
|
||||
const MILESTONE_GOAL: Record<string, string> = {
|
||||
|
|
@ -53,6 +55,8 @@ const MILESTONE_GOAL: Record<string, string> = {
|
|||
M4: 'Alle endpoints, tokenbeheer',
|
||||
M5: 'Todo CRUD, promotie naar PBI/story; Data Table + detail-kaart',
|
||||
M6: 'Foutafhandeling, toegankelijkheid, CI/CD, beveiliging',
|
||||
M7: 'MCP-server voor Claude Code',
|
||||
M8: 'Realtime updates voor Solo Paneel',
|
||||
}
|
||||
|
||||
const MILESTONE_SPRINT_STATUS: Record<string, ParsedMilestone['sprint_status']> = {
|
||||
|
|
@ -64,9 +68,11 @@ const MILESTONE_SPRINT_STATUS: Record<string, ParsedMilestone['sprint_status']>
|
|||
M4: 'COMPLETED',
|
||||
M5: 'COMPLETED',
|
||||
M6: 'COMPLETED',
|
||||
M7: 'COMPLETED',
|
||||
M8: 'COMPLETED',
|
||||
}
|
||||
|
||||
const KNOWN_KEYS = Object.keys(MILESTONE_PRIORITY)
|
||||
const MILESTONE_KEY = /^M[\d.]+$/
|
||||
|
||||
type SubBullet = {
|
||||
headLine: string
|
||||
|
|
@ -96,7 +102,11 @@ function buildAcceptance(b: SubBullet): string {
|
|||
return [head, nested].filter(Boolean).join('\n').trim()
|
||||
}
|
||||
|
||||
export async function loadBacklog(repoRoot: string): Promise<ParsedMilestone[]> {
|
||||
export async function loadBacklog(
|
||||
repoRoot: string,
|
||||
options: { strict?: boolean } = {},
|
||||
): Promise<ParsedMilestone[]> {
|
||||
const { strict = true } = options
|
||||
const file = path.join(repoRoot, 'docs/scrum4me-backlog.md')
|
||||
const md = await readFile(file, 'utf8')
|
||||
|
||||
|
|
@ -126,15 +136,16 @@ export async function loadBacklog(repoRoot: string): Promise<ParsedMilestone[]>
|
|||
|
||||
for (const raw of md.split('\n')) {
|
||||
const headerMatch = raw.match(MILESTONE_HEADER)
|
||||
if (headerMatch && KNOWN_KEYS.includes(headerMatch[1])) {
|
||||
if (headerMatch && MILESTONE_KEY.test(headerMatch[1])) {
|
||||
flushPending()
|
||||
const key = headerMatch[1]
|
||||
const title = headerMatch[2]
|
||||
current = {
|
||||
key,
|
||||
title: headerMatch[2],
|
||||
goal: MILESTONE_GOAL[key],
|
||||
priority: MILESTONE_PRIORITY[key],
|
||||
sprint_status: MILESTONE_SPRINT_STATUS[key],
|
||||
title,
|
||||
goal: MILESTONE_GOAL[key] ?? title,
|
||||
priority: MILESTONE_PRIORITY[key] ?? 4,
|
||||
sprint_status: MILESTONE_SPRINT_STATUS[key] ?? 'COMPLETED',
|
||||
sort_order: milestones.length + 1,
|
||||
stories: [],
|
||||
}
|
||||
|
|
@ -193,17 +204,18 @@ export async function loadBacklog(repoRoot: string): Promise<ParsedMilestone[]>
|
|||
|
||||
flushPending()
|
||||
|
||||
if (milestones.length < 8) {
|
||||
throw new Error(
|
||||
`Backlog parser found only ${milestones.length} milestones (expected 8). Format may have drifted in ${file}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const totalStories = milestones.reduce((acc, m) => acc + m.stories.length, 0)
|
||||
if (totalStories < 60) {
|
||||
throw new Error(
|
||||
`Backlog parser found only ${totalStories} stories (expected ≥ 60). Format may have drifted in ${file}.`,
|
||||
)
|
||||
if (strict) {
|
||||
if (milestones.length < 8) {
|
||||
throw new Error(
|
||||
`Backlog parser found only ${milestones.length} milestones (expected 8). Format may have drifted in ${file}.`,
|
||||
)
|
||||
}
|
||||
const totalStories = milestones.reduce((acc, m) => acc + m.stories.length, 0)
|
||||
if (totalStories < 60) {
|
||||
throw new Error(
|
||||
`Backlog parser found only ${totalStories} stories (expected ≥ 60). Format may have drifted in ${file}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return milestones
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue