| |
| |
| |
| |
| |
| |
|
|
| import type { Feature } from '@automaker/types'; |
|
|
| export interface DependencyResolutionResult { |
| orderedFeatures: Feature[]; |
| circularDependencies: string[][]; |
| missingDependencies: Map<string, string[]>; |
| blockedFeatures: Map<string, string[]>; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function resolveDependencies(features: Feature[]): DependencyResolutionResult { |
| const featureMap = new Map<string, Feature>(features.map((f) => [f.id, f])); |
| const inDegree = new Map<string, number>(); |
| const adjacencyList = new Map<string, string[]>(); |
| const missingDependencies = new Map<string, string[]>(); |
| const blockedFeatures = new Map<string, string[]>(); |
|
|
| |
| for (const feature of features) { |
| inDegree.set(feature.id, 0); |
| adjacencyList.set(feature.id, []); |
| } |
|
|
| |
| for (const feature of features) { |
| const deps = feature.dependencies || []; |
| for (const depId of deps) { |
| if (!featureMap.has(depId)) { |
| |
| if (!missingDependencies.has(feature.id)) { |
| missingDependencies.set(feature.id, []); |
| } |
| missingDependencies.get(feature.id)!.push(depId); |
| } else { |
| |
| adjacencyList.get(depId)!.push(feature.id); |
| inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1); |
|
|
| |
| const depFeature = featureMap.get(depId)!; |
| if (depFeature.status !== 'completed' && depFeature.status !== 'verified') { |
| if (!blockedFeatures.has(feature.id)) { |
| blockedFeatures.set(feature.id, []); |
| } |
| blockedFeatures.get(feature.id)!.push(depId); |
| } |
| } |
| } |
| } |
|
|
| |
| const queue: Feature[] = []; |
| const orderedFeatures: Feature[] = []; |
|
|
| |
| const sortByPriority = (a: Feature, b: Feature) => (a.priority ?? 2) - (b.priority ?? 2); |
|
|
| |
| for (const [id, degree] of inDegree) { |
| if (degree === 0) { |
| queue.push(featureMap.get(id)!); |
| } |
| } |
|
|
| |
| queue.sort(sortByPriority); |
|
|
| |
| while (queue.length > 0) { |
| |
| const current = queue.shift()!; |
| orderedFeatures.push(current); |
|
|
| |
| for (const dependentId of adjacencyList.get(current.id) || []) { |
| const currentDegree = inDegree.get(dependentId); |
| if (currentDegree === undefined) { |
| throw new Error(`In-degree not initialized for feature ${dependentId}`); |
| } |
| const newDegree = currentDegree - 1; |
| inDegree.set(dependentId, newDegree); |
|
|
| if (newDegree === 0) { |
| queue.push(featureMap.get(dependentId)!); |
| |
| queue.sort(sortByPriority); |
| } |
| } |
| } |
|
|
| |
| const circularDependencies: string[][] = []; |
| const processedIds = new Set(orderedFeatures.map((f) => f.id)); |
|
|
| if (orderedFeatures.length < features.length) { |
| |
| const remaining = features.filter((f) => !processedIds.has(f.id)); |
| const cycles = detectCycles(remaining, featureMap); |
| circularDependencies.push(...cycles); |
|
|
| |
| orderedFeatures.push(...remaining); |
| } |
|
|
| return { |
| orderedFeatures, |
| circularDependencies, |
| missingDependencies, |
| blockedFeatures, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function detectCycles(features: Feature[], featureMap: Map<string, Feature>): string[][] { |
| const cycles: string[][] = []; |
| const visited = new Set<string>(); |
| const recursionStack = new Set<string>(); |
| const currentPath: string[] = []; |
|
|
| function dfs(featureId: string): boolean { |
| visited.add(featureId); |
| recursionStack.add(featureId); |
| currentPath.push(featureId); |
|
|
| const feature = featureMap.get(featureId); |
| if (feature) { |
| for (const depId of feature.dependencies || []) { |
| if (!visited.has(depId)) { |
| if (dfs(depId)) return true; |
| } else if (recursionStack.has(depId)) { |
| |
| const cycleStart = currentPath.indexOf(depId); |
| cycles.push(currentPath.slice(cycleStart)); |
| return true; |
| } |
| } |
| } |
|
|
| currentPath.pop(); |
| recursionStack.delete(featureId); |
| return false; |
| } |
|
|
| for (const feature of features) { |
| if (!visited.has(feature.id)) { |
| dfs(feature.id); |
| } |
| } |
|
|
| return cycles; |
| } |
|
|
| export interface DependencySatisfactionOptions { |
| |
| skipVerification?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function areDependenciesSatisfied( |
| feature: Feature, |
| allFeatures: Feature[], |
| options?: DependencySatisfactionOptions |
| ): boolean { |
| if (!feature.dependencies || feature.dependencies.length === 0) { |
| return true; |
| } |
|
|
| const skipVerification = options?.skipVerification ?? false; |
|
|
| return feature.dependencies.every((depId: string) => { |
| const dep = allFeatures.find((f) => f.id === depId); |
| if (!dep) return false; |
|
|
| if (skipVerification) { |
| |
| return dep.status !== 'running'; |
| } |
| |
| return dep.status === 'completed' || dep.status === 'verified'; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getBlockingDependencies(feature: Feature, allFeatures: Feature[]): string[] { |
| if (!feature.dependencies || feature.dependencies.length === 0) { |
| return []; |
| } |
|
|
| return feature.dependencies.filter((depId: string) => { |
| const dep = allFeatures.find((f) => f.id === depId); |
| return dep && dep.status !== 'completed' && dep.status !== 'verified'; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function createFeatureMap(features: Feature[]): Map<string, Feature> { |
| const featureMap = new Map<string, Feature>(); |
| for (const feature of features) { |
| if (feature?.id) { |
| featureMap.set(feature.id, feature); |
| } |
| } |
| return featureMap; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getBlockingDependenciesFromMap( |
| feature: Feature, |
| featureMap: Map<string, Feature> |
| ): string[] { |
| const dependencies = feature.dependencies; |
| if (!dependencies || dependencies.length === 0) { |
| return []; |
| } |
|
|
| const blockingDependencies: string[] = []; |
| for (const depId of dependencies) { |
| const dep = featureMap.get(depId); |
| if (dep && dep.status !== 'completed' && dep.status !== 'verified') { |
| blockingDependencies.push(depId); |
| } |
| } |
|
|
| return blockingDependencies; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function wouldCreateCircularDependency( |
| features: Feature[], |
| sourceId: string, |
| targetId: string |
| ): boolean { |
| const featureMap = new Map(features.map((f) => [f.id, f])); |
| const visited = new Set<string>(); |
|
|
| |
| function canReach(fromId: string, toId: string): boolean { |
| if (fromId === toId) return true; |
| if (visited.has(fromId)) return false; |
|
|
| visited.add(fromId); |
| const feature = featureMap.get(fromId); |
| if (!feature?.dependencies) return false; |
|
|
| for (const depId of feature.dependencies) { |
| if (canReach(depId, toId)) return true; |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| return canReach(sourceId, targetId); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function dependencyExists(features: Feature[], sourceId: string, targetId: string): boolean { |
| const targetFeature = features.find((f) => f.id === targetId); |
| if (!targetFeature?.dependencies) return false; |
| return targetFeature.dependencies.includes(sourceId); |
| } |
|
|
| |
| |
| |
| export interface AncestorContext { |
| id: string; |
| title?: string; |
| description: string; |
| spec?: string; |
| summary?: string; |
| depth: number; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function getAncestors( |
| feature: Feature, |
| allFeatures: Feature[], |
| maxDepth: number = 10 |
| ): AncestorContext[] { |
| const featureMap = new Map(allFeatures.map((f) => [f.id, f])); |
| const ancestors: AncestorContext[] = []; |
| const visited = new Set<string>(); |
|
|
| function traverse(featureId: string, depth: number) { |
| if (depth > maxDepth || visited.has(featureId)) return; |
| visited.add(featureId); |
|
|
| const f = featureMap.get(featureId); |
| if (!f?.dependencies) return; |
|
|
| for (const depId of f.dependencies) { |
| const dep = featureMap.get(depId); |
| if (dep && !visited.has(depId)) { |
| ancestors.push({ |
| id: dep.id, |
| title: dep.title, |
| description: dep.description, |
| spec: dep.spec, |
| summary: dep.summary, |
| depth, |
| }); |
| traverse(depId, depth + 1); |
| } |
| } |
| } |
|
|
| traverse(feature.id, 0); |
|
|
| |
| return ancestors.sort((a, b) => a.depth - b.depth); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function formatAncestorContextForPrompt( |
| ancestors: AncestorContext[], |
| selectedIds: Set<string> |
| ): string { |
| const selectedAncestors = ancestors.filter((a) => selectedIds.has(a.id)); |
| if (selectedAncestors.length === 0) return ''; |
|
|
| |
| const parent = selectedAncestors.find((a) => a.depth === -1); |
| const otherAncestors = selectedAncestors.filter((a) => a.depth !== -1); |
|
|
| const sections: string[] = []; |
|
|
| |
| if (parent) { |
| const parentTitle = parent.title || `Task (${parent.id.slice(0, 8)})`; |
| const parentParts: string[] = []; |
|
|
| parentParts.push(`## Parent Task Context (Already Completed)`); |
| parentParts.push( |
| `> **Note:** The following parent task has already been completed. This context is provided to help you understand the background and requirements for this sub-task. Do not re-implement the parent task - focus only on the new sub-task described below.` |
| ); |
| parentParts.push(`### ${parentTitle}`); |
|
|
| if (parent.description) { |
| parentParts.push(`**Description:** ${parent.description}`); |
| } |
| if (parent.spec) { |
| parentParts.push(`**Specification:**\n${parent.spec}`); |
| } |
| if (parent.summary) { |
| parentParts.push(`**Summary:** ${parent.summary}`); |
| } |
|
|
| sections.push(parentParts.join('\n\n')); |
| } |
|
|
| |
| if (otherAncestors.length > 0) { |
| const ancestorSections = otherAncestors.map((ancestor) => { |
| const parts: string[] = []; |
| const title = ancestor.title || `Task (${ancestor.id.slice(0, 8)})`; |
|
|
| parts.push(`### ${title}`); |
|
|
| if (ancestor.description) { |
| parts.push(`**Description:** ${ancestor.description}`); |
| } |
| if (ancestor.spec) { |
| parts.push(`**Specification:**\n${ancestor.spec}`); |
| } |
| if (ancestor.summary) { |
| parts.push(`**Summary:** ${ancestor.summary}`); |
| } |
|
|
| return parts.join('\n\n'); |
| }); |
|
|
| sections.push(`## Additional Ancestor Context\n\n${ancestorSections.join('\n\n---\n\n')}`); |
| } |
|
|
| return sections.join('\n\n---\n\n'); |
| } |
|
|