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,511 @@
1
+ """Task Scheduler Component for Intelligent Task Scheduling.
2
+
3
+ This module provides scheduling capabilities for task management:
4
+ - schedule_tasks() - Assign start times based on dependencies and resources
5
+ - optimize_schedule() - Use heuristics to minimize project duration
6
+ - predict_completion_date() - Estimate project end date
7
+ - identify_bottlenecks() - Find tasks/agents causing delays
8
+ """
9
+
10
+ import logging
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta
13
+ from typing import Dict, List, Optional, Any
14
+
15
+ from codeframe.agents.dependency_resolver import DependencyResolver
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class TaskAssignment:
22
+ """Assignment of a task to a time slot and agent."""
23
+
24
+ task_id: int
25
+ start_time: float # Hours from project start
26
+ end_time: float # Hours from project start
27
+ assigned_agent: Optional[int] = None
28
+
29
+
30
+ @dataclass
31
+ class TimelineEntry:
32
+ """Entry in the project timeline."""
33
+
34
+ time: float
35
+ event_type: str # "start", "end"
36
+ task_id: int
37
+ agent_id: Optional[int] = None
38
+
39
+
40
+ @dataclass
41
+ class ScheduleResult:
42
+ """Result of task scheduling."""
43
+
44
+ task_assignments: Dict[int, TaskAssignment]
45
+ total_duration: float
46
+ timeline: List[TimelineEntry]
47
+ agents_used: int = 1
48
+
49
+
50
+ @dataclass
51
+ class ScheduleOptimization:
52
+ """Result of schedule optimization."""
53
+
54
+ optimized_schedule: "ScheduleResult"
55
+ improvement_percentage: float
56
+ changes_made: List[str]
57
+ original_duration: float
58
+ optimized_duration: float
59
+
60
+
61
+ @dataclass
62
+ class CompletionPrediction:
63
+ """Prediction for project completion."""
64
+
65
+ predicted_date: datetime
66
+ confidence_interval: Dict[str, datetime]
67
+ remaining_hours: float
68
+ completed_percentage: float
69
+
70
+
71
+ @dataclass
72
+ class BottleneckInfo:
73
+ """Information about a scheduling bottleneck."""
74
+
75
+ task_id: int
76
+ bottleneck_type: str # "duration", "dependencies", "resource"
77
+ impact_hours: float
78
+ recommendation: str
79
+
80
+
81
+ class TaskScheduler:
82
+ """
83
+ Intelligent task scheduler using Critical Path Method (CPM).
84
+
85
+ Capabilities:
86
+ - Schedule tasks based on dependencies and resource constraints
87
+ - Optimize schedules to minimize project duration
88
+ - Predict completion dates with confidence intervals
89
+ - Identify scheduling bottlenecks
90
+ """
91
+
92
+ def __init__(self):
93
+ """Initialize the task scheduler."""
94
+ # Default uncertainty factor for predictions (±20%)
95
+ self.uncertainty_factor = 0.2
96
+
97
+ def schedule_tasks(
98
+ self,
99
+ tasks: List[Any],
100
+ task_durations: Dict[int, float],
101
+ resolver: DependencyResolver,
102
+ agents_available: int = 1,
103
+ ) -> ScheduleResult:
104
+ """
105
+ Schedule tasks based on dependencies and available resources.
106
+
107
+ Uses Critical Path Method to assign start/end times while
108
+ respecting dependency constraints and agent availability.
109
+
110
+ Args:
111
+ tasks: List of Task objects
112
+ task_durations: Dict mapping task_id to duration in hours
113
+ resolver: DependencyResolver with built dependency graph
114
+ agents_available: Number of parallel agents/workers
115
+
116
+ Returns:
117
+ ScheduleResult with task assignments and timeline
118
+ """
119
+ # Get parallel execution waves
120
+ waves = resolver.identify_parallel_opportunities()
121
+
122
+ # Build assignments
123
+ task_assignments: Dict[int, TaskAssignment] = {}
124
+ timeline: List[TimelineEntry] = []
125
+
126
+ # Track agent availability
127
+ agent_end_times = [0.0] * agents_available
128
+
129
+ # Process tasks wave by wave
130
+ for wave_num in sorted(waves.keys()):
131
+ wave_tasks = waves[wave_num]
132
+
133
+ # Sort by duration (longer tasks first for better packing)
134
+ wave_tasks_sorted = sorted(
135
+ wave_tasks,
136
+ key=lambda tid: task_durations.get(tid, 0),
137
+ reverse=True,
138
+ )
139
+
140
+ for task_id in wave_tasks_sorted:
141
+ duration = task_durations.get(task_id, 0.0)
142
+
143
+ # Find earliest start time based on dependencies
144
+ deps = resolver.dependencies.get(task_id, set())
145
+ earliest_start = 0.0
146
+ if deps:
147
+ for dep_id in deps:
148
+ if dep_id in task_assignments:
149
+ earliest_start = max(
150
+ earliest_start,
151
+ task_assignments[dep_id].end_time,
152
+ )
153
+
154
+ # Find the first available agent
155
+ best_agent = 0
156
+ best_start = max(earliest_start, agent_end_times[0])
157
+
158
+ for agent_idx, agent_end in enumerate(agent_end_times):
159
+ potential_start = max(earliest_start, agent_end)
160
+ if potential_start < best_start:
161
+ best_start = potential_start
162
+ best_agent = agent_idx
163
+
164
+ # Create assignment
165
+ end_time = best_start + duration
166
+ assignment = TaskAssignment(
167
+ task_id=task_id,
168
+ start_time=best_start,
169
+ end_time=end_time,
170
+ assigned_agent=best_agent,
171
+ )
172
+ task_assignments[task_id] = assignment
173
+
174
+ # Update agent availability
175
+ agent_end_times[best_agent] = end_time
176
+
177
+ # Add timeline entries
178
+ timeline.append(
179
+ TimelineEntry(
180
+ time=best_start,
181
+ event_type="start",
182
+ task_id=task_id,
183
+ agent_id=best_agent,
184
+ )
185
+ )
186
+ timeline.append(
187
+ TimelineEntry(
188
+ time=end_time,
189
+ event_type="end",
190
+ task_id=task_id,
191
+ agent_id=best_agent,
192
+ )
193
+ )
194
+
195
+ # Sort timeline by time
196
+ timeline.sort(key=lambda e: (e.time, 0 if e.event_type == "start" else 1))
197
+
198
+ # Warn if any tasks were not scheduled (e.g., due to unknown dependency IDs)
199
+ expected_task_ids = set(task_durations.keys())
200
+ scheduled_task_ids = set(task_assignments.keys())
201
+ missing_task_ids = expected_task_ids - scheduled_task_ids
202
+ if missing_task_ids:
203
+ logger.warning(
204
+ f"Tasks omitted from schedule (possibly due to unknown dependencies): "
205
+ f"{sorted(missing_task_ids)}"
206
+ )
207
+
208
+ # Calculate total duration
209
+ total_duration = max(
210
+ (a.end_time for a in task_assignments.values()),
211
+ default=0.0,
212
+ )
213
+
214
+ return ScheduleResult(
215
+ task_assignments=task_assignments,
216
+ total_duration=total_duration,
217
+ timeline=timeline,
218
+ agents_used=agents_available,
219
+ )
220
+
221
+ def optimize_schedule(
222
+ self,
223
+ schedule: ScheduleResult,
224
+ resolver: DependencyResolver,
225
+ constraints: Optional[Dict[str, Any]] = None,
226
+ ) -> ScheduleOptimization:
227
+ """
228
+ Optimize a schedule to minimize project duration.
229
+
230
+ Uses heuristics to identify parallelization opportunities
231
+ and suggest task reordering within slack windows.
232
+
233
+ Args:
234
+ schedule: Current ScheduleResult to optimize
235
+ resolver: DependencyResolver with dependency graph
236
+ constraints: Optional constraints (max_parallel, etc.)
237
+
238
+ Returns:
239
+ ScheduleOptimization with optimized schedule and improvements
240
+ """
241
+ constraints = constraints or {}
242
+ max_parallel = constraints.get("max_parallel", schedule.agents_used)
243
+
244
+ changes_made: List[str] = []
245
+ original_duration = schedule.total_duration
246
+
247
+ # If we can use more agents than currently scheduled, re-schedule
248
+ if max_parallel > schedule.agents_used:
249
+ # Extract task durations from current assignments
250
+ task_durations = {
251
+ tid: assignment.end_time - assignment.start_time
252
+ for tid, assignment in schedule.task_assignments.items()
253
+ }
254
+
255
+ # Get task list from assignments
256
+ task_ids = list(schedule.task_assignments.keys())
257
+
258
+ # Re-schedule with more agents
259
+ optimized = self._reschedule_with_agents(
260
+ task_ids=task_ids,
261
+ task_durations=task_durations,
262
+ resolver=resolver,
263
+ agents_available=max_parallel,
264
+ )
265
+
266
+ if optimized.total_duration < original_duration:
267
+ changes_made.append(
268
+ f"Increased parallelization from {schedule.agents_used} "
269
+ f"to {max_parallel} agents"
270
+ )
271
+
272
+ improvement = (
273
+ (original_duration - optimized.total_duration) / original_duration * 100
274
+ if original_duration > 0
275
+ else 0
276
+ )
277
+
278
+ return ScheduleOptimization(
279
+ optimized_schedule=optimized,
280
+ improvement_percentage=improvement,
281
+ changes_made=changes_made,
282
+ original_duration=original_duration,
283
+ optimized_duration=optimized.total_duration,
284
+ )
285
+
286
+ # No optimization possible
287
+ return ScheduleOptimization(
288
+ optimized_schedule=schedule,
289
+ improvement_percentage=0.0,
290
+ changes_made=["No optimizations found"],
291
+ original_duration=original_duration,
292
+ optimized_duration=original_duration,
293
+ )
294
+
295
+ def _reschedule_with_agents(
296
+ self,
297
+ task_ids: List[int],
298
+ task_durations: Dict[int, float],
299
+ resolver: DependencyResolver,
300
+ agents_available: int,
301
+ ) -> ScheduleResult:
302
+ """Re-schedule tasks with a different number of agents."""
303
+ # Get parallel waves
304
+ waves = resolver.identify_parallel_opportunities()
305
+
306
+ task_assignments: Dict[int, TaskAssignment] = {}
307
+ timeline: List[TimelineEntry] = []
308
+ agent_end_times = [0.0] * agents_available
309
+
310
+ for wave_num in sorted(waves.keys()):
311
+ wave_tasks = [tid for tid in waves[wave_num] if tid in task_ids]
312
+ wave_tasks_sorted = sorted(
313
+ wave_tasks,
314
+ key=lambda tid: task_durations.get(tid, 0),
315
+ reverse=True,
316
+ )
317
+
318
+ for task_id in wave_tasks_sorted:
319
+ duration = task_durations.get(task_id, 0.0)
320
+
321
+ # Find earliest start based on dependencies
322
+ deps = resolver.dependencies.get(task_id, set())
323
+ earliest_start = 0.0
324
+ for dep_id in deps:
325
+ if dep_id in task_assignments:
326
+ earliest_start = max(
327
+ earliest_start,
328
+ task_assignments[dep_id].end_time,
329
+ )
330
+
331
+ # Find best agent
332
+ best_agent = 0
333
+ best_start = max(earliest_start, agent_end_times[0])
334
+
335
+ for agent_idx, agent_end in enumerate(agent_end_times):
336
+ potential_start = max(earliest_start, agent_end)
337
+ if potential_start < best_start:
338
+ best_start = potential_start
339
+ best_agent = agent_idx
340
+
341
+ end_time = best_start + duration
342
+ task_assignments[task_id] = TaskAssignment(
343
+ task_id=task_id,
344
+ start_time=best_start,
345
+ end_time=end_time,
346
+ assigned_agent=best_agent,
347
+ )
348
+ agent_end_times[best_agent] = end_time
349
+
350
+ timeline.append(
351
+ TimelineEntry(
352
+ time=best_start,
353
+ event_type="start",
354
+ task_id=task_id,
355
+ agent_id=best_agent,
356
+ )
357
+ )
358
+ timeline.append(
359
+ TimelineEntry(
360
+ time=end_time,
361
+ event_type="end",
362
+ task_id=task_id,
363
+ agent_id=best_agent,
364
+ )
365
+ )
366
+
367
+ timeline.sort(key=lambda e: e.time)
368
+ total_duration = max(
369
+ (a.end_time for a in task_assignments.values()), default=0.0
370
+ )
371
+
372
+ return ScheduleResult(
373
+ task_assignments=task_assignments,
374
+ total_duration=total_duration,
375
+ timeline=timeline,
376
+ agents_used=agents_available,
377
+ )
378
+
379
+ def predict_completion_date(
380
+ self,
381
+ schedule: ScheduleResult,
382
+ current_progress: Dict[int, str],
383
+ start_date: datetime,
384
+ hours_per_day: float = 8.0,
385
+ ) -> CompletionPrediction:
386
+ """
387
+ Predict project completion date based on schedule and progress.
388
+
389
+ Args:
390
+ schedule: Current ScheduleResult
391
+ current_progress: Dict mapping task_id to status ("completed", "in_progress")
392
+ start_date: Project start date
393
+ hours_per_day: Working hours per day (default 8)
394
+
395
+ Returns:
396
+ CompletionPrediction with predicted date and confidence interval
397
+ """
398
+ # Calculate remaining hours
399
+ total_hours = schedule.total_duration
400
+ completed_hours = 0.0
401
+
402
+ for task_id, status in current_progress.items():
403
+ if status == "completed" and task_id in schedule.task_assignments:
404
+ assignment = schedule.task_assignments[task_id]
405
+ completed_hours += assignment.end_time - assignment.start_time
406
+
407
+ remaining_hours = max(0, total_hours - completed_hours)
408
+ completed_percentage = (
409
+ (completed_hours / total_hours * 100) if total_hours > 0 else 100
410
+ )
411
+
412
+ # Calculate working days needed
413
+ days_needed = remaining_hours / hours_per_day
414
+
415
+ # Apply uncertainty for confidence interval
416
+ early_days = days_needed * (1 - self.uncertainty_factor)
417
+ late_days = days_needed * (1 + self.uncertainty_factor)
418
+
419
+ predicted_date = start_date + timedelta(days=days_needed)
420
+ early_date = start_date + timedelta(days=early_days)
421
+ late_date = start_date + timedelta(days=late_days)
422
+
423
+ return CompletionPrediction(
424
+ predicted_date=predicted_date,
425
+ confidence_interval={
426
+ "early": early_date,
427
+ "late": late_date,
428
+ },
429
+ remaining_hours=remaining_hours,
430
+ completed_percentage=completed_percentage,
431
+ )
432
+
433
+ def identify_bottlenecks(
434
+ self,
435
+ schedule: ScheduleResult,
436
+ task_durations: Dict[int, float],
437
+ resolver: DependencyResolver,
438
+ ) -> List[BottleneckInfo]:
439
+ """
440
+ Identify scheduling bottlenecks in the project.
441
+
442
+ Identifies:
443
+ - Long duration tasks on critical path
444
+ - Tasks with many dependents causing delays
445
+ - Resource constraints limiting parallelization
446
+
447
+ Args:
448
+ schedule: Current ScheduleResult
449
+ task_durations: Dict mapping task_id to duration
450
+ resolver: DependencyResolver with dependency graph
451
+
452
+ Returns:
453
+ List of BottleneckInfo objects
454
+ """
455
+ bottlenecks: List[BottleneckInfo] = []
456
+
457
+ # Get critical path
458
+ cp_result = resolver.calculate_critical_path(task_durations)
459
+ critical_set = set(cp_result.critical_task_ids)
460
+
461
+ # Identify long duration tasks on critical path
462
+ avg_duration = (
463
+ sum(task_durations.values()) / len(task_durations)
464
+ if task_durations
465
+ else 0
466
+ )
467
+ duration_threshold = avg_duration * 2 # Tasks > 2x average
468
+
469
+ for task_id in critical_set:
470
+ duration = task_durations.get(task_id, 0)
471
+
472
+ if duration > duration_threshold:
473
+ dependent_count = len(resolver.dependents.get(task_id, set()))
474
+ impact = duration - avg_duration
475
+
476
+ bottlenecks.append(
477
+ BottleneckInfo(
478
+ task_id=task_id,
479
+ bottleneck_type="duration",
480
+ impact_hours=impact,
481
+ recommendation=(
482
+ f"Task {task_id} takes {duration:.1f}h (2x average). "
483
+ f"Consider splitting into smaller tasks. "
484
+ f"Blocks {dependent_count} downstream tasks."
485
+ ),
486
+ )
487
+ )
488
+
489
+ # Identify tasks with many dependents
490
+ dependent_threshold = 3
491
+ for task_id in resolver.all_tasks:
492
+ dependent_count = len(resolver.dependents.get(task_id, set()))
493
+
494
+ if dependent_count >= dependent_threshold and task_id in critical_set:
495
+ duration = task_durations.get(task_id, 0)
496
+ # Impact is duration times number of blocked tasks
497
+ impact = duration * (dependent_count - 1)
498
+
499
+ bottlenecks.append(
500
+ BottleneckInfo(
501
+ task_id=task_id,
502
+ bottleneck_type="dependencies",
503
+ impact_hours=impact,
504
+ recommendation=(
505
+ f"Task {task_id} blocks {dependent_count} tasks. "
506
+ f"Prioritize this task or consider parallelizing dependents."
507
+ ),
508
+ )
509
+ )
510
+
511
+ return bottlenecks