Files
2026-06-06 10:40:48 +08:00

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