Files
itworx-knowledge-hub-Public/scripts/lib/product-backlog.mjs
T

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;
}