51 lines
2.5 KiB
JavaScript
51 lines
2.5 KiB
JavaScript
export function validateProductBacklog(backlog, plan) {
|
|
const errors = [];
|
|
if (backlog?.schemaVersion !== 2) errors.push("Product backlog schemaVersion must be 2.");
|
|
if (!Array.isArray(backlog?.statusValues)) errors.push("Product backlog statusValues must be an array.");
|
|
if (!Array.isArray(backlog?.items)) {
|
|
errors.push("Product backlog items must be an array.");
|
|
return errors;
|
|
}
|
|
const milestoneById = new Map((plan?.milestones ?? []).map((milestone) => [milestone.id, milestone]));
|
|
const itemIds = new Set();
|
|
const statuses = new Set(backlog.statusValues ?? []);
|
|
for (const item of backlog.items) {
|
|
if (!item || typeof item !== "object") {
|
|
errors.push("Product backlog contains an invalid item.");
|
|
continue;
|
|
}
|
|
if (typeof item.id !== "string" || !item.id || itemIds.has(item.id)) {
|
|
errors.push(`Product backlog item has a missing or duplicate ID: ${item.id ?? "(missing)"}.`);
|
|
}
|
|
itemIds.add(item.id);
|
|
if (!statuses.has(item.status)) errors.push(`${item.id} has an invalid status: ${item.status}.`);
|
|
const milestone = milestoneById.get(item.milestone);
|
|
if (!milestone) {
|
|
errors.push(`${item.id} references an unknown milestone: ${item.milestone}.`);
|
|
continue;
|
|
}
|
|
if (item.status === "complete" && (!Array.isArray(item.evidence) || item.evidence.length === 0)) {
|
|
errors.push(`${item.id} is complete without evidence references.`);
|
|
}
|
|
if (item.status === "todo" && milestone.status === "complete" && item.postMilestone !== true) {
|
|
errors.push(`${item.id} is todo in a completed milestone without an explicit postMilestone marker.`);
|
|
}
|
|
if (item.status === "partial" && (!item.notes || !Array.isArray(item.evidence) || item.evidence.length === 0)) {
|
|
errors.push(`${item.id} is partial without notes and evidence references.`);
|
|
}
|
|
if (item.status === "blocked_external") {
|
|
const acceptance = milestone.acceptance?.find((criterion) => criterion.id === item.acceptance);
|
|
if (!item.externalOwner || !acceptance?.external || acceptance.met !== false) {
|
|
errors.push(`${item.id} must reference an unmet external acceptance criterion and owner.`);
|
|
}
|
|
}
|
|
if (item.status === "complete" && milestone.status !== "complete") {
|
|
const acceptance = milestone.acceptance?.find((criterion) => criterion.id === item.acceptance);
|
|
if (!acceptance || acceptance.met !== true) {
|
|
errors.push(`${item.id} completes an unfinished milestone without a met acceptance criterion.`);
|
|
}
|
|
}
|
|
}
|
|
return errors;
|
|
}
|