feat(PBI-12 T-51): voeg create_sprint tool toe
Maakt een sprint aan met status=OPEN. Code auto-gegenereerd als
S-{YYYY-MM-DD}-{N} per product per datum als niet meegegeven, met retry
bij race-conflict op @@unique([product_id, code]). Volgt create-pbi.ts
template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ffa25f053
commit
268e926187
1 changed files with 124 additions and 0 deletions
124
src/tools/create-sprint.ts
Normal file
124
src/tools/create-sprint.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// MCP authoring tool: create een Sprint binnen een product.
|
||||
//
|
||||
// Status start altijd op OPEN; geen reuse-check op bestaande OPEN-sprints
|
||||
// (per plan-to-pbi-flow.md "altijd nieuwe sprint"). Code wordt auto-gegenereerd
|
||||
// als S-{YYYY-MM-DD}-{N} per product per datum, met retry bij race-condition
|
||||
// op de unique constraint (@@unique([product_id, code])).
|
||||
|
||||
import { z } from 'zod'
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { prisma } from '../prisma.js'
|
||||
import { requireWriteAccess } from '../auth.js'
|
||||
import { userCanAccessProduct } from '../access.js'
|
||||
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||
|
||||
const SPRINT_AUTO_RE = /^S-(\d{4}-\d{2}-\d{2})-(\d+)$/
|
||||
const MAX_CODE_ATTEMPTS = 3
|
||||
|
||||
function todayIsoDate(): string {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
async function generateNextSprintCode(productId: string): Promise<string> {
|
||||
const today = todayIsoDate()
|
||||
const sprints = await prisma.sprint.findMany({
|
||||
where: { product_id: productId, code: { startsWith: `S-${today}-` } },
|
||||
select: { code: true },
|
||||
})
|
||||
let max = 0
|
||||
for (const s of sprints) {
|
||||
const m = s.code?.match(SPRINT_AUTO_RE)
|
||||
if (m) {
|
||||
const n = Number.parseInt(m[2], 10)
|
||||
if (!Number.isNaN(n) && n > max) max = n
|
||||
}
|
||||
}
|
||||
return `S-${today}-${max + 1}`
|
||||
}
|
||||
|
||||
function isCodeUniqueConflict(error: unknown): boolean {
|
||||
if (!(error instanceof Prisma.PrismaClientKnownRequestError)) return false
|
||||
if (error.code !== 'P2002') return false
|
||||
const target = (error.meta as { target?: string[] | string } | undefined)?.target
|
||||
if (!target) return false
|
||||
return Array.isArray(target) ? target.includes('code') : target.includes('code')
|
||||
}
|
||||
|
||||
const inputSchema = z.object({
|
||||
product_id: z.string().min(1),
|
||||
code: z.string().min(1).max(30).optional(),
|
||||
sprint_goal: z.string().min(1).max(500),
|
||||
start_date: z.string().date().optional(),
|
||||
})
|
||||
|
||||
export function registerCreateSprintTool(server: McpServer) {
|
||||
server.registerTool(
|
||||
'create_sprint',
|
||||
{
|
||||
title: 'Create Sprint',
|
||||
description:
|
||||
'Create a new sprint for a product with status=OPEN. Code auto-generated as S-{YYYY-MM-DD}-{N} per product per date if not provided. Forbidden for demo accounts.',
|
||||
inputSchema,
|
||||
},
|
||||
async ({ product_id, code, sprint_goal, start_date }) =>
|
||||
withToolErrors(async () => {
|
||||
const auth = await requireWriteAccess()
|
||||
if (!(await userCanAccessProduct(product_id, auth.userId))) {
|
||||
return toolError(`Product ${product_id} not found or not accessible`)
|
||||
}
|
||||
|
||||
const resolvedStartDate = start_date ? new Date(start_date) : new Date()
|
||||
|
||||
if (code) {
|
||||
const sprint = await prisma.sprint.create({
|
||||
data: {
|
||||
product_id,
|
||||
code,
|
||||
sprint_goal,
|
||||
status: 'OPEN',
|
||||
start_date: resolvedStartDate,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
sprint_goal: true,
|
||||
status: true,
|
||||
start_date: true,
|
||||
created_at: true,
|
||||
},
|
||||
})
|
||||
return toolJson(sprint)
|
||||
}
|
||||
|
||||
let lastError: unknown
|
||||
for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) {
|
||||
const generated = await generateNextSprintCode(product_id)
|
||||
try {
|
||||
const sprint = await prisma.sprint.create({
|
||||
data: {
|
||||
product_id,
|
||||
code: generated,
|
||||
sprint_goal,
|
||||
status: 'OPEN',
|
||||
start_date: resolvedStartDate,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
sprint_goal: true,
|
||||
status: true,
|
||||
start_date: true,
|
||||
created_at: true,
|
||||
},
|
||||
})
|
||||
return toolJson(sprint)
|
||||
} catch (e) {
|
||||
if (isCodeUniqueConflict(e)) { lastError = e; continue }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error('Kon geen unieke sprint-code genereren')
|
||||
}),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue