File size: 15,278 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
/**
 * 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<string, string[]>; // featureId -> missing dep IDs
  blockedFeatures: Map<string, string[]>; // 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<string, Feature>(features.map((f) => [f.id, f]));
  const inDegree = new Map<string, number>();
  const adjacencyList = new Map<string, string[]>(); // dependencyId -> [dependentIds]
  const missingDependencies = new Map<string, string[]>();
  const blockedFeatures = new Map<string, string[]>();

  // 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, 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)) {
          // 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;
}

export interface DependencySatisfactionOptions {
  /** If true, only require dependencies to not be 'running' (ignore verification requirement) */
  skipVerification?: boolean;
}

/**
 * Checks if a feature's dependencies are satisfied (all complete or verified)
 *
 * @param feature - Feature to check
 * @param allFeatures - All features in the project
 * @param options - Optional configuration for dependency checking
 * @returns true if all dependencies are satisfied, false otherwise
 */
export function areDependenciesSatisfied(
  feature: Feature,
  allFeatures: Feature[],
  options?: DependencySatisfactionOptions
): boolean {
  if (!feature.dependencies || feature.dependencies.length === 0) {
    return true; // No dependencies = always ready
  }

  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) {
      // When skipping verification, only block if dependency is currently running
      return dep.status !== 'running';
    }
    // Default: require 'completed' or 'verified'
    return 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';
  });
}

/**
 * Builds a lookup map for features by id.
 *
 * @param features - Features to index
 * @returns Map keyed by feature id
 */
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;
}

/**
 * Gets the blocking dependencies using a precomputed feature map.
 *
 * @param feature - Feature to check
 * @param featureMap - Map of all features by id
 * @returns Array of feature IDs that are blocking this feature
 */
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;
}

/**
 * Checks if adding a dependency from sourceId to targetId would create a circular dependency.
 * When we say "targetId depends on sourceId", we add sourceId to targetId.dependencies.
 * A cycle would occur if sourceId already depends on targetId (directly or transitively).
 *
 * @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>();

  // Check if 'from' can reach 'to' by following dependencies
  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;
  }

  // We want to add: targetId depends on sourceId (sourceId -> targetId in dependency graph)
  // This would create a cycle if sourceId already depends on targetId (transitively)
  // i.e., if we can reach targetId starting from sourceId by following dependencies
  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.
 * The parent task (depth=-1) is formatted with special emphasis indicating
 * it was already completed and is provided for context only.
 *
 * @param ancestors - Array of ancestor contexts (including parent with depth=-1)
 * @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 '';

  // Separate parent (depth=-1) from other ancestors
  const parent = selectedAncestors.find((a) => a.depth === -1);
  const otherAncestors = selectedAncestors.filter((a) => a.depth !== -1);

  const sections: string[] = [];

  // Format parent with special emphasis
  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'));
  }

  // Format other ancestors if any
  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');
}