🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* PR-37 Delivery Planning — Batch Planner (Part H)
|
||||
*
|
||||
* Generates 4 batch types based on dependency analysis:
|
||||
* - DevelopmentBatch: Group stories by dependency level for parallel dev
|
||||
* - TestingBatch: Group completed dev batches ready for testing
|
||||
* - ReleaseBatch: Group features ready for staged deployment
|
||||
* - DeliveryBatch: Customer-facing delivery increments
|
||||
*
|
||||
* Each batch captures parallel groups, prerequisite batches, and story/feature/epic references.
|
||||
*
|
||||
* @module pr-37-delivery-planning/batch-planner
|
||||
* @since PR-37
|
||||
*/
|
||||
|
||||
let _batchSeq = 0;
|
||||
|
||||
/**
|
||||
* Generate a complete batch plan from SF-02 output and sprint/release plans.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object[]} opts.stories — User stories (from SF-02)
|
||||
* @param {object} opts.dependencies — Dependency graph { nodes, edges }
|
||||
* @param {object[]} opts.priorities — Priority matrix entries
|
||||
* @param {object} opts.sprints — Sprint plan (from sprint-planner)
|
||||
* @param {object} opts.releases — Release plan (from release-planner)
|
||||
* @returns {object} Batch plan { batches, summary }
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const batchPlan = generateBatchPlan({
|
||||
* stories: myStories,
|
||||
* dependencies: myGraph,
|
||||
* priorities: myPriorities,
|
||||
* sprints: sprintPlan,
|
||||
* releases: releasePlan,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function generateBatchPlan({ stories, dependencies, priorities, sprints, releases }) {
|
||||
// ── Input validation ──
|
||||
if (!stories || stories.length === 0) {
|
||||
return {
|
||||
batches: [],
|
||||
summary: { totalBatches: 0, devBatches: 0, testBatches: 0, releaseBatches: 0, deliveryBatches: 0 },
|
||||
warnings: ["No stories provided"],
|
||||
};
|
||||
}
|
||||
|
||||
const warnings = [];
|
||||
const batches = [];
|
||||
|
||||
const graph = dependencies && dependencies.graph ? dependencies.graph : (dependencies || { nodes: [], edges: [] });
|
||||
const storyMap = new Map();
|
||||
for (const s of stories) storyMap.set(s.id, s);
|
||||
|
||||
// Build dependency map: storyId → set of dependency IDs
|
||||
const depMap = new Map();
|
||||
for (const s of stories) {
|
||||
depMap.set(s.id, new Set(s.dependencies || []));
|
||||
}
|
||||
for (const edge of (graph.edges || [])) {
|
||||
// edge.from depends on edge.to (SF-02 semantics: from REQUIRES to)
|
||||
if (!depMap.has(edge.from)) depMap.set(edge.from, new Set());
|
||||
depMap.get(edge.from).add(edge.to);
|
||||
}
|
||||
|
||||
// ── Compute dependency levels ──
|
||||
const depLevels = new Map();
|
||||
function computeLevel(id, visited = new Set()) {
|
||||
if (visited.has(id)) return 0;
|
||||
visited.add(id);
|
||||
const deps = depMap.get(id) || new Set();
|
||||
if (deps.size === 0) {
|
||||
depLevels.set(id, 0);
|
||||
return 0;
|
||||
}
|
||||
let max = 0;
|
||||
for (const depId of deps) {
|
||||
max = Math.max(max, computeLevel(depId, visited) + 1);
|
||||
}
|
||||
depLevels.set(id, max);
|
||||
return max;
|
||||
}
|
||||
for (const s of stories) {
|
||||
if (!depLevels.has(s.id)) computeLevel(s.id);
|
||||
}
|
||||
|
||||
// ── Priority map ──
|
||||
const priorityMap = new Map();
|
||||
for (const p of (priorities || [])) {
|
||||
if (p.itemType === "story") priorityMap.set(p.itemId, p.category);
|
||||
}
|
||||
|
||||
// ── 1. Development Batches ──
|
||||
// Group by dependency level
|
||||
const levelGroups = new Map(); // level → [storyIds]
|
||||
for (const s of stories) {
|
||||
const level = depLevels.get(s.id) ?? 0;
|
||||
// Bucket: L0, L1, L2, L3+
|
||||
const bucket = level >= 3 ? "L3" : `L${level}`;
|
||||
if (!levelGroups.has(bucket)) levelGroups.set(bucket, []);
|
||||
levelGroups.get(bucket).push(s.id);
|
||||
}
|
||||
|
||||
const levelOrder = ["L0", "L1", "L2", "L3"];
|
||||
for (const bucket of levelOrder) {
|
||||
const ids = levelGroups.get(bucket) || [];
|
||||
if (ids.length === 0) continue;
|
||||
|
||||
// Stories at same level can be developed in parallel — group them
|
||||
const batchId = `dev-batch-${++_batchSeq}`;
|
||||
const batchName = `Development ${bucket.replace("L", "Level ")}`;
|
||||
|
||||
// Collect feature and epic IDs
|
||||
const featureIds = new Set();
|
||||
const epicIds = new Set();
|
||||
for (const id of ids) {
|
||||
const s = storyMap.get(id);
|
||||
if (s) {
|
||||
if (s.featureId) featureIds.add(s.featureId);
|
||||
if (s.epicId) epicIds.add(s.epicId);
|
||||
}
|
||||
}
|
||||
|
||||
// Prerequisite: all lower-level batches
|
||||
const levelNum = parseInt(bucket.slice(1));
|
||||
const prerequisiteBatches = [];
|
||||
for (let l = 0; l < levelNum; l++) {
|
||||
const lowerBucket = `L${l}`;
|
||||
const lowerIds = levelGroups.get(lowerBucket);
|
||||
if (lowerIds && lowerIds.length > 0) {
|
||||
prerequisiteBatches.push(`dev-batch-${lowerBucket}`);
|
||||
}
|
||||
}
|
||||
|
||||
batches.push({
|
||||
batchId,
|
||||
batchType: "DevelopmentBatch",
|
||||
name: batchName,
|
||||
storyIds: ids,
|
||||
featureIds: [...featureIds],
|
||||
epicIds: [...epicIds],
|
||||
parallelGroups: groupForParallel(ids, depMap, depLevels, storyMap),
|
||||
prerequisiteBatches,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 2. Testing Batches ──
|
||||
// Group completed dev batches ready for testing (by sprint boundaries)
|
||||
if (sprints && sprints.sprints) {
|
||||
for (const sprint of sprints.sprints) {
|
||||
const testBatchId = `test-batch-${++_batchSeq}`;
|
||||
const batchName = `Testing Sprint ${sprint.sprintNumber}`;
|
||||
|
||||
const featureIds = new Set();
|
||||
const epicIds = new Set();
|
||||
for (const sid of (sprint.storyIds || [])) {
|
||||
const s = storyMap.get(sid);
|
||||
if (s) {
|
||||
if (s.featureId) featureIds.add(s.featureId);
|
||||
if (s.epicId) epicIds.add(s.epicId);
|
||||
}
|
||||
}
|
||||
|
||||
batches.push({
|
||||
batchId: testBatchId,
|
||||
batchType: "TestingBatch",
|
||||
name: batchName,
|
||||
storyIds: sprint.storyIds || [],
|
||||
featureIds: [...featureIds],
|
||||
epicIds: [...epicIds],
|
||||
parallelGroups: [sprint.storyIds || []], // All stories from a sprint can be tested together
|
||||
prerequisiteBatches: [`dev-batch-${batchName}`],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Release Batches ──
|
||||
// Group features ready for staged deployment (by release)
|
||||
if (releases && releases.releases) {
|
||||
for (const rel of releases.releases) {
|
||||
const releaseBatchId = `release-batch-${++_batchSeq}`;
|
||||
|
||||
const featureIds = new Set();
|
||||
const epicIds = new Set();
|
||||
for (const sid of (rel.storyIds || [])) {
|
||||
const s = storyMap.get(sid);
|
||||
if (s) {
|
||||
if (s.featureId) featureIds.add(s.featureId);
|
||||
if (s.epicId) epicIds.add(s.epicId);
|
||||
}
|
||||
}
|
||||
|
||||
batches.push({
|
||||
batchId: releaseBatchId,
|
||||
batchType: "ReleaseBatch",
|
||||
name: `Release ${rel.name} (${rel.version})`,
|
||||
storyIds: rel.storyIds || [],
|
||||
featureIds: [...featureIds],
|
||||
epicIds: [...epicIds],
|
||||
parallelGroups: [rel.storyIds || []],
|
||||
prerequisiteBatches: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Delivery Batches ──
|
||||
// Customer-facing delivery increments — group by release
|
||||
if (releases && releases.releases) {
|
||||
for (const rel of releases.releases) {
|
||||
const deliveryBatchId = `delivery-batch-${++_batchSeq}`;
|
||||
|
||||
const featureIds = new Set();
|
||||
const epicIds = new Set();
|
||||
for (const sid of (rel.storyIds || [])) {
|
||||
const s = storyMap.get(sid);
|
||||
if (s) {
|
||||
if (s.featureId) featureIds.add(s.featureId);
|
||||
if (s.epicId) epicIds.add(s.epicId);
|
||||
}
|
||||
}
|
||||
|
||||
batches.push({
|
||||
batchId: deliveryBatchId,
|
||||
batchType: "DeliveryBatch",
|
||||
name: `Delivery ${rel.name} (${rel.version})`,
|
||||
storyIds: rel.storyIds || [],
|
||||
featureIds: [...featureIds],
|
||||
epicIds: [...epicIds],
|
||||
parallelGroups: [],
|
||||
prerequisiteBatches: [`release-batch-${rel.name}`],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generate warnings ──
|
||||
if (levelGroups.size === 0) {
|
||||
warnings.push("No development batches generated — empty story list");
|
||||
}
|
||||
if (batches.length === 0) {
|
||||
warnings.push("No batches of any type could be generated");
|
||||
}
|
||||
|
||||
const summary = {
|
||||
totalBatches: batches.length,
|
||||
devBatches: batches.filter(b => b.batchType === "DevelopmentBatch").length,
|
||||
testBatches: batches.filter(b => b.batchType === "TestingBatch").length,
|
||||
releaseBatches: batches.filter(b => b.batchType === "ReleaseBatch").length,
|
||||
deliveryBatches: batches.filter(b => b.batchType === "DeliveryBatch").length,
|
||||
};
|
||||
|
||||
return { batches, summary, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Group story IDs into parallel work groups based on dependency analysis.
|
||||
* Stories with no interdependency at the same level can be done in parallel.
|
||||
*
|
||||
* @param {string[]} ids — Story IDs at the same dependency level
|
||||
* @param {Map<string, Set<string>>} depMap — Dependency map
|
||||
* @param {Map<string, number>} depLevels — Computed levels
|
||||
* @param {Map<string, object>} storyMap — Story lookup
|
||||
* @returns {string[][]} Groups of stories that can be developed in parallel
|
||||
*/
|
||||
function groupForParallel(ids, depMap, depLevels, storyMap) {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length === 1) return [ids];
|
||||
|
||||
// Simple heuristic: group by epic ID so stories from same epic are together,
|
||||
// but different epics can proceed in parallel
|
||||
const epicGroups = new Map();
|
||||
for (const id of ids) {
|
||||
const s = storyMap.get(id);
|
||||
const epicId = s ? s.epicId || "unknown" : "unknown";
|
||||
if (!epicGroups.has(epicId)) epicGroups.set(epicId, []);
|
||||
epicGroups.get(epicId).push(id);
|
||||
}
|
||||
|
||||
// Return epic-based groups as parallel units
|
||||
return [...epicGroups.values()];
|
||||
}
|
||||
|
||||
export default { generateBatchPlan };
|
||||
Reference in New Issue
Block a user