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