feat(PBI-12): code-bindende sort_order + priority uit story/taak-orderings

- Voeg parseCodeNumber-helper toe in src/lib/code.ts
- create-story/create-task: sort_order = parseCodeNumber(code), sort_order-inputparam verwijderd
- get-claude-context + wait-for-job: priority verwijderd uit story/taak orderBy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-14 17:49:40 +02:00
parent 55fa133150
commit 870d1d356a
5 changed files with 21 additions and 30 deletions

10
src/lib/code.ts Normal file
View file

@ -0,0 +1,10 @@
// Sync met Scrum4Me/lib/code.ts — bewust duplicate (geen gedeeld package)
// om de MCP-server eigenstandig te houden.
//
// Extraheert het achterste getal uit een code-string (bijv. "ST-001" → 1,
// "T-42" → 42). Gebruikt als sort_order bij create_story / create_task.
export function parseCodeNumber(code: string | null | undefined): number {
if (!code) return 0
const m = code.match(/(\d+)$/)
return m ? Number.parseInt(m[1], 10) : 0
}

View file

@ -12,6 +12,7 @@ import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js' import { requireWriteAccess } from '../auth.js'
import { userCanAccessProduct } from '../access.js' import { userCanAccessProduct } from '../access.js'
import { toolError, toolJson, withToolErrors } from '../errors.js' import { toolError, toolJson, withToolErrors } from '../errors.js'
import { parseCodeNumber } from '../lib/code.js'
const STORY_AUTO_RE = /^ST-(\d+)$/ const STORY_AUTO_RE = /^ST-(\d+)$/
const MAX_CODE_ATTEMPTS = 3 const MAX_CODE_ATTEMPTS = 3
@ -46,7 +47,6 @@ const inputSchema = z.object({
description: z.string().max(4000).optional(), description: z.string().max(4000).optional(),
acceptance_criteria: z.string().max(4000).optional(), acceptance_criteria: z.string().max(4000).optional(),
priority: z.number().int().min(1).max(4), priority: z.number().int().min(1).max(4),
sort_order: z.number().optional(),
// Optionele sprint-koppeling: bij creatie de story direct aan een sprint // Optionele sprint-koppeling: bij creatie de story direct aan een sprint
// hangen (status=IN_SPRINT). De sprint moet bij hetzelfde product horen. // hangen (status=IN_SPRINT). De sprint moet bij hetzelfde product horen.
sprint_id: z.string().min(1).optional(), sprint_id: z.string().min(1).optional(),
@ -59,7 +59,6 @@ export async function handleCreateStory(
description, description,
acceptance_criteria, acceptance_criteria,
priority, priority,
sort_order,
sprint_id, sprint_id,
}: z.infer<typeof inputSchema>, }: z.infer<typeof inputSchema>,
) { ) {
@ -90,19 +89,10 @@ export async function handleCreateStory(
} }
} }
let resolvedSortOrder = sort_order
if (resolvedSortOrder === undefined) {
const last = await prisma.story.findFirst({
where: { pbi_id, priority },
orderBy: { sort_order: 'desc' },
select: { sort_order: true },
})
resolvedSortOrder = (last?.sort_order ?? 0) + 1.0
}
let lastError: unknown let lastError: unknown
for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) { for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) {
const code = await generateNextStoryCode(pbi.product_id) const code = await generateNextStoryCode(pbi.product_id)
const resolvedSortOrder = parseCodeNumber(code)
try { try {
const story = await prisma.story.create({ const story = await prisma.story.create({
data: { data: {
@ -146,7 +136,7 @@ export function registerCreateStoryTool(server: McpServer) {
{ {
title: 'Create story', title: 'Create story',
description: description:
'Add a story under an existing PBI. Optionally link it to a sprint via sprint_id — when given, the story is created with status=IN_SPRINT and the sprint must belong to the same product as the PBI; otherwise status=OPEN and the story lands in the product backlog. Sort_order auto-set to last+1 within the PBI/priority group if not provided. Forbidden for demo accounts.', 'Add a story under an existing PBI. Optionally link it to a sprint via sprint_id — when given, the story is created with status=IN_SPRINT and the sprint must belong to the same product as the PBI; otherwise status=OPEN and the story lands in the product backlog. Sort_order is derived from the auto-generated code number. Forbidden for demo accounts.',
inputSchema, inputSchema,
}, },
handleCreateStory, handleCreateStory,

View file

@ -10,6 +10,7 @@ import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js' import { requireWriteAccess } from '../auth.js'
import { userCanAccessProduct } from '../access.js' import { userCanAccessProduct } from '../access.js'
import { toolError, toolJson, withToolErrors } from '../errors.js' import { toolError, toolJson, withToolErrors } from '../errors.js'
import { parseCodeNumber } from '../lib/code.js'
const TASK_AUTO_RE = /^T-(\d+)$/ const TASK_AUTO_RE = /^T-(\d+)$/
const MAX_CODE_ATTEMPTS = 3 const MAX_CODE_ATTEMPTS = 3
@ -44,7 +45,6 @@ const inputSchema = z.object({
description: z.string().max(4000).optional(), description: z.string().max(4000).optional(),
implementation_plan: z.string().max(8000).optional(), implementation_plan: z.string().max(8000).optional(),
priority: z.number().int().min(1).max(4), priority: z.number().int().min(1).max(4),
sort_order: z.number().optional(),
// Cross-repo override: zet expliciet de repo waarop de worker deze task // Cross-repo override: zet expliciet de repo waarop de worker deze task
// moet uitvoeren (overrides product.repo_url). Gebruik dit voor PBI's die // moet uitvoeren (overrides product.repo_url). Gebruik dit voor PBI's die
// werk in meerdere repos coördineren — bv. PBI op Scrum4Me-product met // werk in meerdere repos coördineren — bv. PBI op Scrum4Me-product met
@ -60,10 +60,10 @@ export function registerCreateTaskTool(server: McpServer) {
{ {
title: 'Create task', title: 'Create task',
description: description:
'Add a task under an existing story. Inherits sprint_id from the story (denormalized). Status defaults to TO_DO. Sort_order auto-set to last+1 within the story/priority group if not provided. Optional repo_url overrides the product.repo_url for cross-repo work (e.g. tasks targeting scrum4me-mcp under a Scrum4Me PBI). Forbidden for demo accounts.', 'Add a task under an existing story. Inherits sprint_id from the story (denormalized). Status defaults to TO_DO. Sort_order is derived from the auto-generated code number. Optional repo_url overrides the product.repo_url for cross-repo work (e.g. tasks targeting scrum4me-mcp under a Scrum4Me PBI). Forbidden for demo accounts.',
inputSchema, inputSchema,
}, },
async ({ story_id, title, description, implementation_plan, priority, sort_order, repo_url }) => async ({ story_id, title, description, implementation_plan, priority, repo_url }) =>
withToolErrors(async () => { withToolErrors(async () => {
const auth = await requireWriteAccess() const auth = await requireWriteAccess()
@ -76,19 +76,10 @@ export function registerCreateTaskTool(server: McpServer) {
return toolError(`Story ${story_id} not accessible`) return toolError(`Story ${story_id} not accessible`)
} }
let resolvedSortOrder = sort_order
if (resolvedSortOrder === undefined) {
const last = await prisma.task.findFirst({
where: { story_id, priority },
orderBy: { sort_order: 'desc' },
select: { sort_order: true },
})
resolvedSortOrder = (last?.sort_order ?? 0) + 1.0
}
let lastError: unknown let lastError: unknown
for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) { for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; attempt++) {
const code = await generateNextTaskCode(story.product_id) const code = await generateNextTaskCode(story.product_id)
const resolvedSortOrder = parseCodeNumber(code)
try { try {
const task = await prisma.task.create({ const task = await prisma.task.create({
data: { data: {

View file

@ -63,7 +63,7 @@ export function registerGetClaudeContextTool(server: McpServer) {
{ tasks: { some: { status: { not: 'DONE' } } } }, { tasks: { some: { status: { not: 'DONE' } } } },
], ],
}, },
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], orderBy: [{ sort_order: 'asc' }],
select: { select: {
id: true, id: true,
code: true, code: true,
@ -73,7 +73,7 @@ export function registerGetClaudeContextTool(server: McpServer) {
priority: true, priority: true,
status: true, status: true,
tasks: { tasks: {
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], orderBy: [{ sort_order: 'asc' }],
select: { select: {
id: true, id: true,
title: true, title: true,

View file

@ -606,10 +606,10 @@ export async function getFullJobContext(jobId: string) {
}, },
tasks: { tasks: {
where: { status: 'TO_DO' }, where: { status: 'TO_DO' },
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], orderBy: [{ sort_order: 'asc' }],
}, },
}, },
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], orderBy: [{ sort_order: 'asc' }],
}, },
}, },
}, },