codeframe-ai 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,714 @@
1
+ """
2
+ DAG-based Task Dependency Resolver (Sprint 4: cf-50).
3
+
4
+ This module provides dependency resolution for multi-agent task coordination,
5
+ ensuring tasks are executed in correct order based on their dependencies.
6
+
7
+ Enhanced in Phase 2 with:
8
+ - Critical path analysis (longest path through DAG)
9
+ - Task slack/float calculation
10
+ - Parallel execution opportunity identification
11
+ - Dependency conflict detection and resolution suggestions
12
+ """
13
+
14
+ import logging
15
+ from typing import Dict, List, Set, Optional
16
+ from collections import defaultdict, deque
17
+ from dataclasses import dataclass
18
+
19
+ from codeframe.core.models import Task
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class TaskTiming:
26
+ """Timing information for a task in critical path analysis."""
27
+
28
+ earliest_start: float
29
+ earliest_finish: float
30
+ latest_start: float
31
+ latest_finish: float
32
+
33
+
34
+ @dataclass
35
+ class CriticalPathResult:
36
+ """Result of critical path calculation."""
37
+
38
+ critical_task_ids: List[int]
39
+ total_duration: float
40
+ task_timings: Dict[int, TaskTiming]
41
+
42
+
43
+ @dataclass
44
+ class DependencyConflict:
45
+ """Represents a detected dependency conflict or bottleneck."""
46
+
47
+ task_id: int
48
+ conflict_type: str # "bottleneck", "long_chain", "high_risk_multiplier"
49
+ severity: str # "critical", "high", "medium"
50
+ recommendation: str
51
+ impact_analysis: str
52
+
53
+
54
+ @dataclass
55
+ class ResolutionSuggestion:
56
+ """Suggested resolution for a dependency conflict."""
57
+
58
+ suggestion_type: str # "split_task", "reorder", "prioritize"
59
+ description: str
60
+ affected_task_ids: List[int]
61
+ expected_improvement: str
62
+
63
+
64
+ class DependencyResolver:
65
+ """
66
+ DAG-based dependency resolver for task coordination.
67
+
68
+ Capabilities:
69
+ - Build directed acyclic graph (DAG) from task dependencies
70
+ - Identify ready tasks (all dependencies satisfied)
71
+ - Find newly unblocked tasks after completion
72
+ - Detect circular dependencies
73
+ - Validate dependencies before adding
74
+ - Suggest execution order via topological sort
75
+ """
76
+
77
+ def __init__(self):
78
+ """Initialize dependency resolver."""
79
+ # Adjacency list: task_id -> set of task_ids it depends on
80
+ self.dependencies: Dict[int, Set[int]] = defaultdict(set)
81
+
82
+ # Reverse adjacency list: task_id -> set of task_ids that depend on it
83
+ self.dependents: Dict[int, Set[int]] = defaultdict(set)
84
+
85
+ # Track completed tasks
86
+ self.completed_tasks: Set[int] = set()
87
+
88
+ # Track all known tasks
89
+ self.all_tasks: Set[int] = set()
90
+
91
+ def build_dependency_graph(self, tasks: List[Task]) -> None:
92
+ """
93
+ Build dependency graph from task list.
94
+
95
+ Args:
96
+ tasks: List of tasks with dependency information
97
+
98
+ Raises:
99
+ ValueError: If circular dependencies detected
100
+ """
101
+ # Clear existing graph
102
+ self.dependencies.clear()
103
+ self.dependents.clear()
104
+ self.completed_tasks.clear()
105
+ self.all_tasks.clear()
106
+
107
+ # First pass: register all tasks
108
+ for task in tasks:
109
+ self.all_tasks.add(task.id)
110
+ # Handle both v2 (TaskStatus enum) and legacy (string) status
111
+ status = task.status.value if hasattr(task.status, 'value') else str(task.status)
112
+ if status.upper() in ("DONE", "COMPLETED"):
113
+ self.completed_tasks.add(task.id)
114
+
115
+ # Second pass: build dependency edges
116
+ for task in tasks:
117
+ task_id = task.id
118
+
119
+ # Parse depends_on field - supports both:
120
+ # - List format (v2): [task_id_1, task_id_2]
121
+ # - String format (legacy): "[1, 2, 3]" or "1,2,3"
122
+ dep_ids = []
123
+
124
+ if task.depends_on:
125
+ if isinstance(task.depends_on, list):
126
+ # v2 format: already a list
127
+ dep_ids = task.depends_on
128
+ elif isinstance(task.depends_on, str) and task.depends_on.strip():
129
+ # Legacy string format
130
+ depends_on_str = task.depends_on.strip()
131
+
132
+ if depends_on_str.startswith("[") and depends_on_str.endswith("]"):
133
+ # JSON array format
134
+ import json
135
+
136
+ try:
137
+ dep_ids = json.loads(depends_on_str)
138
+ except json.JSONDecodeError:
139
+ logger.warning(
140
+ f"Invalid JSON in depends_on for task {task_id}: {depends_on_str}"
141
+ )
142
+ dep_ids = []
143
+ else:
144
+ # Comma-separated format
145
+ try:
146
+ dep_ids = [int(x.strip()) for x in depends_on_str.split(",") if x.strip()]
147
+ except ValueError:
148
+ logger.warning(
149
+ f"Invalid depends_on format for task {task_id}: {depends_on_str}"
150
+ )
151
+ dep_ids = []
152
+
153
+ for dep_id in dep_ids:
154
+ if dep_id == task_id:
155
+ raise ValueError(
156
+ f"Task {task_id} cannot depend on itself (self-dependency)"
157
+ )
158
+
159
+ if dep_id not in self.all_tasks:
160
+ logger.warning(
161
+ f"Task {task_id} depends on unknown task {dep_id}. "
162
+ "Dependency will be tracked but may cause blocking."
163
+ )
164
+
165
+ self.dependencies[task_id].add(dep_id)
166
+ self.dependents[dep_id].add(task_id)
167
+
168
+ # Validate no cycles
169
+ if self.detect_cycles():
170
+ cycles = self._find_cycle_details()
171
+ raise ValueError(f"Circular dependencies detected: {cycles}")
172
+
173
+ logger.info(
174
+ f"Built dependency graph: {len(self.all_tasks)} tasks, "
175
+ f"{sum(len(deps) for deps in self.dependencies.values())} dependencies"
176
+ )
177
+
178
+ def get_ready_tasks(self, exclude_completed: bool = True) -> List[int]:
179
+ """
180
+ Get tasks that are ready to execute (all dependencies satisfied).
181
+
182
+ Args:
183
+ exclude_completed: If True, exclude already completed tasks
184
+
185
+ Returns:
186
+ List of task IDs ready for execution
187
+ """
188
+ ready = []
189
+
190
+ for task_id in self.all_tasks:
191
+ # Skip completed tasks if requested
192
+ if exclude_completed and task_id in self.completed_tasks:
193
+ continue
194
+
195
+ # Check if all dependencies are satisfied
196
+ deps = self.dependencies.get(task_id, set())
197
+
198
+ if not deps:
199
+ # No dependencies - always ready
200
+ ready.append(task_id)
201
+ elif deps.issubset(self.completed_tasks):
202
+ # All dependencies completed
203
+ ready.append(task_id)
204
+
205
+ return sorted(ready)
206
+
207
+ def unblock_dependent_tasks(self, completed_task_id: int) -> List[int]:
208
+ """
209
+ Find tasks that become unblocked after a task completes.
210
+
211
+ Args:
212
+ completed_task_id: ID of task that just completed
213
+
214
+ Returns:
215
+ List of task IDs that are now unblocked
216
+ """
217
+ # Mark task as completed
218
+ self.completed_tasks.add(completed_task_id)
219
+
220
+ # Find tasks that depend on this task
221
+ dependent_ids = self.dependents.get(completed_task_id, set())
222
+
223
+ unblocked = []
224
+ for dep_id in dependent_ids:
225
+ # Check if all dependencies are now satisfied
226
+ all_deps = self.dependencies.get(dep_id, set())
227
+ if all_deps.issubset(self.completed_tasks):
228
+ unblocked.append(dep_id)
229
+
230
+ logger.debug(
231
+ f"Task {completed_task_id} completion unblocked {len(unblocked)} tasks: {unblocked}"
232
+ )
233
+
234
+ return sorted(unblocked)
235
+
236
+ def detect_cycles(self) -> bool:
237
+ """
238
+ Detect if dependency graph contains cycles using DFS.
239
+
240
+ Returns:
241
+ True if cycles detected, False otherwise
242
+ """
243
+ # Track visited nodes and nodes in current DFS path
244
+ visited = set()
245
+ rec_stack = set()
246
+
247
+ def has_cycle(node: int) -> bool:
248
+ """DFS helper to detect cycle from node."""
249
+ visited.add(node)
250
+ rec_stack.add(node)
251
+
252
+ # Check all dependencies of this node
253
+ for dep in self.dependencies.get(node, set()):
254
+ if dep not in visited:
255
+ if has_cycle(dep):
256
+ return True
257
+ elif dep in rec_stack:
258
+ # Found a back edge (cycle)
259
+ return True
260
+
261
+ rec_stack.remove(node)
262
+ return False
263
+
264
+ # Check all nodes
265
+ for task_id in self.all_tasks:
266
+ if task_id not in visited:
267
+ if has_cycle(task_id):
268
+ return True
269
+
270
+ return False
271
+
272
+ def validate_dependency(self, task_id: int, depends_on_id: int) -> bool:
273
+ """
274
+ Validate adding a dependency would not create a cycle.
275
+
276
+ Args:
277
+ task_id: Task that will depend on another
278
+ depends_on_id: Task to depend on
279
+
280
+ Returns:
281
+ True if dependency is valid (no cycle), False if would create cycle
282
+
283
+ Raises:
284
+ ValueError: If self-dependency attempted
285
+ """
286
+ if task_id == depends_on_id:
287
+ raise ValueError(f"Task {task_id} cannot depend on itself (self-dependency)")
288
+
289
+ # Temporarily add the dependency
290
+ self.dependencies[task_id].add(depends_on_id)
291
+ self.dependents[depends_on_id].add(task_id)
292
+
293
+ # Check for cycles
294
+ has_cycle = self.detect_cycles()
295
+
296
+ # Remove temporary dependency
297
+ self.dependencies[task_id].discard(depends_on_id)
298
+ self.dependents[depends_on_id].discard(task_id)
299
+
300
+ if has_cycle:
301
+ logger.warning(
302
+ f"Cannot add dependency: task {task_id} → {depends_on_id} " "would create a cycle"
303
+ )
304
+ return False
305
+
306
+ return True
307
+
308
+ def topological_sort(self) -> Optional[List[int]]:
309
+ """
310
+ Compute topological ordering of tasks using Kahn's algorithm.
311
+
312
+ Returns:
313
+ List of task IDs in topological order, or None if cycle exists
314
+ """
315
+ # Compute in-degree for each task
316
+ in_degree = {
317
+ task_id: len(self.dependencies.get(task_id, set())) for task_id in self.all_tasks
318
+ }
319
+
320
+ # Queue of tasks with no dependencies
321
+ queue = deque([task_id for task_id in self.all_tasks if in_degree[task_id] == 0])
322
+
323
+ result = []
324
+
325
+ while queue:
326
+ # Process task with no remaining dependencies
327
+ task_id = queue.popleft()
328
+ result.append(task_id)
329
+
330
+ # Reduce in-degree of dependent tasks
331
+ for dependent_id in self.dependents.get(task_id, set()):
332
+ in_degree[dependent_id] -= 1
333
+ if in_degree[dependent_id] == 0:
334
+ queue.append(dependent_id)
335
+
336
+ # If we processed all tasks, we have a valid topological order
337
+ if len(result) == len(self.all_tasks):
338
+ return result
339
+ else:
340
+ # Cycle detected
341
+ logger.error("Cannot perform topological sort: cycle detected")
342
+ return None
343
+
344
+ def get_dependency_depth(self, task_id: int) -> int:
345
+ """
346
+ Get maximum dependency depth for a task (for priority calculation).
347
+
348
+ Args:
349
+ task_id: Task ID to analyze
350
+
351
+ Returns:
352
+ Maximum depth (0 for no dependencies, N for N levels deep)
353
+ """
354
+ if task_id not in self.all_tasks:
355
+ return 0
356
+
357
+ # Use BFS to find maximum depth
358
+ deps = self.dependencies.get(task_id, set())
359
+ if not deps:
360
+ return 0
361
+
362
+ max_depth = 0
363
+ for dep_id in deps:
364
+ depth = 1 + self.get_dependency_depth(dep_id)
365
+ max_depth = max(max_depth, depth)
366
+
367
+ return max_depth
368
+
369
+ def get_blocked_tasks(self) -> Dict[int, List[int]]:
370
+ """
371
+ Get all blocked tasks and what they're blocked by.
372
+
373
+ Returns:
374
+ Dict mapping task_id to list of blocking task_ids
375
+ """
376
+ blocked = {}
377
+
378
+ for task_id in self.all_tasks:
379
+ if task_id in self.completed_tasks:
380
+ continue
381
+
382
+ deps = self.dependencies.get(task_id, set())
383
+ if deps:
384
+ # Get incomplete dependencies
385
+ incomplete_deps = deps - self.completed_tasks
386
+ if incomplete_deps:
387
+ blocked[task_id] = sorted(incomplete_deps)
388
+
389
+ return blocked
390
+
391
+ def _find_cycle_details(self) -> str:
392
+ """
393
+ Find and describe a cycle in the graph (for error messages).
394
+
395
+ Returns:
396
+ String describing the cycle
397
+ """
398
+ visited = set()
399
+ rec_stack = []
400
+
401
+ def find_cycle_from(node: int) -> Optional[str]:
402
+ """DFS to find cycle."""
403
+ visited.add(node)
404
+ rec_stack.append(node)
405
+
406
+ for dep in self.dependencies.get(node, set()):
407
+ if dep not in visited:
408
+ result = find_cycle_from(dep)
409
+ if result:
410
+ return result
411
+ elif dep in rec_stack:
412
+ # Found cycle
413
+ cycle_start = rec_stack.index(dep)
414
+ cycle = rec_stack[cycle_start:] + [dep]
415
+ return " → ".join(map(str, cycle))
416
+
417
+ rec_stack.pop()
418
+ return None
419
+
420
+ for task_id in self.all_tasks:
421
+ if task_id not in visited:
422
+ cycle = find_cycle_from(task_id)
423
+ if cycle:
424
+ return cycle
425
+
426
+ return "Unknown cycle"
427
+
428
+ def clear(self) -> None:
429
+ """Clear all dependency data (for testing/reset)."""
430
+ self.dependencies.clear()
431
+ self.dependents.clear()
432
+ self.completed_tasks.clear()
433
+ self.all_tasks.clear()
434
+
435
+ # ========== Phase 2: Critical Path Analysis ==========
436
+
437
+ def calculate_critical_path(
438
+ self, task_durations: Dict[int, float]
439
+ ) -> CriticalPathResult:
440
+ """
441
+ Calculate the critical path through the task dependency graph.
442
+
443
+ Uses forward and backward passes to compute earliest/latest times
444
+ and identifies tasks with zero slack (critical path).
445
+
446
+ Args:
447
+ task_durations: Dict mapping task_id to duration in hours.
448
+ Missing tasks default to 0 duration.
449
+
450
+ Returns:
451
+ CriticalPathResult with critical task IDs, total duration, and timings
452
+ """
453
+ # Get topological order
454
+ topo_order = self.topological_sort()
455
+ if not topo_order:
456
+ # If cycle detected, return empty result
457
+ return CriticalPathResult(
458
+ critical_task_ids=[],
459
+ total_duration=0.0,
460
+ task_timings={},
461
+ )
462
+
463
+ # Initialize timing data
464
+ task_timings: Dict[int, TaskTiming] = {}
465
+
466
+ # Forward pass: compute earliest start/finish times
467
+ earliest_start: Dict[int, float] = {}
468
+ earliest_finish: Dict[int, float] = {}
469
+
470
+ for task_id in topo_order:
471
+ duration = task_durations.get(task_id, 0.0)
472
+ deps = self.dependencies.get(task_id, set())
473
+
474
+ if not deps:
475
+ # No dependencies - starts at time 0
476
+ earliest_start[task_id] = 0.0
477
+ else:
478
+ # Earliest start is max of all dependency finish times
479
+ earliest_start[task_id] = max(
480
+ earliest_finish.get(dep_id, 0.0) for dep_id in deps
481
+ )
482
+
483
+ earliest_finish[task_id] = earliest_start[task_id] + duration
484
+
485
+ # Project end time is the maximum earliest finish
486
+ project_duration = max(earliest_finish.values()) if earliest_finish else 0.0
487
+
488
+ # Backward pass: compute latest start/finish times
489
+ latest_finish: Dict[int, float] = {}
490
+ latest_start: Dict[int, float] = {}
491
+
492
+ for task_id in reversed(topo_order):
493
+ duration = task_durations.get(task_id, 0.0)
494
+ dependents = self.dependents.get(task_id, set())
495
+
496
+ if not dependents:
497
+ # No dependents - must finish by project end
498
+ latest_finish[task_id] = project_duration
499
+ else:
500
+ # Latest finish is min of all dependent start times
501
+ latest_finish[task_id] = min(
502
+ latest_start.get(dep_id, project_duration) for dep_id in dependents
503
+ )
504
+
505
+ latest_start[task_id] = latest_finish[task_id] - duration
506
+
507
+ # Build timing objects and identify critical path (zero slack)
508
+ critical_task_ids = []
509
+
510
+ for task_id in self.all_tasks:
511
+ timing = TaskTiming(
512
+ earliest_start=earliest_start.get(task_id, 0.0),
513
+ earliest_finish=earliest_finish.get(task_id, 0.0),
514
+ latest_start=latest_start.get(task_id, 0.0),
515
+ latest_finish=latest_finish.get(task_id, 0.0),
516
+ )
517
+ task_timings[task_id] = timing
518
+
519
+ # Task is on critical path if slack is zero
520
+ slack = timing.latest_start - timing.earliest_start
521
+ if abs(slack) < 0.001: # Float comparison tolerance
522
+ critical_task_ids.append(task_id)
523
+
524
+ return CriticalPathResult(
525
+ critical_task_ids=sorted(critical_task_ids),
526
+ total_duration=project_duration,
527
+ task_timings=task_timings,
528
+ )
529
+
530
+ def calculate_task_slack(self, task_durations: Dict[int, float]) -> Dict[int, float]:
531
+ """
532
+ Calculate slack/float time for each task.
533
+
534
+ Slack = Latest Start - Earliest Start
535
+ Tasks with zero slack are on the critical path.
536
+
537
+ Args:
538
+ task_durations: Dict mapping task_id to duration in hours
539
+
540
+ Returns:
541
+ Dict mapping task_id to slack time in hours
542
+ """
543
+ result = self.calculate_critical_path(task_durations)
544
+
545
+ slack = {}
546
+ for task_id, timing in result.task_timings.items():
547
+ slack[task_id] = timing.latest_start - timing.earliest_start
548
+
549
+ return slack
550
+
551
+ def identify_parallel_opportunities(self) -> Dict[int, List[int]]:
552
+ """
553
+ Identify tasks that can execute in parallel (execution waves).
554
+
555
+ Groups tasks by their dependency level - tasks in the same wave
556
+ have no dependencies on each other and can run concurrently.
557
+
558
+ Returns:
559
+ Dict mapping wave number (0, 1, 2...) to list of task IDs
560
+ """
561
+ # Use topological sort with level tracking
562
+ in_degree = {
563
+ task_id: len(self.dependencies.get(task_id, set()))
564
+ for task_id in self.all_tasks
565
+ }
566
+
567
+ # Track which level each task belongs to
568
+ task_level: Dict[int, int] = {}
569
+
570
+ # Start with tasks that have no dependencies (level 0)
571
+ current_level = 0
572
+ queue = deque([task_id for task_id in self.all_tasks if in_degree[task_id] == 0])
573
+
574
+ while queue:
575
+ # Process all tasks at current level
576
+ level_size = len(queue)
577
+
578
+ for _ in range(level_size):
579
+ task_id = queue.popleft()
580
+ task_level[task_id] = current_level
581
+
582
+ # Add dependents to next level if their in-degree becomes 0
583
+ for dependent_id in self.dependents.get(task_id, set()):
584
+ in_degree[dependent_id] -= 1
585
+ if in_degree[dependent_id] == 0:
586
+ queue.append(dependent_id)
587
+
588
+ current_level += 1
589
+
590
+ # Group tasks by level
591
+ waves: Dict[int, List[int]] = defaultdict(list)
592
+ for task_id, level in task_level.items():
593
+ waves[level].append(task_id)
594
+
595
+ # Sort task IDs within each wave
596
+ return {level: sorted(tasks) for level, tasks in waves.items()}
597
+
598
+ # ========== Phase 2: Conflict Detection ==========
599
+
600
+ def detect_dependency_conflicts(
601
+ self, task_durations: Dict[int, float]
602
+ ) -> List[DependencyConflict]:
603
+ """
604
+ Detect dependency conflicts, bottlenecks, and risk patterns.
605
+
606
+ Identifies:
607
+ - Bottleneck tasks (many dependents, high impact on critical path)
608
+ - Long dependency chains (> 5 tasks in sequence)
609
+ - High-risk multipliers (high complexity + many dependents)
610
+
611
+ Args:
612
+ task_durations: Dict mapping task_id to duration in hours
613
+
614
+ Returns:
615
+ List of DependencyConflict objects with severity and recommendations
616
+ """
617
+ conflicts: List[DependencyConflict] = []
618
+
619
+ # Get critical path info
620
+ cp_result = self.calculate_critical_path(task_durations)
621
+ critical_set = set(cp_result.critical_task_ids)
622
+
623
+ # Detect bottleneck tasks (3+ dependents)
624
+ bottleneck_threshold = 3
625
+ for task_id in self.all_tasks:
626
+ dependent_count = len(self.dependents.get(task_id, set()))
627
+
628
+ if dependent_count >= bottleneck_threshold:
629
+ duration = task_durations.get(task_id, 0.0)
630
+ is_critical = task_id in critical_set
631
+
632
+ severity = "critical" if is_critical and duration > 4 else "high" if is_critical else "medium"
633
+
634
+ conflicts.append(
635
+ DependencyConflict(
636
+ task_id=task_id,
637
+ conflict_type="bottleneck",
638
+ severity=severity,
639
+ recommendation=f"Consider splitting task {task_id} into smaller tasks "
640
+ f"to reduce blocking impact on {dependent_count} dependent tasks",
641
+ impact_analysis=f"Task {task_id} blocks {dependent_count} tasks. "
642
+ f"Duration: {duration}h. On critical path: {is_critical}",
643
+ )
644
+ )
645
+
646
+ # Detect long dependency chains (> 5 tasks)
647
+ chain_threshold = 5
648
+ for task_id in self.all_tasks:
649
+ depth = self.get_dependency_depth(task_id)
650
+ if depth >= chain_threshold:
651
+ conflicts.append(
652
+ DependencyConflict(
653
+ task_id=task_id,
654
+ conflict_type="long_chain",
655
+ severity="high" if task_id in critical_set else "medium",
656
+ recommendation=f"Consider parallelizing some tasks in the chain "
657
+ f"leading to task {task_id}",
658
+ impact_analysis=f"Task {task_id} has dependency depth of {depth}, "
659
+ f"creating a long sequential chain",
660
+ )
661
+ )
662
+
663
+ return conflicts
664
+
665
+ def suggest_dependency_resolution(
666
+ self, conflicts: List[DependencyConflict]
667
+ ) -> List[ResolutionSuggestion]:
668
+ """
669
+ Generate resolution suggestions for detected conflicts.
670
+
671
+ Args:
672
+ conflicts: List of detected dependency conflicts
673
+
674
+ Returns:
675
+ List of ResolutionSuggestion objects
676
+ """
677
+ suggestions: List[ResolutionSuggestion] = []
678
+
679
+ for conflict in conflicts:
680
+ if conflict.conflict_type == "bottleneck":
681
+ # Suggest task splitting
682
+ suggestions.append(
683
+ ResolutionSuggestion(
684
+ suggestion_type="split_task",
685
+ description=f"Split task {conflict.task_id} into multiple smaller tasks "
686
+ f"that can be worked on independently",
687
+ affected_task_ids=[conflict.task_id],
688
+ expected_improvement="Reduces blocking time and enables more parallel work",
689
+ )
690
+ )
691
+
692
+ # Suggest prioritization
693
+ suggestions.append(
694
+ ResolutionSuggestion(
695
+ suggestion_type="prioritize",
696
+ description=f"Prioritize task {conflict.task_id} to unblock dependent tasks sooner",
697
+ affected_task_ids=[conflict.task_id],
698
+ expected_improvement="Earlier completion of blocking task reduces overall delay",
699
+ )
700
+ )
701
+
702
+ elif conflict.conflict_type == "long_chain":
703
+ # Suggest dependency reordering
704
+ suggestions.append(
705
+ ResolutionSuggestion(
706
+ suggestion_type="reorder",
707
+ description=f"Review dependencies leading to task {conflict.task_id} "
708
+ f"- some may be removable or parallelizable",
709
+ affected_task_ids=[conflict.task_id],
710
+ expected_improvement="Shorter critical path reduces project duration",
711
+ )
712
+ )
713
+
714
+ return suggestions