feat: auto-create Integration phase for multi-leaf initiatives

When an initiative has multiple end phases (leaf nodes with no
dependents), queueAllPhases now auto-creates an Integration phase
that depends on all of them. This catches cross-phase incompatibilities
(type mismatches, conflicting exports, broken tests) before review.
This commit is contained in:
Lukas May
2026-03-06 21:31:20 +01:00
parent 094b7e6307
commit 0f53930610
2 changed files with 72 additions and 2 deletions

View File

@@ -4,10 +4,35 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import type { Task } from '../../db/schema.js';
import type { Phase, Task } from '../../db/schema.js';
import type { ProcedureBuilder } from '../trpc.js';
import { requirePhaseDispatchManager, requirePhaseRepository, requireTaskRepository } from './_helpers.js';
const INTEGRATION_PHASE_NAME = 'Integration';
const INTEGRATION_TASK_DESCRIPTION = `Verify that all phase branches integrate correctly after merging into the initiative branch.
Steps:
1. Build the project — fix any compilation errors
2. Run the full test suite — fix any failing tests
3. Run type checking and linting — fix any errors
4. Review cross-phase imports and shared interfaces for compatibility
5. Smoke test key user flows affected by the merged changes
Only fix integration issues (type mismatches, conflicting exports, broken tests). Do not refactor or improve existing code.`;
/**
* Find phase IDs that have no dependents (no other phase depends on them).
* These are the "end" / "leaf" phases in the dependency graph.
*/
function findEndPhaseIds(
phases: Phase[],
edges: Array<{ phaseId: string; dependsOnPhaseId: string }>,
): string[] {
const dependedOn = new Set(edges.map((e) => e.dependsOnPhaseId));
return phases.filter((p) => !dependedOn.has(p.id)).map((p) => p.id);
}
export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) {
return {
queuePhase: publicProcedure
@@ -23,7 +48,40 @@ export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) {
.mutation(async ({ ctx, input }) => {
const phaseDispatchManager = requirePhaseDispatchManager(ctx);
const phaseRepo = requirePhaseRepository(ctx);
const phases = await phaseRepo.findByInitiativeId(input.initiativeId);
const taskRepo = requireTaskRepository(ctx);
let phases = await phaseRepo.findByInitiativeId(input.initiativeId);
const edges = await phaseRepo.findDependenciesByInitiativeId(input.initiativeId);
// Auto-create Integration phase if multiple end phases exist
const existingIntegration = phases.find((p) => p.name === INTEGRATION_PHASE_NAME);
if (!existingIntegration) {
const endPhaseIds = findEndPhaseIds(phases, edges);
if (endPhaseIds.length > 1) {
const integrationPhase = await phaseRepo.create({
initiativeId: input.initiativeId,
name: INTEGRATION_PHASE_NAME,
status: 'approved',
});
for (const endPhaseId of endPhaseIds) {
await phaseRepo.createDependency(integrationPhase.id, endPhaseId);
}
await taskRepo.create({
phaseId: integrationPhase.id,
initiativeId: input.initiativeId,
name: 'Verify integration',
description: INTEGRATION_TASK_DESCRIPTION,
category: 'verify',
status: 'pending',
});
// Re-fetch so the new phase gets queued in the loop below
phases = await phaseRepo.findByInitiativeId(input.initiativeId);
}
}
let queued = 0;
for (const phase of phases) {
if (phase.status === 'approved') {