mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
@@ -52,8 +52,8 @@ import { useNavigate } from '@tanstack/react-router';
|
|||||||
import {
|
import {
|
||||||
getAncestors,
|
getAncestors,
|
||||||
formatAncestorContextForPrompt,
|
formatAncestorContextForPrompt,
|
||||||
AncestorContext,
|
type AncestorContext,
|
||||||
} from '@/components/views/graph-view/utils';
|
} from '@automaker/dependency-resolver';
|
||||||
|
|
||||||
interface AddFeatureDialogProps {
|
interface AddFeatureDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ChevronDown, ChevronRight, Users } from 'lucide-react';
|
import { ChevronDown, ChevronRight, Users } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
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';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
|
||||||
interface ParentFeatureContext {
|
interface ParentFeatureContext {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Feature, useAppStore } from '@/store/app-store';
|
|||||||
import { GraphCanvas } from './graph-canvas';
|
import { GraphCanvas } from './graph-canvas';
|
||||||
import { useBoardBackground } from '../board-view/hooks';
|
import { useBoardBackground } from '../board-view/hooks';
|
||||||
import { NodeActionCallbacks } from './hooks';
|
import { NodeActionCallbacks } from './hooks';
|
||||||
import { wouldCreateCircularDependency, dependencyExists } from './utils';
|
import { wouldCreateCircularDependency, dependencyExists } from '@automaker/dependency-resolver';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface GraphViewProps {
|
interface GraphViewProps {
|
||||||
|
|||||||
@@ -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')}`;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './dependency-validation';
|
|
||||||
export * from './ancestor-context';
|
|
||||||
@@ -7,5 +7,10 @@ export {
|
|||||||
resolveDependencies,
|
resolveDependencies,
|
||||||
areDependenciesSatisfied,
|
areDependenciesSatisfied,
|
||||||
getBlockingDependencies,
|
getBlockingDependencies,
|
||||||
|
wouldCreateCircularDependency,
|
||||||
|
dependencyExists,
|
||||||
|
getAncestors,
|
||||||
|
formatAncestorContextForPrompt,
|
||||||
type DependencyResolutionResult,
|
type DependencyResolutionResult,
|
||||||
|
type AncestorContext,
|
||||||
} from './resolver.js';
|
} from './resolver.js';
|
||||||
|
|||||||
@@ -209,3 +209,148 @@ export function getBlockingDependencies(feature: Feature, allFeatures: Feature[]
|
|||||||
return dep && dep.status !== 'completed' && dep.status !== 'verified';
|
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')}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user