From 8b3103955798cf059872ef0cb99c2f46477fa4cc Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 19 Dec 2025 23:30:57 +0100 Subject: [PATCH] feat: add @automaker/dependency-resolver package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ELIMINATES CODE DUPLICATION: This file was duplicated in both server and UI (222 lines each). - Extract feature dependency resolution using topological sort - Implement Kahn's algorithm with priority-aware ordering - Detect circular dependencies using DFS - Check for missing and blocking dependencies - Provide helper functions (areDependenciesSatisfied, getBlockingDependencies) This package will replace: - apps/server/src/lib/dependency-resolver.ts (to be deleted) - apps/ui/src/lib/dependency-resolver.ts (to be deleted) Impact: Eliminates 222 lines of duplicated code. Dependencies: @automaker/types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- libs/dependency-resolver/package.json | 21 +++ libs/dependency-resolver/src/index.ts | 11 ++ libs/dependency-resolver/src/resolver.ts | 221 +++++++++++++++++++++++ libs/dependency-resolver/tsconfig.json | 20 ++ 4 files changed, 273 insertions(+) create mode 100644 libs/dependency-resolver/package.json create mode 100644 libs/dependency-resolver/src/index.ts create mode 100644 libs/dependency-resolver/src/resolver.ts create mode 100644 libs/dependency-resolver/tsconfig.json diff --git a/libs/dependency-resolver/package.json b/libs/dependency-resolver/package.json new file mode 100644 index 00000000..7f2c9254 --- /dev/null +++ b/libs/dependency-resolver/package.json @@ -0,0 +1,21 @@ +{ + "name": "@automaker/dependency-resolver", + "version": "1.0.0", + "description": "Feature dependency resolution for AutoMaker", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "keywords": ["automaker", "dependency", "resolver"], + "author": "", + "license": "MIT", + "dependencies": { + "@automaker/types": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "typescript": "^5.7.3" + } +} diff --git a/libs/dependency-resolver/src/index.ts b/libs/dependency-resolver/src/index.ts new file mode 100644 index 00000000..d9b7cf72 --- /dev/null +++ b/libs/dependency-resolver/src/index.ts @@ -0,0 +1,11 @@ +/** + * @automaker/dependency-resolver + * Feature dependency resolution for AutoMaker + */ + +export { + resolveDependencies, + areDependenciesSatisfied, + getBlockingDependencies, + type DependencyResolutionResult, +} from './resolver'; diff --git a/libs/dependency-resolver/src/resolver.ts b/libs/dependency-resolver/src/resolver.ts new file mode 100644 index 00000000..d8115646 --- /dev/null +++ b/libs/dependency-resolver/src/resolver.ts @@ -0,0 +1,221 @@ +/** + * Dependency Resolution Utility + * + * Provides topological sorting and dependency analysis for features. + * Uses a modified Kahn's algorithm that respects both dependencies and priorities. + */ + +import type { Feature } from '@automaker/types'; + +export interface DependencyResolutionResult { + orderedFeatures: Feature[]; // Features in dependency-aware order + circularDependencies: string[][]; // Groups of IDs forming cycles + missingDependencies: Map; // featureId -> missing dep IDs + blockedFeatures: Map; // featureId -> blocking dep IDs (incomplete dependencies) +} + +/** + * Resolves feature dependencies using topological sort with priority-aware ordering. + * + * Algorithm: + * 1. Build dependency graph and detect missing/blocked dependencies + * 2. Apply Kahn's algorithm for topological sort + * 3. Within same dependency level, sort by priority (1=high, 2=medium, 3=low) + * 4. Detect circular dependencies for features that can't be ordered + * + * @param features - Array of features to order + * @returns Resolution result with ordered features and dependency metadata + */ +export function resolveDependencies(features: Feature[]): DependencyResolutionResult { + const featureMap = new Map(features.map(f => [f.id, f])); + const inDegree = new Map(); + const adjacencyList = new Map(); // dependencyId -> [dependentIds] + const missingDependencies = new Map(); + const blockedFeatures = new Map(); + + // Initialize graph structures + for (const feature of features) { + inDegree.set(feature.id, 0); + adjacencyList.set(feature.id, []); + } + + // Build dependency graph and detect missing/blocked dependencies + for (const feature of features) { + const deps = feature.dependencies || []; + for (const depId of deps) { + if (!featureMap.has(depId)) { + // Missing dependency - track it + if (!missingDependencies.has(feature.id)) { + missingDependencies.set(feature.id, []); + } + missingDependencies.get(feature.id)!.push(depId); + } else { + // Valid dependency - add edge to graph + adjacencyList.get(depId)!.push(feature.id); + inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1); + + // Check if dependency is incomplete (blocking) + 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); + } + } + } + } + + // Kahn's algorithm with priority-aware selection + const queue: Feature[] = []; + const orderedFeatures: Feature[] = []; + + // Helper to sort features by priority (lower number = higher priority) + const sortByPriority = (a: Feature, b: Feature) => + (a.priority ?? 2) - (b.priority ?? 2); + + // Start with features that have no dependencies (in-degree 0) + for (const [id, degree] of inDegree) { + if (degree === 0) { + queue.push(featureMap.get(id)!); + } + } + + // Sort initial queue by priority + queue.sort(sortByPriority); + + // Process features in topological order + while (queue.length > 0) { + // Take highest priority feature from queue + const current = queue.shift()!; + orderedFeatures.push(current); + + // Process features that depend on this one + 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)!); + // Re-sort queue to maintain priority order + queue.sort(sortByPriority); + } + } + } + + // Detect circular dependencies (features not in output = part of cycle) + const circularDependencies: string[][] = []; + const processedIds = new Set(orderedFeatures.map(f => f.id)); + + if (orderedFeatures.length < features.length) { + // Find cycles using DFS + const remaining = features.filter(f => !processedIds.has(f.id)); + const cycles = detectCycles(remaining, featureMap); + circularDependencies.push(...cycles); + + // Add remaining features at end (part of cycles) + orderedFeatures.push(...remaining); + } + + return { + orderedFeatures, + circularDependencies, + missingDependencies, + blockedFeatures + }; +} + +/** + * Detects circular dependencies using depth-first search + * + * @param features - Features that couldn't be topologically sorted (potential cycles) + * @param featureMap - Map of all features by ID + * @returns Array of cycles, where each cycle is an array of feature IDs + */ +function detectCycles( + features: Feature[], + featureMap: Map +): string[][] { + const cycles: string[][] = []; + const visited = new Set(); + const recursionStack = new Set(); + 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)) { + // Found cycle - extract it + 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; +} + +/** + * Checks if a feature's dependencies are satisfied (all complete or verified) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns true if all dependencies are satisfied, false otherwise + */ +export function areDependenciesSatisfied( + feature: Feature, + allFeatures: Feature[] +): boolean { + if (!feature.dependencies || feature.dependencies.length === 0) { + return true; // No dependencies = always ready + } + + return feature.dependencies.every((depId: string) => { + const dep = allFeatures.find(f => f.id === depId); + return dep && (dep.status === 'completed' || dep.status === 'verified'); + }); +} + +/** + * Gets the blocking dependencies for a feature (dependencies that are incomplete) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns Array of feature IDs that are blocking this feature + */ +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'; + }); +} diff --git a/libs/dependency-resolver/tsconfig.json b/libs/dependency-resolver/tsconfig.json new file mode 100644 index 00000000..54e9774b --- /dev/null +++ b/libs/dependency-resolver/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "types": ["node"], + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}