refactor: Move utility functions to @automaker/dependency-resolver

Consolidated dependency validation and ancestor traversal utilities:
- wouldCreateCircularDependency, dependencyExists -> @automaker/dependency-resolver
- getAncestors, formatAncestorContextForPrompt, AncestorContext -> @automaker/dependency-resolver
- Removed graph-view/utils directory (now redundant)
- Updated all imports to use shared package

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jbotwina
2025-12-23 11:25:55 -05:00
committed by James
parent 8d80c73faa
commit 76b7cfec9e
8 changed files with 154 additions and 150 deletions

View File

@@ -52,8 +52,8 @@ import { useNavigate } from '@tanstack/react-router';
import {
getAncestors,
formatAncestorContextForPrompt,
AncestorContext,
} from '@/components/views/graph-view/utils';
type AncestorContext,
} from '@automaker/dependency-resolver';
interface AddFeatureDialogProps {
open: boolean;

View File

@@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { ChevronDown, ChevronRight, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AncestorContext } from '@/components/views/graph-view/utils';
import type { AncestorContext } from '@automaker/dependency-resolver';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
interface ParentFeatureContext {

View File

@@ -3,7 +3,7 @@ import { Feature, useAppStore } from '@/store/app-store';
import { GraphCanvas } from './graph-canvas';
import { useBoardBackground } from '../board-view/hooks';
import { NodeActionCallbacks } from './hooks';
import { wouldCreateCircularDependency, dependencyExists } from './utils';
import { wouldCreateCircularDependency, dependencyExists } from '@automaker/dependency-resolver';
import { toast } from 'sonner';
interface GraphViewProps {

View File

@@ -1,93 +0,0 @@
import { Feature } from '@/store/app-store';
export interface AncestorContext {
id: string;
title?: string;
description: string;
spec?: string;
summary?: string;
depth: number; // 0 = immediate parent, 1 = grandparent, etc.
}
/**
* Traverses the dependency graph to find all ancestors of a feature.
* Returns ancestors ordered by depth (closest first).
*
* @param feature - The feature to find ancestors for
* @param allFeatures - All features in the system
* @param maxDepth - Maximum depth to traverse (prevents infinite loops)
* @returns Array of ancestor contexts, sorted by depth (closest first)
*/
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);
// Sort by depth (closest ancestors first)
return ancestors.sort((a, b) => a.depth - b.depth);
}
/**
* Formats ancestor context for inclusion in a task description.
*
* @param ancestors - Array of ancestor contexts (including parent)
* @param selectedIds - Set of selected ancestor IDs to include
* @returns Formatted markdown string with ancestor context
*/
export function formatAncestorContextForPrompt(
ancestors: AncestorContext[],
selectedIds: Set<string>
): string {
const selectedAncestors = ancestors.filter((a) => selectedIds.has(a.id));
if (selectedAncestors.length === 0) return '';
const sections = selectedAncestors.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');
});
return `## Ancestor Context\n\n${sections.join('\n\n---\n\n')}`;
}

View File

@@ -1,51 +0,0 @@
import { Feature } from '@/store/app-store';
/**
* Checks if adding a dependency from sourceId to targetId would create a circular dependency.
* Uses DFS to detect if targetId can reach sourceId through existing dependencies.
*
* @param features - All features in the system
* @param sourceId - The feature that would become a dependency (the prerequisite)
* @param targetId - The feature that would depend on sourceId
* @returns true if adding this dependency would create a cycle
*/
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(currentId: string, targetId: string): boolean {
if (currentId === targetId) return true;
if (visited.has(currentId)) return false;
visited.add(currentId);
const feature = featureMap.get(currentId);
if (!feature?.dependencies) return false;
for (const depId of feature.dependencies) {
if (canReach(depId, targetId)) return true;
}
return false;
}
// Check if source can reach target through existing dependencies
// If so, adding target -> source would create a cycle
return canReach(sourceId, targetId);
}
/**
* Checks if a dependency already exists between two features.
*
* @param features - All features in the system
* @param sourceId - The potential dependency (prerequisite)
* @param targetId - The feature that might depend on sourceId
* @returns true if targetId already depends on sourceId
*/
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);
}

View File

@@ -1,2 +0,0 @@
export * from './dependency-validation';
export * from './ancestor-context';

View File

@@ -7,5 +7,10 @@ export {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
wouldCreateCircularDependency,
dependencyExists,
getAncestors,
formatAncestorContextForPrompt,
type DependencyResolutionResult,
type AncestorContext,
} from './resolver.js';

View File

@@ -209,3 +209,148 @@ export function getBlockingDependencies(feature: Feature, allFeatures: Feature[]
return dep && dep.status !== 'completed' && dep.status !== 'verified';
});
}
/**
* Checks if adding a dependency from sourceId to targetId would create a circular dependency.
* Uses DFS to detect if targetId can reach sourceId through existing dependencies.
*
* @param features - All features in the system
* @param sourceId - The feature that would become a dependency (the prerequisite)
* @param targetId - The feature that would depend on sourceId
* @returns true if adding this dependency would create a cycle
*/
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(currentId: string, target: string): boolean {
if (currentId === target) return true;
if (visited.has(currentId)) return false;
visited.add(currentId);
const feature = featureMap.get(currentId);
if (!feature?.dependencies) return false;
for (const depId of feature.dependencies) {
if (canReach(depId, target)) return true;
}
return false;
}
// Check if source can reach target through existing dependencies
// If so, adding target -> source would create a cycle
return canReach(sourceId, targetId);
}
/**
* Checks if a dependency already exists between two features.
*
* @param features - All features in the system
* @param sourceId - The potential dependency (prerequisite)
* @param targetId - The feature that might depend on sourceId
* @returns true if targetId already depends on sourceId
*/
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);
}
/**
* Context information about an ancestor feature in the dependency graph.
*/
export interface AncestorContext {
id: string;
title?: string;
description: string;
spec?: string;
summary?: string;
depth: number; // 0 = immediate parent, 1 = grandparent, etc.
}
/**
* Traverses the dependency graph to find all ancestors of a feature.
* Returns ancestors ordered by depth (closest first).
*
* @param feature - The feature to find ancestors for
* @param allFeatures - All features in the system
* @param maxDepth - Maximum depth to traverse (prevents infinite loops)
* @returns Array of ancestor contexts, sorted by depth (closest first)
*/
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);
// Sort by depth (closest ancestors first)
return ancestors.sort((a, b) => a.depth - b.depth);
}
/**
* Formats ancestor context for inclusion in a task description.
*
* @param ancestors - Array of ancestor contexts (including parent)
* @param selectedIds - Set of selected ancestor IDs to include
* @returns Formatted markdown string with ancestor context
*/
export function formatAncestorContextForPrompt(
ancestors: AncestorContext[],
selectedIds: Set<string>
): string {
const selectedAncestors = ancestors.filter((a) => selectedIds.has(a.id));
if (selectedAncestors.length === 0) return '';
const sections = selectedAncestors.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');
});
return `## Ancestor Context\n\n${sections.join('\n\n---\n\n')}`;
}