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