🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
Reference in New Issue
Block a user