🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* PR-37 Delivery Planning — Release Planner (Part G)
|
||||
*
|
||||
* Maps MVP/V1/V2/Enterprise/Global phases to concrete releases:
|
||||
* - Release 1.0 (MVP)
|
||||
* - Release 1.1 (MVP polish)
|
||||
* - Release 2.0 (V1)
|
||||
* - Enterprise Edition
|
||||
* - Global Edition
|
||||
*
|
||||
* Each release includes version info, feature counts, validation gates,
|
||||
* release goals, dependency checks, and relative timing.
|
||||
*
|
||||
* @module pr-37-delivery-planning/release-planner
|
||||
* @since PR-37
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standard validation gates applied to every release.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const DEFAULT_VALIDATION_GATES = [
|
||||
"All ACs pass",
|
||||
"Regression test pass",
|
||||
"Performance benchmark meet",
|
||||
"Security scan pass",
|
||||
"UAT sign-off",
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a release plan from SF-02 output.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.mvpScope — MVP scope from SF-02 (mvp, v1, v2, enterprise, global phases)
|
||||
* @param {object[]} opts.priorities — Priority matrix entries (from SF-02)
|
||||
* @param {object} opts.dependencies — Dependency graph { nodes, edges }
|
||||
* @param {object[]} opts.stories — User stories (from SF-02 userStories)
|
||||
* @param {object[]} opts.epics — Epics (from SF-02)
|
||||
* @param {object[]} opts.features — Features (from SF-02)
|
||||
* @returns {object} Release plan
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const plan = generateReleasePlan({
|
||||
* mvpScope: sf02MvpScope,
|
||||
* priorities: sf02Priorities,
|
||||
* dependencies: sf02Graph,
|
||||
* stories: sf02Stories,
|
||||
* epics: sf02Epics,
|
||||
* features: sf02Features,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function generateReleasePlan({ mvpScope, priorities, dependencies, stories, epics, features }) {
|
||||
// ── Input validation ──
|
||||
if (!stories || stories.length === 0) {
|
||||
return {
|
||||
releases: [],
|
||||
warnings: ["No stories provided; no releases generated"],
|
||||
};
|
||||
}
|
||||
|
||||
const warnings = [];
|
||||
const priorityMap = new Map();
|
||||
for (const p of (priorities || [])) {
|
||||
if (p.itemType === "story") {
|
||||
priorityMap.set(p.itemId, p.category);
|
||||
}
|
||||
}
|
||||
|
||||
// Build lookup maps
|
||||
const storyMap = new Map();
|
||||
for (const s of (stories || [])) storyMap.set(s.id, s);
|
||||
|
||||
const featureMap = new Map();
|
||||
for (const f of (features || [])) featureMap.set(f.id, f);
|
||||
|
||||
const epicMap = new Map();
|
||||
for (const e of (epics || [])) epicMap.set(e.id, e);
|
||||
|
||||
const dependencyEdges = (dependencies && dependencies.edges) ? dependencies.edges : [];
|
||||
|
||||
/**
|
||||
* Map phase scope from MVP scope → actual story/feature/epic IDs.
|
||||
*/
|
||||
function resolvePhaseStories(phase) {
|
||||
if (!phase || !phase.mustHaveStories) return [];
|
||||
return phase.mustHaveStories.map(s => s.id || s);
|
||||
}
|
||||
|
||||
function resolvePhaseEpics(phase, epics) {
|
||||
if (!phase || !phase.epics) return [];
|
||||
// If epics is a count, we can't resolve IDs; return empty
|
||||
if (typeof phase.epics === "number") return [];
|
||||
return [...phase.epics].slice(0);
|
||||
}
|
||||
|
||||
function resolvePhaseFeatures(phase, features) {
|
||||
if (!phase || !phase.features) return [];
|
||||
if (typeof phase.features === "number") return [];
|
||||
return [...phase.features].slice(0);
|
||||
}
|
||||
|
||||
// ── Helper: filter stories by priority ──
|
||||
function filterStoriesByPriority(categories) {
|
||||
const ids = [];
|
||||
for (const s of (stories || [])) {
|
||||
const cat = priorityMap.get(s.id) || s.priority || "Could Have";
|
||||
if (categories.includes(cat)) ids.push(s.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// ── Helper: find stories with critical unresolved deps ──
|
||||
function findCriticalUnresolved(storyIds, availableIds) {
|
||||
const available = new Set(availableIds);
|
||||
const issues = [];
|
||||
for (const sid of storyIds) {
|
||||
const story = storyMap.get(sid);
|
||||
if (!story) continue;
|
||||
const deps = story.dependencies || [];
|
||||
for (const depId of deps) {
|
||||
if (!available.has(depId)) {
|
||||
const depStory = storyMap.get(depId);
|
||||
issues.push({
|
||||
storyId: sid,
|
||||
storyStatement: story.statement || sid,
|
||||
unresolvedDepId: depId,
|
||||
unresolvedDepStatement: depStory ? depStory.statement : depId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check dependency graph edges
|
||||
const edgeIssues = [];
|
||||
const storyIdSet = new Set(storyIds);
|
||||
for (const edge of dependencyEdges) {
|
||||
if (storyIdSet.has(edge.to)) {
|
||||
const fromStory = storyMap.get(edge.from);
|
||||
const toStory = storyMap.get(edge.to);
|
||||
if (!available.has(edge.from) && fromStory && toStory) {
|
||||
edgeIssues.push({
|
||||
storyId: edge.to,
|
||||
storyStatement: toStory.statement || edge.to,
|
||||
unresolvedDepId: edge.from,
|
||||
unresolvedDepStatement: fromStory.statement || edge.from,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...issues, ...edgeIssues].slice(0, 5); // limit to 5
|
||||
}
|
||||
|
||||
// ── Helper: build release object ──
|
||||
function buildRelease({ version, name, description, storyIds, releaseNumber }) {
|
||||
const uniqueEpicIds = new Set();
|
||||
const uniqueFeatureIds = new Set();
|
||||
let storyCount = 0;
|
||||
let storyPoints = 0;
|
||||
|
||||
for (const sid of storyIds) {
|
||||
const s = storyMap.get(sid);
|
||||
if (!s) continue;
|
||||
storyCount++;
|
||||
storyPoints += s.storyPoints || 1;
|
||||
if (s.epicId) uniqueEpicIds.add(s.epicId);
|
||||
if (s.featureId) uniqueFeatureIds.add(s.featureId);
|
||||
}
|
||||
|
||||
// Resolve epic/feature names
|
||||
const epicNames = [...uniqueEpicIds].map(eid => {
|
||||
const epic = epicMap.get(eid);
|
||||
return epic ? epic.title : eid;
|
||||
}).filter(Boolean);
|
||||
|
||||
const featureNames = [...uniqueFeatureIds].map(fid => {
|
||||
const feat = featureMap.get(fid);
|
||||
return feat ? feat.title : fid;
|
||||
}).filter(Boolean);
|
||||
|
||||
// Determine all resolved IDs for dependency check
|
||||
const resolvedIds = new Set(storyIds);
|
||||
|
||||
const depIssues = findCriticalUnresolved(storyIds, resolvedIds);
|
||||
|
||||
return {
|
||||
version,
|
||||
name,
|
||||
description,
|
||||
features: {
|
||||
epicCount: uniqueEpicIds.size,
|
||||
epicNames,
|
||||
featureCount: uniqueFeatureIds.size,
|
||||
featureNames: featureNames.slice(0, 10), // top 10
|
||||
storyCount,
|
||||
storyPoints,
|
||||
},
|
||||
validationGates: [...DEFAULT_VALIDATION_GATES],
|
||||
releaseGoals: deriveReleaseGoals(name, version, storyIds, storyMap),
|
||||
dependencyCheck: depIssues,
|
||||
releaseDate: `Release ${releaseNumber}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ── 1. Release 1.0 (MVP) ──
|
||||
// Must Have stories from MVP phase
|
||||
const mvpPhase = mvpScope && mvpScope.mvp;
|
||||
let mvpStoryIds = [];
|
||||
|
||||
if (mvpPhase && mvpPhase.mustHaveStories && Array.isArray(mvpPhase.mustHaveStories)) {
|
||||
mvpStoryIds = mvpPhase.mustHaveStories.map(s => s.id || s);
|
||||
}
|
||||
|
||||
// Fallback: filter Must Have from all stories
|
||||
if (mvpStoryIds.length === 0) {
|
||||
mvpStoryIds = filterStoriesByPriority(["Must Have"]);
|
||||
}
|
||||
|
||||
// ── 2. Release 1.1 (MVP polish) ──
|
||||
// Remaining Must Have + critical Should Have fixes
|
||||
const shouldHaveIds = filterStoriesByPriority(["Should Have"]);
|
||||
// Take first ~30% of Should Have for critical fixes
|
||||
const criticalShouldHave = shouldHaveIds.slice(0, Math.max(1, Math.ceil(shouldHaveIds.length * 0.3)));
|
||||
|
||||
// Remaining Must Have stories not in MVP
|
||||
const remainingMustHave = mvpStoryIds.filter(id => !mvpStoryIds.includes(id));
|
||||
|
||||
// ── 3. Release 2.0 (V1) ──
|
||||
// All Should Have stories that aren't in 1.1
|
||||
const v1StoryIds = shouldHaveIds.filter(id => !criticalShouldHave.includes(id));
|
||||
|
||||
// ── 4. Enterprise Edition ──
|
||||
const enterprisePhase = mvpScope && mvpScope.enterprise;
|
||||
let enterpriseStoryIds = [];
|
||||
if (enterprisePhase && enterprisePhase.stories) {
|
||||
enterpriseStoryIds = [...enterprisePhase.stories];
|
||||
}
|
||||
|
||||
// ── 5. Global Edition ──
|
||||
const globalPhase = mvpScope && mvpScope.global;
|
||||
let globalStoryIds = [];
|
||||
if (globalPhase && globalPhase.stories) {
|
||||
globalStoryIds = [...globalPhase.stories];
|
||||
}
|
||||
|
||||
// Remove duplicates: Enterprise/Global should contain stories not in earlier releases
|
||||
const v2CouldIds = filterStoriesByPriority(["Could Have"]);
|
||||
if (enterpriseStoryIds.length === 0) {
|
||||
// Fallback: Could Have stories for Enterprise
|
||||
enterpriseStoryIds = v2CouldIds.slice(0, Math.ceil(v2CouldIds.length * 0.6));
|
||||
}
|
||||
if (globalStoryIds.length === 0) {
|
||||
globalStoryIds = v2CouldIds.slice(Math.ceil(v2CouldIds.length * 0.6));
|
||||
}
|
||||
|
||||
// ── Assemble releases ──
|
||||
const releases = [];
|
||||
|
||||
if (mvpStoryIds.length > 0) {
|
||||
releases.push(buildRelease({
|
||||
version: "1.0.0",
|
||||
name: "MVP",
|
||||
description: "Minimum Viable Product — core Must Have functionalities for initial market validation",
|
||||
storyIds: mvpStoryIds,
|
||||
releaseNumber: 1,
|
||||
}));
|
||||
}
|
||||
|
||||
if (criticalShouldHave.length > 0 || remainingMustHave.length > 0) {
|
||||
const polishIds = [...new Set([...criticalShouldHave, ...remainingMustHave])];
|
||||
releases.push(buildRelease({
|
||||
version: "1.1.0",
|
||||
name: "MVP Polish",
|
||||
description: "MVP refinement — remaining Must Have items and critical Should Have fixes",
|
||||
storyIds: polishIds,
|
||||
releaseNumber: 2,
|
||||
}));
|
||||
}
|
||||
|
||||
if (v1StoryIds.length > 0) {
|
||||
releases.push(buildRelease({
|
||||
version: "2.0.0",
|
||||
name: "V1",
|
||||
description: "First full version — complete Should Have feature set for competitive feature parity",
|
||||
storyIds: v1StoryIds,
|
||||
releaseNumber: 3,
|
||||
}));
|
||||
}
|
||||
|
||||
if (enterpriseStoryIds.length > 0) {
|
||||
releases.push(buildRelease({
|
||||
version: "3.0.0",
|
||||
name: "Enterprise Edition",
|
||||
description: "Enterprise-grade features — advanced security, multi-tenancy, compliance, and admin capabilities",
|
||||
storyIds: enterpriseStoryIds,
|
||||
releaseNumber: 4,
|
||||
}));
|
||||
}
|
||||
|
||||
if (globalStoryIds.length > 0) {
|
||||
releases.push(buildRelease({
|
||||
version: "4.0.0",
|
||||
name: "Global Edition",
|
||||
description: "Global-scale features — i18n, multi-region, compliance, and localization",
|
||||
storyIds: globalStoryIds,
|
||||
releaseNumber: 5,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Generate warnings ──
|
||||
if (mvpStoryIds.length === 0) {
|
||||
warnings.push("No MVP stories found; Release 1.0 will be empty");
|
||||
}
|
||||
if (releases.length === 0) {
|
||||
warnings.push("No releases could be generated from the provided data");
|
||||
}
|
||||
for (const rel of releases) {
|
||||
if (rel.dependencyCheck.length > 0) {
|
||||
warnings.push(`${rel.name} (${rel.version}): ${rel.dependencyCheck.length} stories have unresolved critical dependencies`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
releases,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive business-oriented release goals from the story set.
|
||||
*
|
||||
* @param {string} name — Release name
|
||||
* @param {string} version — Semver
|
||||
* @param {string[]} storyIds — Stories in this release
|
||||
* @param {Map<string, object>} storyMap — Story lookup
|
||||
* @returns {string[]} Goals
|
||||
*/
|
||||
function deriveReleaseGoals(name, version, storyIds, storyMap) {
|
||||
const goals = [];
|
||||
|
||||
switch (name) {
|
||||
case "MVP":
|
||||
goals.push("Deliver core value proposition to early adopters");
|
||||
goals.push("Validate product-market fit with real users");
|
||||
goals.push("Establish foundational user identity and access system");
|
||||
break;
|
||||
case "MVP Polish":
|
||||
goals.push("Address critical feedback from MVP users");
|
||||
goals.push("Stabilize core features for broader adoption");
|
||||
goals.push("Improve reliability and performance to production standards");
|
||||
break;
|
||||
case "V1":
|
||||
goals.push("Achieve feature parity with market competitors");
|
||||
goals.push("Provide comprehensive user experience for mainstream adoption");
|
||||
goals.push("Enable self-service onboarding and administration");
|
||||
break;
|
||||
case "Enterprise Edition":
|
||||
goals.push("Meet enterprise security and compliance requirements");
|
||||
goals.push("Support multi-tenant deployment at scale");
|
||||
goals.push("Provide advanced audit, governance, and admin tools");
|
||||
break;
|
||||
case "Global Edition":
|
||||
goals.push("Support internationalization and localization");
|
||||
goals.push("Meet global data residency and privacy regulations");
|
||||
goals.push("Enable multi-region deployment and failover");
|
||||
break;
|
||||
default:
|
||||
goals.push(`Deliver ${name} (${version}) features to users`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add story-specific goal from top stories
|
||||
const topStories = storyIds.slice(0, 3);
|
||||
for (const sid of topStories) {
|
||||
const s = storyMap.get(sid);
|
||||
if (s && s.value) {
|
||||
goals.push(`Enable: ${s.value.length > 60 ? s.value.slice(0, 60) + "…" : s.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return goals;
|
||||
}
|
||||
|
||||
export default { generateReleasePlan };
|
||||
Reference in New Issue
Block a user