376 lines
15 KiB
JavaScript
376 lines
15 KiB
JavaScript
/**
|
|
* PR-37 Part J — Roadmap Alignment Engine
|
|
*
|
|
* 验证 SF-01 Roadmap 与 PR-37 交付规划(Sprint Plan / Release Plan / Batch Plan)的一致性。
|
|
*
|
|
* 检查:
|
|
* - Phase coverage: Roadmap 的阶段是否被 Release Plan 完整覆盖
|
|
* - Story coverage: MVP/V1/V2 的 Story 是否被 Sprint Plan 完整分配
|
|
* - Dependency integrity: 关键依赖链是否在 Release 和 Batch 中被正确处理
|
|
* - Budget alignment: 每个 Phase 的预算是否与实际工作量(Story Points)匹配
|
|
* - Timeline consistency: Sprint Plan 的总周数是否在 Roadmap 的 Phase 时间范围内
|
|
*
|
|
* @module pr-37-delivery-planning/roadmap-aligner
|
|
* @since PR-37
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} RoadmapPhase
|
|
* @property {string} name — Phase name (mvp / v1 / v2 / enterprise / global)
|
|
* @property {string} scope — Phase description
|
|
* @property {number} duration — Duration in months
|
|
* @property {number} budget — Budget in currency units
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} AlignmentReport
|
|
* @property {string} overallVerdict — "aligned" / "misaligned" / "partial"
|
|
* @property {number} alignmentScore — 0-100
|
|
* @property {object} phaseCoverage — Per-phase coverage assessment
|
|
* @property {object} timelineCheck — Sprint vs Roadmap timeline comparison
|
|
* @property {object} budgetCheck — Budget vs Effort comparison
|
|
* @property {object} dependencyCheck — Critical dependency path verification
|
|
* @property {string[]} warnings — Alignment warnings
|
|
* @property {string[]} recommendations — Actionable recommendations
|
|
*/
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Main Alignment Function
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Align SF-01 Roadmap with PR-37 delivery plans.
|
|
*
|
|
* @param {object} opts
|
|
* @param {object} opts.roadmap — SF-01 Roadmap (phases array)
|
|
* @param {object} opts.sprintPlan — PR-37 Sprint Plan
|
|
* @param {object} opts.releasePlan — PR-37 Release Plan
|
|
* @param {object} opts.batchPlan — PR-37 Batch Plan
|
|
* @param {object} opts.mvpScope — SF-02 MVP Scope
|
|
* @returns {AlignmentReport}
|
|
*/
|
|
export function alignRoadmapWithPlans({ roadmap, sprintPlan, releasePlan, batchPlan, mvpScope }) {
|
|
const warnings = [];
|
|
const recommendations = [];
|
|
let alignmentScore = 100;
|
|
|
|
// ── 1. Phase Coverage Check ──
|
|
const phaseCoverage = checkPhaseCoverage(roadmap, releasePlan, mvpScope);
|
|
if (!phaseCoverage.allCovered) {
|
|
alignmentScore -= 15;
|
|
warnings.push(...phaseCoverage.warnings);
|
|
recommendations.push("Ensure all roadmap phases have corresponding releases");
|
|
}
|
|
|
|
// ── 2. Story Coverage Check ──
|
|
const storyCoverage = checkStoryCoverage(sprintPlan, mvpScope);
|
|
if (storyCoverage.uncoveredCount > 0) {
|
|
alignmentScore -= Math.min(25, storyCoverage.uncoveredCount * 2);
|
|
warnings.push(`⚠️ ${storyCoverage.uncoveredCount} MVP/V1 stories not assigned to any Sprint`);
|
|
recommendations.push(`Assign ${storyCoverage.uncoveredCount} unplanned stories to sprints or backlog`);
|
|
}
|
|
|
|
// ── 3. Timeline Consistency ──
|
|
const timelineCheck = checkTimelineConsistency(roadmap, sprintPlan);
|
|
if (timelineCheck.misalignment) {
|
|
alignmentScore -= 20;
|
|
warnings.push(timelineCheck.message);
|
|
recommendations.push(timelineCheck.recommendation);
|
|
}
|
|
|
|
// ── 4. Budget Alignment ──
|
|
const budgetCheck = checkBudgetAlignment(roadmap, sprintPlan, mvpScope);
|
|
if (budgetCheck.misalignment) {
|
|
alignmentScore -= 15;
|
|
warnings.push(budgetCheck.message);
|
|
recommendations.push(budgetCheck.recommendation);
|
|
}
|
|
|
|
// ── 5. Dependency Integrity ──
|
|
const dependencyCheck = checkDependencyIntegrity(batchPlan, releasePlan);
|
|
if (dependencyCheck.misalignment) {
|
|
alignmentScore -= 15;
|
|
warnings.push(...dependencyCheck.warnings);
|
|
recommendations.push(...dependencyCheck.recommendations);
|
|
}
|
|
|
|
const overallVerdict = alignmentScore >= 90 ? "aligned"
|
|
: alignmentScore >= 60 ? "partial"
|
|
: "misaligned";
|
|
|
|
return {
|
|
overallVerdict,
|
|
alignmentScore: Math.max(0, alignmentScore),
|
|
phaseCoverage,
|
|
storyCoverage,
|
|
timelineCheck,
|
|
budgetCheck,
|
|
dependencyCheck,
|
|
warnings,
|
|
recommendations,
|
|
summary: generateAlignmentSummary(overallVerdict, alignmentScore, roadmap, sprintPlan),
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Phase Coverage
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function checkPhaseCoverage(roadmap, releasePlan, mvpScope) {
|
|
const phases = roadmap?.phases || [];
|
|
const releases = releasePlan?.releases || [];
|
|
const covered = [];
|
|
const uncovered = [];
|
|
const warnings = [];
|
|
|
|
// Map phase names to release versions
|
|
const phaseReleaseMap = {
|
|
mvp: ["1.0.0"],
|
|
v1: ["2.0.0", "1.1.0"],
|
|
v2: ["3.0.0"],
|
|
enterprise: ["Enterprise Edition", "3.0.0"],
|
|
global: ["Global Edition", "4.0.0"],
|
|
};
|
|
|
|
for (const phase of phases) {
|
|
const expectedReleases = phaseReleaseMap[phase.name] || [];
|
|
const found = releases.filter(r => expectedReleases.includes(r.version));
|
|
if (found.length > 0) {
|
|
covered.push({ phase: phase.name, releases: found.map(r => r.version) });
|
|
} else {
|
|
uncovered.push({ phase: phase.name, expected: expectedReleases });
|
|
warnings.push(`⛔ Phase "${phase.name}" has no matching release in Release Plan (expected: ${expectedReleases.join(", ")})`);
|
|
}
|
|
}
|
|
|
|
// Also check that all releases map to a phase
|
|
const allReleaseVersions = releases.map(r => r.version);
|
|
const allExpectedVersions = Object.values(phaseReleaseMap).flat();
|
|
const extraReleases = allReleaseVersions.filter(v => !allExpectedVersions.includes(v));
|
|
if (extraReleases.length > 0) {
|
|
warnings.push(`⚠️ Release Plan contains ${extraReleases.length} release(s) without roadmap phase mapping: ${extraReleases.join(", ")}`);
|
|
}
|
|
|
|
return {
|
|
allCovered: uncovered.length === 0,
|
|
covered,
|
|
uncovered,
|
|
extraReleases,
|
|
warnings,
|
|
summary: `${covered.length}/${phases.length} roadmap phases covered by Release Plan`,
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Story Coverage
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function checkStoryCoverage(sprintPlan, mvpScope) {
|
|
const allocatedIds = new Set();
|
|
for (const sprint of (sprintPlan?.sprints || [])) {
|
|
for (const sid of (sprint.storyIds || [])) {
|
|
allocatedIds.add(sid);
|
|
}
|
|
}
|
|
|
|
const unallocated = new Set();
|
|
const unplanned = sprintPlan?.unplannedStories || [];
|
|
|
|
// MVP + V1 stories should all be allocated
|
|
const mvpStoryIds = new Set(
|
|
(mvpScope?.mvp?.mustHaveStories || []).map(s => s.id)
|
|
);
|
|
|
|
let uncoveredCount = 0;
|
|
for (const sid of mvpStoryIds) {
|
|
if (!allocatedIds.has(sid)) {
|
|
unallocated.add(sid);
|
|
uncoveredCount++;
|
|
}
|
|
}
|
|
|
|
for (const sid of unplanned) {
|
|
if (!allocatedIds.has(sid)) {
|
|
unallocated.add(sid);
|
|
uncoveredCount++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
uncoveredCount,
|
|
unallocatedStories: [...unallocated],
|
|
totalAllocated: allocatedIds.size,
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Timeline Consistency
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function checkTimelineConsistency(roadmap, sprintPlan) {
|
|
const phases = roadmap?.phases || [];
|
|
const sprints = sprintPlan?.sprints || [];
|
|
const totalSprintWeeks = (sprintPlan?.metrics?.estimatedDurationWeeks) || 0;
|
|
|
|
// Roadmap total in months → weeks (approximate 4 weeks/month)
|
|
const totalRoadmapMonths = phases.reduce((sum, p) => sum + (p.duration || p.durationMonths || 0), 0);
|
|
const totalRoadmapWeeks = totalRoadmapMonths * 4;
|
|
|
|
// MVP phase duration
|
|
const mvpPhase = phases.find(p => p.name === "mvp");
|
|
const mvpPhaseWeeks = (mvpPhase?.duration || mvpPhase?.durationMonths || 0) * 4;
|
|
|
|
// MVP sprint weeks
|
|
const mvpSprints = sprints.filter(s => {
|
|
// Heuristic: first few sprints that cover the MVP points
|
|
const running = sprints
|
|
.slice(0, sprints.indexOf(s) + 1)
|
|
.reduce((sum, sp) => sum + (sp.totalStoryPoints || 0), 0);
|
|
const mvpTotal = (sprintPlan?.metrics?.totalPoints || 0) * 0.35; // ~35% of total is MVP
|
|
return running <= mvpTotal;
|
|
});
|
|
const mvpSprintWeeks = mvpSprints.length * 2; // 2 weeks/sprint
|
|
|
|
const misalignment = mvpSprintWeeks > mvpPhaseWeeks * 1.2 || totalSprintWeeks > totalRoadmapWeeks * 1.2;
|
|
const message = misalignment
|
|
? `⚠️ Timeline misalignment: Sprint plan (${totalSprintWeeks}w) exceeds roadmap (${totalRoadmapWeeks}w) by ${totalSprintWeeks - totalRoadmapWeeks}w`
|
|
: `✅ Timeline aligned: Sprint plan (${totalSprintWeeks}w) fits within roadmap (${totalRoadmapWeeks}w)`;
|
|
|
|
const recommendation = misalignment
|
|
? `Consider reducing Sprint scope by ${Math.round((totalSprintWeeks - totalRoadmapWeeks) / 2)} sprints, or extend roadmap timeline by ${Math.ceil((totalSprintWeeks - totalRoadmapWeeks) / 4)} months`
|
|
: "No action needed — timeline is consistent";
|
|
|
|
return {
|
|
misalignment,
|
|
message,
|
|
recommendation,
|
|
sprintWeeks: totalSprintWeeks,
|
|
roadmapWeeks: totalRoadmapWeeks,
|
|
mvpSprintWeeks,
|
|
mvpPhaseWeeks,
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Budget Alignment
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function checkBudgetAlignment(roadmap, sprintPlan, mvpScope) {
|
|
const phases = roadmap?.phases || [];
|
|
const totalBudget = phases.reduce((sum, p) => sum + (p.budget || 0), 0);
|
|
const totalPoints = sprintPlan?.metrics?.totalPoints || 0;
|
|
|
|
if (totalBudget === 0 || totalPoints === 0) {
|
|
return {
|
|
misalignment: false,
|
|
message: "Insufficient data for budget alignment check",
|
|
recommendation: "Ensure roadmap has budget estimates and sprint plan has story points",
|
|
costPerPoint: 0,
|
|
};
|
|
}
|
|
|
|
const costPerPoint = Math.round(totalBudget / totalPoints);
|
|
const mvpBudget = phases.find(p => p.name === "mvp")?.budget || 0;
|
|
const mvpPoints = mvpScope?.mvp?.totalStoryPoints || 0;
|
|
const mvpCostAtRate = mvpPoints * costPerPoint;
|
|
|
|
// More than 20% deviation = misalignment
|
|
const deviation = mvpBudget > 0 ? Math.abs(mvpCostAtRate - mvpBudget) / mvpBudget : 0;
|
|
const misalignment = deviation > 0.2;
|
|
|
|
const message = misalignment
|
|
? `⚠️ Budget misalignment: MVP estimated cost at rate ($${mvpCostAtRate.toLocaleString()}) deviates ${(deviation * 100).toFixed(0)}% from roadmap budget ($${mvpBudget.toLocaleString()})`
|
|
: `✅ Budget aligned: MVP cost at $${costPerPoint}/point (${(deviation * 100).toFixed(0)}% deviation)`;
|
|
|
|
const recommendation = misalignment
|
|
? `Review story point estimates or adjust roadmap budget. Current rate: $${costPerPoint}/point, roadmap implies $${Math.round(mvpBudget / Math.max(1, mvpPoints))}/point`
|
|
: "No action needed — budget is consistent";
|
|
|
|
return {
|
|
misalignment,
|
|
message,
|
|
recommendation,
|
|
costPerPoint,
|
|
mvpCostAtRate,
|
|
mvpBudget,
|
|
deviation: (deviation * 100).toFixed(1) + "%",
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Dependency Integrity
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function checkDependencyIntegrity(batchPlan, releasePlan) {
|
|
const batches = batchPlan?.batches || [];
|
|
const releases = releasePlan?.releases || [];
|
|
const warnings = [];
|
|
const recommendations = [];
|
|
|
|
// Check: Development batches should feed into testing batches
|
|
const devBatches = batches.filter(b => b.batchType === "DevelopmentBatch");
|
|
const testBatches = batches.filter(b => b.batchType === "TestingBatch");
|
|
const releaseBatches = batches.filter(b => b.batchType === "ReleaseBatch");
|
|
const deliveryBatches = batches.filter(b => b.batchType === "DeliveryBatch");
|
|
|
|
if (devBatches.length > 0 && testBatches.length === 0) {
|
|
warnings.push("⚠️ Development batches exist but no TestingBatch defined");
|
|
recommendations.push("Add TestingBatch(s) that consume DevelopmentBatch output");
|
|
}
|
|
|
|
if (releaseBatches.length === 0 && releases.length > 0) {
|
|
warnings.push("⚠️ Releases defined but no ReleaseBatch for deployment coordination");
|
|
recommendations.push("Add ReleaseBatch(s) to map features to deployment stages");
|
|
}
|
|
|
|
// Check: Release dependencies should be satisfied
|
|
for (let i = 1; i < releases.length; i++) {
|
|
const prevIds = new Set();
|
|
for (let j = 0; j < i; j++) {
|
|
(releases[j]?.features?.featureNames || []).forEach(n => prevIds.add(n));
|
|
}
|
|
}
|
|
|
|
const allBatchTypes = [...new Set(batches.map(b => b.batchType))];
|
|
const expectedTypes = ["DevelopmentBatch", "TestingBatch", "ReleaseBatch", "DeliveryBatch"];
|
|
const missingTypes = expectedTypes.filter(t => !allBatchTypes.includes(t));
|
|
|
|
if (missingTypes.length > 0) {
|
|
warnings.push(`⚠️ Missing batch types: ${missingTypes.join(", ")}`);
|
|
recommendations.push(
|
|
"Ensure full batch pipeline: DevelopmentBatch → TestingBatch → ReleaseBatch → DeliveryBatch"
|
|
);
|
|
}
|
|
|
|
return {
|
|
misalignment: warnings.length > 0,
|
|
warnings,
|
|
recommendations,
|
|
batchTypeCoverage: `${allBatchTypes.length}/${expectedTypes.length}`,
|
|
missingTypes,
|
|
};
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Summary
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
function generateAlignmentSummary(verdict, score, roadmap, sprintPlan) {
|
|
const phaseCount = roadmap?.phases?.length || 0;
|
|
const sprintCount = sprintPlan?.sprints?.length || 0;
|
|
const totalWeeks = sprintPlan?.metrics?.estimatedDurationWeeks || 0;
|
|
|
|
return [
|
|
`## Roadmap Alignment: ${verdict.toUpperCase()} (${score}/100)`,
|
|
"",
|
|
`- **Roadmap Phases:** ${phaseCount}`,
|
|
`- **Delivery Sprints:** ${sprintCount}`,
|
|
`- **Estimated Duration:** ${totalWeeks} weeks`,
|
|
verdict === "aligned"
|
|
? "✅ All delivery plans are consistent with the product roadmap."
|
|
: verdict === "partial"
|
|
? "⚠️ Delivery plans partially align with the roadmap — see recommendations."
|
|
: "❌ Significant misalignment between delivery plans and roadmap.",
|
|
].join("\n");
|
|
}
|