roadmap-cli 0.1.1__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 (521) hide show
  1. roadmap/__init__.py +27 -0
  2. roadmap/adapters/__init__.py +1 -0
  3. roadmap/adapters/base_paginated_adapter.py +117 -0
  4. roadmap/adapters/cli/__init__.py +249 -0
  5. roadmap/adapters/cli/analysis/__init__.py +5 -0
  6. roadmap/adapters/cli/analysis/commands.py +370 -0
  7. roadmap/adapters/cli/analysis/presenter.py +125 -0
  8. roadmap/adapters/cli/archive_operations.py +86 -0
  9. roadmap/adapters/cli/cli_command_helpers.py +109 -0
  10. roadmap/adapters/cli/cli_confirmations.py +74 -0
  11. roadmap/adapters/cli/cli_error_handlers.py +146 -0
  12. roadmap/adapters/cli/cli_validators.py +39 -0
  13. roadmap/adapters/cli/commands/sync_status.py +308 -0
  14. roadmap/adapters/cli/comment/__init__.py +5 -0
  15. roadmap/adapters/cli/comment/commands.py +106 -0
  16. roadmap/adapters/cli/config/__init__.py +5 -0
  17. roadmap/adapters/cli/config/commands.py +198 -0
  18. roadmap/adapters/cli/console_exports.py +10 -0
  19. roadmap/adapters/cli/core.py +26 -0
  20. roadmap/adapters/cli/crud/__init__.py +47 -0
  21. roadmap/adapters/cli/crud/base_archive.py +208 -0
  22. roadmap/adapters/cli/crud/base_create.py +154 -0
  23. roadmap/adapters/cli/crud/base_delete.py +155 -0
  24. roadmap/adapters/cli/crud/base_restore.py +283 -0
  25. roadmap/adapters/cli/crud/base_update.py +193 -0
  26. roadmap/adapters/cli/crud/crud_helpers.py +206 -0
  27. roadmap/adapters/cli/crud/crud_utils.py +152 -0
  28. roadmap/adapters/cli/crud/entity_builders.py +374 -0
  29. roadmap/adapters/cli/data/__init__.py +8 -0
  30. roadmap/adapters/cli/data/commands.py +149 -0
  31. roadmap/adapters/cli/decorators.py +208 -0
  32. roadmap/adapters/cli/dtos/__init__.py +155 -0
  33. roadmap/adapters/cli/exception_handler.py +105 -0
  34. roadmap/adapters/cli/git/__init__.py +8 -0
  35. roadmap/adapters/cli/git/commands.py +475 -0
  36. roadmap/adapters/cli/git/handlers/__init__.py +1 -0
  37. roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
  38. roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
  39. roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
  40. roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
  41. roadmap/adapters/cli/git/hooks_config.py +327 -0
  42. roadmap/adapters/cli/git/status_display.py +158 -0
  43. roadmap/adapters/cli/health/__init__.py +8 -0
  44. roadmap/adapters/cli/health/db_integrity.py +204 -0
  45. roadmap/adapters/cli/health/enhancer.py +227 -0
  46. roadmap/adapters/cli/health/fixer.py +295 -0
  47. roadmap/adapters/cli/health/fixers/__init__.py +10 -0
  48. roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
  49. roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
  50. roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
  51. roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
  52. roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
  53. roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
  54. roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
  55. roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
  56. roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
  57. roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
  58. roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
  59. roadmap/adapters/cli/health/formatter.py +250 -0
  60. roadmap/adapters/cli/health/formatters.py +427 -0
  61. roadmap/adapters/cli/health/scan.py +293 -0
  62. roadmap/adapters/cli/init/__init__.py +5 -0
  63. roadmap/adapters/cli/init/commands.py +587 -0
  64. roadmap/adapters/cli/init.py +9 -0
  65. roadmap/adapters/cli/issues/__init__.py +70 -0
  66. roadmap/adapters/cli/issues/archive.py +137 -0
  67. roadmap/adapters/cli/issues/archive_class.py +253 -0
  68. roadmap/adapters/cli/issues/block.py +39 -0
  69. roadmap/adapters/cli/issues/close.py +216 -0
  70. roadmap/adapters/cli/issues/comment.py +178 -0
  71. roadmap/adapters/cli/issues/create.py +162 -0
  72. roadmap/adapters/cli/issues/delete.py +31 -0
  73. roadmap/adapters/cli/issues/deps.py +161 -0
  74. roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
  75. roadmap/adapters/cli/issues/link.py +308 -0
  76. roadmap/adapters/cli/issues/list.py +402 -0
  77. roadmap/adapters/cli/issues/lookup.py +96 -0
  78. roadmap/adapters/cli/issues/progress.py +102 -0
  79. roadmap/adapters/cli/issues/restore.py +96 -0
  80. roadmap/adapters/cli/issues/restore_class.py +57 -0
  81. roadmap/adapters/cli/issues/start.py +165 -0
  82. roadmap/adapters/cli/issues/sync_status.py +374 -0
  83. roadmap/adapters/cli/issues/unblock.py +43 -0
  84. roadmap/adapters/cli/issues/unlink.py +104 -0
  85. roadmap/adapters/cli/issues/update.py +120 -0
  86. roadmap/adapters/cli/issues/view.py +36 -0
  87. roadmap/adapters/cli/layout.py +220 -0
  88. roadmap/adapters/cli/mappers.py +200 -0
  89. roadmap/adapters/cli/milestones/__init__.py +52 -0
  90. roadmap/adapters/cli/milestones/archive.py +125 -0
  91. roadmap/adapters/cli/milestones/archive_class.py +236 -0
  92. roadmap/adapters/cli/milestones/assign.py +62 -0
  93. roadmap/adapters/cli/milestones/close.py +177 -0
  94. roadmap/adapters/cli/milestones/create.py +131 -0
  95. roadmap/adapters/cli/milestones/delete.py +27 -0
  96. roadmap/adapters/cli/milestones/kanban.py +123 -0
  97. roadmap/adapters/cli/milestones/list.py +87 -0
  98. roadmap/adapters/cli/milestones/recalculate.py +79 -0
  99. roadmap/adapters/cli/milestones/restore.py +87 -0
  100. roadmap/adapters/cli/milestones/restore_class.py +99 -0
  101. roadmap/adapters/cli/milestones/update.py +61 -0
  102. roadmap/adapters/cli/milestones/view.py +142 -0
  103. roadmap/adapters/cli/output_manager.py +319 -0
  104. roadmap/adapters/cli/presentation/base_presenter.py +121 -0
  105. roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
  106. roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
  107. roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
  108. roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
  109. roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
  110. roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
  111. roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
  112. roadmap/adapters/cli/presentation/project_presenter.py +233 -0
  113. roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
  114. roadmap/adapters/cli/presentation/table_builders.py +95 -0
  115. roadmap/adapters/cli/projects/__init__.py +42 -0
  116. roadmap/adapters/cli/projects/archive.py +127 -0
  117. roadmap/adapters/cli/projects/archive_class.py +96 -0
  118. roadmap/adapters/cli/projects/close.py +94 -0
  119. roadmap/adapters/cli/projects/create.py +50 -0
  120. roadmap/adapters/cli/projects/delete.py +27 -0
  121. roadmap/adapters/cli/projects/list.py +227 -0
  122. roadmap/adapters/cli/projects/restore.py +87 -0
  123. roadmap/adapters/cli/projects/restore_class.py +56 -0
  124. roadmap/adapters/cli/projects/update.py +57 -0
  125. roadmap/adapters/cli/projects/view.py +157 -0
  126. roadmap/adapters/cli/services/__init__.py +1 -0
  127. roadmap/adapters/cli/services/daily_summary_service.py +313 -0
  128. roadmap/adapters/cli/services/export_manager.py +268 -0
  129. roadmap/adapters/cli/services/milestone_list_service.py +266 -0
  130. roadmap/adapters/cli/services/project_initialization_service.py +29 -0
  131. roadmap/adapters/cli/services/project_status_service.py +462 -0
  132. roadmap/adapters/cli/services/sync_service.py +62 -0
  133. roadmap/adapters/cli/status.py +408 -0
  134. roadmap/adapters/cli/styling.py +21 -0
  135. roadmap/adapters/cli/sync.py +1117 -0
  136. roadmap/adapters/cli/sync_context.py +432 -0
  137. roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
  138. roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
  139. roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
  140. roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
  141. roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
  142. roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
  143. roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
  144. roadmap/adapters/cli/sync_metrics_command.py +190 -0
  145. roadmap/adapters/cli/sync_presenter.py +58 -0
  146. roadmap/adapters/cli/sync_validation.py +295 -0
  147. roadmap/adapters/cli/today.py +52 -0
  148. roadmap/adapters/cli/utils/click_options.py +38 -0
  149. roadmap/adapters/git/__init__.py +1 -0
  150. roadmap/adapters/git/git.py +208 -0
  151. roadmap/adapters/git/git_branch_manager.py +400 -0
  152. roadmap/adapters/git/git_command_executor.py +56 -0
  153. roadmap/adapters/git/git_commit_analyzer.py +334 -0
  154. roadmap/adapters/git/git_hooks.py +22 -0
  155. roadmap/adapters/git/git_hooks_manager.py +455 -0
  156. roadmap/adapters/git/git_repository_info.py +65 -0
  157. roadmap/adapters/git/hook_installer.py +106 -0
  158. roadmap/adapters/git/hook_registry.py +97 -0
  159. roadmap/adapters/git/hook_script_generator.py +58 -0
  160. roadmap/adapters/git/sync_monitor.py +409 -0
  161. roadmap/adapters/git/workflow_automation.py +286 -0
  162. roadmap/adapters/github/__init__.py +1 -0
  163. roadmap/adapters/github/github.py +360 -0
  164. roadmap/adapters/github/handlers/__init__.py +17 -0
  165. roadmap/adapters/github/handlers/base.py +337 -0
  166. roadmap/adapters/github/handlers/collaborators.py +165 -0
  167. roadmap/adapters/github/handlers/comments.py +118 -0
  168. roadmap/adapters/github/handlers/issues.py +150 -0
  169. roadmap/adapters/github/handlers/labels.py +164 -0
  170. roadmap/adapters/github/handlers/milestones.py +104 -0
  171. roadmap/adapters/persistence/__init__.py +35 -0
  172. roadmap/adapters/persistence/conflict_resolver.py +190 -0
  173. roadmap/adapters/persistence/database_manager.py +492 -0
  174. roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
  175. roadmap/adapters/persistence/file_locking.py +348 -0
  176. roadmap/adapters/persistence/file_parser.py +92 -0
  177. roadmap/adapters/persistence/file_synchronizer.py +107 -0
  178. roadmap/adapters/persistence/focused_managers.py +140 -0
  179. roadmap/adapters/persistence/git_history.py +330 -0
  180. roadmap/adapters/persistence/parser/__init__.py +20 -0
  181. roadmap/adapters/persistence/parser/frontmatter.py +152 -0
  182. roadmap/adapters/persistence/parser/issue.py +245 -0
  183. roadmap/adapters/persistence/parser/milestone.py +146 -0
  184. roadmap/adapters/persistence/parser/project.py +106 -0
  185. roadmap/adapters/persistence/persistence.py +288 -0
  186. roadmap/adapters/persistence/repositories/__init__.py +15 -0
  187. roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
  188. roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
  189. roadmap/adapters/persistence/repositories/project_repository.py +168 -0
  190. roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
  191. roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
  192. roadmap/adapters/persistence/storage/__init__.py +36 -0
  193. roadmap/adapters/persistence/storage/conflicts.py +101 -0
  194. roadmap/adapters/persistence/storage/connection_manager.py +79 -0
  195. roadmap/adapters/persistence/storage/issue_storage.py +89 -0
  196. roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
  197. roadmap/adapters/persistence/storage/project_storage.py +102 -0
  198. roadmap/adapters/persistence/storage/queries.py +227 -0
  199. roadmap/adapters/persistence/storage/state_manager.py +652 -0
  200. roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
  201. roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
  202. roadmap/adapters/persistence/sync_orchestrator.py +293 -0
  203. roadmap/adapters/persistence/sync_state_tracker.py +86 -0
  204. roadmap/adapters/persistence/yaml_repositories.py +711 -0
  205. roadmap/adapters/sync/__init__.py +21 -0
  206. roadmap/adapters/sync/backend_factory.py +108 -0
  207. roadmap/adapters/sync/backends/__init__.py +15 -0
  208. roadmap/adapters/sync/backends/converters.py +209 -0
  209. roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
  210. roadmap/adapters/sync/backends/github_client.py +99 -0
  211. roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
  212. roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
  213. roadmap/adapters/sync/backends/services/__init__.py +1 -0
  214. roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
  215. roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
  216. roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
  217. roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
  218. roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
  219. roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
  220. roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
  221. roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
  222. roadmap/adapters/sync/services/__init__.py +11 -0
  223. roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
  224. roadmap/adapters/sync/services/conflict_converter.py +85 -0
  225. roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
  226. roadmap/adapters/sync/services/issue_state_service.py +243 -0
  227. roadmap/adapters/sync/services/local_change_filter.py +129 -0
  228. roadmap/adapters/sync/services/pull_result_processor.py +71 -0
  229. roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
  230. roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
  231. roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
  232. roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
  233. roadmap/adapters/sync/services/sync_linking_service.py +329 -0
  234. roadmap/adapters/sync/services/sync_report_service.py +62 -0
  235. roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
  236. roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
  237. roadmap/adapters/sync/sync_merge_engine.py +416 -0
  238. roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
  239. roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
  240. roadmap/application/services/__init__.py +1 -0
  241. roadmap/application/services/deduplicate_service.py +517 -0
  242. roadmap/application/use_cases/__init__.py +1 -0
  243. roadmap/common/__init__.py +308 -0
  244. roadmap/common/cache.py +209 -0
  245. roadmap/common/cli_errors.py +147 -0
  246. roadmap/common/configuration/__init__.py +33 -0
  247. roadmap/common/configuration/config_loader.py +264 -0
  248. roadmap/common/configuration/config_manager.py +164 -0
  249. roadmap/common/configuration/config_schema.py +145 -0
  250. roadmap/common/configuration/github/__init__.py +9 -0
  251. roadmap/common/configuration/github/config_manager.py +154 -0
  252. roadmap/common/configuration/github/token_resolver.py +84 -0
  253. roadmap/common/console.py +184 -0
  254. roadmap/common/constants.py +134 -0
  255. roadmap/common/datetime_parser.py +319 -0
  256. roadmap/common/error_formatter.py +84 -0
  257. roadmap/common/errors/__init__.py +143 -0
  258. roadmap/common/errors/error_base.py +73 -0
  259. roadmap/common/errors/error_file.py +82 -0
  260. roadmap/common/errors/error_git.py +67 -0
  261. roadmap/common/errors/error_handler.py +166 -0
  262. roadmap/common/errors/error_network.py +90 -0
  263. roadmap/common/errors/error_security.py +78 -0
  264. roadmap/common/errors/error_standards.py +561 -0
  265. roadmap/common/errors/error_validation.py +111 -0
  266. roadmap/common/errors/exceptions.py +432 -0
  267. roadmap/common/formatters/__init__.py +29 -0
  268. roadmap/common/formatters/base_table_formatter.py +96 -0
  269. roadmap/common/formatters/export/__init__.py +7 -0
  270. roadmap/common/formatters/export/issue_exporter.py +97 -0
  271. roadmap/common/formatters/helpers.py +28 -0
  272. roadmap/common/formatters/kanban/__init__.py +9 -0
  273. roadmap/common/formatters/kanban/layout.py +48 -0
  274. roadmap/common/formatters/kanban/organizer.py +81 -0
  275. roadmap/common/formatters/output/__init__.py +15 -0
  276. roadmap/common/formatters/tables/__init__.py +11 -0
  277. roadmap/common/formatters/tables/column_factory.py +312 -0
  278. roadmap/common/formatters/tables/issue_table.py +276 -0
  279. roadmap/common/formatters/tables/milestone_table.py +247 -0
  280. roadmap/common/formatters/tables/project_table.py +195 -0
  281. roadmap/common/formatters/text/__init__.py +57 -0
  282. roadmap/common/formatters/text/basic.py +217 -0
  283. roadmap/common/formatters/text/display.py +42 -0
  284. roadmap/common/formatters/text/duration.py +36 -0
  285. roadmap/common/formatters/text/operations.py +325 -0
  286. roadmap/common/formatters/text/status_badges.py +57 -0
  287. roadmap/common/initialization/github/__init__.py +11 -0
  288. roadmap/common/initialization/github/setup_service.py +238 -0
  289. roadmap/common/initialization/github/setup_validator.py +78 -0
  290. roadmap/common/logging/__init__.py +113 -0
  291. roadmap/common/logging/audit_logging.py +5 -0
  292. roadmap/common/logging/correlation.py +45 -0
  293. roadmap/common/logging/decorators.py +267 -0
  294. roadmap/common/logging/error_logging.py +217 -0
  295. roadmap/common/logging/formatters.py +124 -0
  296. roadmap/common/logging/loggers.py +68 -0
  297. roadmap/common/logging/performance_tracking.py +267 -0
  298. roadmap/common/logging/utils.py +335 -0
  299. roadmap/common/models/__init__.py +44 -0
  300. roadmap/common/models/cli_models.py +118 -0
  301. roadmap/common/models/config_models.py +199 -0
  302. roadmap/common/models/output_models.py +381 -0
  303. roadmap/common/observability/__init__.py +28 -0
  304. roadmap/common/observability/instrumentation.py +103 -0
  305. roadmap/common/observability/observability.py +76 -0
  306. roadmap/common/observability/otel_init.py +109 -0
  307. roadmap/common/output_formatter.py +393 -0
  308. roadmap/common/progress.py +379 -0
  309. roadmap/common/result.py +396 -0
  310. roadmap/common/security/__init__.py +48 -0
  311. roadmap/common/security/exceptions.py +13 -0
  312. roadmap/common/security/export_cleanup.py +79 -0
  313. roadmap/common/security/file_operations.py +104 -0
  314. roadmap/common/security/filename_sanitization.py +54 -0
  315. roadmap/common/security/logging.py +27 -0
  316. roadmap/common/security/path_validation.py +137 -0
  317. roadmap/common/security/temp_files.py +41 -0
  318. roadmap/common/services/__init__.py +65 -0
  319. roadmap/common/services/decorators.py +183 -0
  320. roadmap/common/services/logging_utils.py +208 -0
  321. roadmap/common/services/metrics.py +161 -0
  322. roadmap/common/services/performance.py +219 -0
  323. roadmap/common/services/profiling.py +288 -0
  324. roadmap/common/services/retry.py +288 -0
  325. roadmap/common/status_style_manager.py +114 -0
  326. roadmap/common/union_find.py +88 -0
  327. roadmap/common/update_constants.py +13 -0
  328. roadmap/common/utils/__init__.py +65 -0
  329. roadmap/common/utils/cli_helpers.py +381 -0
  330. roadmap/common/utils/file_utils.py +440 -0
  331. roadmap/common/utils/path_utils.py +38 -0
  332. roadmap/common/utils/status_utils.py +130 -0
  333. roadmap/common/utils/timezone_utils.py +480 -0
  334. roadmap/common/validation/__init__.py +37 -0
  335. roadmap/common/validation/field_validator.py +104 -0
  336. roadmap/common/validation/result.py +37 -0
  337. roadmap/common/validation/roadmap_validator.py +310 -0
  338. roadmap/common/validation/schema_validator.py +36 -0
  339. roadmap/common/validation/validators.py +50 -0
  340. roadmap/core/__init__.py +1 -0
  341. roadmap/core/domain/__init__.py +30 -0
  342. roadmap/core/domain/comment.py +24 -0
  343. roadmap/core/domain/health.py +15 -0
  344. roadmap/core/domain/issue.py +274 -0
  345. roadmap/core/domain/milestone.py +173 -0
  346. roadmap/core/domain/ports/__init__.py +1 -0
  347. roadmap/core/domain/ports/baseline_repository.py +24 -0
  348. roadmap/core/domain/ports/issue_repository.py +34 -0
  349. roadmap/core/domain/ports/remote_backend_port.py +31 -0
  350. roadmap/core/domain/project.py +130 -0
  351. roadmap/core/interfaces/__init__.py +109 -0
  352. roadmap/core/interfaces/assignee_validator.py +40 -0
  353. roadmap/core/interfaces/backend_factory.py +175 -0
  354. roadmap/core/interfaces/github.py +102 -0
  355. roadmap/core/interfaces/parsers.py +140 -0
  356. roadmap/core/interfaces/persistence.py +45 -0
  357. roadmap/core/interfaces/repositories.py +174 -0
  358. roadmap/core/interfaces/state_managers.py +111 -0
  359. roadmap/core/interfaces/state_storage.py +64 -0
  360. roadmap/core/interfaces/sync_backend.py +275 -0
  361. roadmap/core/interfaces/sync_services.py +119 -0
  362. roadmap/core/interfaces/sync_validators.py +180 -0
  363. roadmap/core/models/__init__.py +31 -0
  364. roadmap/core/models/sync_models.py +162 -0
  365. roadmap/core/models.py +115 -0
  366. roadmap/core/observability/__init__.py +17 -0
  367. roadmap/core/observability/sync_metrics.py +699 -0
  368. roadmap/core/repositories.py +200 -0
  369. roadmap/core/services/__init__.py +313 -0
  370. roadmap/core/services/baseline/__init__.py +1 -0
  371. roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
  372. roadmap/core/services/baseline/baseline_retriever.py +63 -0
  373. roadmap/core/services/baseline/baseline_selector.py +248 -0
  374. roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
  375. roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
  376. roadmap/core/services/comment/__init__.py +1 -0
  377. roadmap/core/services/comment/comment_service.py +277 -0
  378. roadmap/core/services/git/__init__.py +1 -0
  379. roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
  380. roadmap/core/services/github/__init__.py +1 -0
  381. roadmap/core/services/github/github_change_detector.py +162 -0
  382. roadmap/core/services/github/github_config_validator.py +148 -0
  383. roadmap/core/services/github/github_conflict_detector.py +179 -0
  384. roadmap/core/services/github/github_entity_classifier.py +51 -0
  385. roadmap/core/services/github/github_integration_service.py +439 -0
  386. roadmap/core/services/github/github_issue_client.py +346 -0
  387. roadmap/core/services/health/__init__.py +1 -0
  388. roadmap/core/services/health/backup_cleanup_service.py +202 -0
  389. roadmap/core/services/health/data_integrity_validator_service.py +90 -0
  390. roadmap/core/services/health/entity_health_scanner.py +460 -0
  391. roadmap/core/services/health/file_repair_service.py +193 -0
  392. roadmap/core/services/health/health_check_service.py +166 -0
  393. roadmap/core/services/health/health_models.py +80 -0
  394. roadmap/core/services/health/infrastructure_validator_service.py +328 -0
  395. roadmap/core/services/health/issue_health_scanner.py +263 -0
  396. roadmap/core/services/helpers/__init__.py +18 -0
  397. roadmap/core/services/helpers/status_change_helpers.py +88 -0
  398. roadmap/core/services/initialization/__init__.py +19 -0
  399. roadmap/core/services/initialization/utils.py +104 -0
  400. roadmap/core/services/initialization/validator.py +78 -0
  401. roadmap/core/services/initialization/workflow.py +182 -0
  402. roadmap/core/services/initialization_service.py +111 -0
  403. roadmap/core/services/issue/__init__.py +1 -0
  404. roadmap/core/services/issue/assignee_validation_service.py +312 -0
  405. roadmap/core/services/issue/issue_creation_service.py +309 -0
  406. roadmap/core/services/issue/issue_filter_service.py +302 -0
  407. roadmap/core/services/issue/issue_matching_service.py +127 -0
  408. roadmap/core/services/issue/issue_service.py +765 -0
  409. roadmap/core/services/issue/issue_update_service.py +144 -0
  410. roadmap/core/services/issue/start_issue_service.py +174 -0
  411. roadmap/core/services/issue_helpers/__init__.py +18 -0
  412. roadmap/core/services/issue_helpers/issue_filters.py +296 -0
  413. roadmap/core/services/milestone_service.py +438 -0
  414. roadmap/core/services/project/__init__.py +1 -0
  415. roadmap/core/services/project/project_service.py +434 -0
  416. roadmap/core/services/project/project_status_service.py +125 -0
  417. roadmap/core/services/project_init/__init__.py +22 -0
  418. roadmap/core/services/project_init/context_detection.py +146 -0
  419. roadmap/core/services/project_init/creation.py +76 -0
  420. roadmap/core/services/project_init/detection.py +51 -0
  421. roadmap/core/services/project_init/template.py +228 -0
  422. roadmap/core/services/status_change_service.py +88 -0
  423. roadmap/core/services/sync/__init__.py +1 -0
  424. roadmap/core/services/sync/batch_processor.py +290 -0
  425. roadmap/core/services/sync/conflict_resolver.py +154 -0
  426. roadmap/core/services/sync/dependency_resolver.py +367 -0
  427. roadmap/core/services/sync/duplicate_detector.py +771 -0
  428. roadmap/core/services/sync/duplicate_resolver.py +315 -0
  429. roadmap/core/services/sync/error_classification.py +470 -0
  430. roadmap/core/services/sync/performance.py +263 -0
  431. roadmap/core/services/sync/sync_change_computer.py +211 -0
  432. roadmap/core/services/sync/sync_checkpoint.py +348 -0
  433. roadmap/core/services/sync/sync_conflict_detector.py +147 -0
  434. roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
  435. roadmap/core/services/sync/sync_errors.py +383 -0
  436. roadmap/core/services/sync/sync_key_normalizer.py +222 -0
  437. roadmap/core/services/sync/sync_metadata_service.py +361 -0
  438. roadmap/core/services/sync/sync_plan.py +171 -0
  439. roadmap/core/services/sync/sync_plan_executor.py +447 -0
  440. roadmap/core/services/sync/sync_report.py +296 -0
  441. roadmap/core/services/sync/sync_state.py +173 -0
  442. roadmap/core/services/sync/sync_state_comparator.py +690 -0
  443. roadmap/core/services/sync/sync_state_manager.py +431 -0
  444. roadmap/core/services/sync/sync_state_normalizer.py +98 -0
  445. roadmap/core/services/sync/sync_three_way.py +78 -0
  446. roadmap/core/services/sync/three_way_merger.py +178 -0
  447. roadmap/core/services/utils/__init__.py +1 -0
  448. roadmap/core/services/utils/configuration_service.py +113 -0
  449. roadmap/core/services/utils/critical_path_calculator.py +418 -0
  450. roadmap/core/services/utils/dependency_analyzer.py +360 -0
  451. roadmap/core/services/utils/field_conflict_detector.py +126 -0
  452. roadmap/core/services/utils/remote_fetcher.py +202 -0
  453. roadmap/core/services/utils/remote_state_normalizer.py +110 -0
  454. roadmap/core/services/utils/retry_policy.py +51 -0
  455. roadmap/core/services/validator_base.py +130 -0
  456. roadmap/core/services/validators/__init__.py +28 -0
  457. roadmap/core/services/validators/_utils.py +20 -0
  458. roadmap/core/services/validators/archivable_issues_validator.py +71 -0
  459. roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
  460. roadmap/core/services/validators/backup_validator.py +89 -0
  461. roadmap/core/services/validators/data_integrity_validator.py +84 -0
  462. roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
  463. roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
  464. roadmap/core/services/validators/folder_structure_validator.py +165 -0
  465. roadmap/core/services/validators/health_status_utils.py +28 -0
  466. roadmap/core/services/validators/milestone_naming_validator.py +146 -0
  467. roadmap/core/services/validators/missing_headlines_validator.py +97 -0
  468. roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
  469. roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
  470. roadmap/core/utils/git_remote_parser.py +83 -0
  471. roadmap/core/utils/github_urls.py +66 -0
  472. roadmap/domain/validation.py +30 -0
  473. roadmap/infrastructure/__init__.py +55 -0
  474. roadmap/infrastructure/coordination/__init__.py +8 -0
  475. roadmap/infrastructure/coordination/coordinator_params.py +82 -0
  476. roadmap/infrastructure/coordination/core.py +446 -0
  477. roadmap/infrastructure/coordination/git_coordinator.py +160 -0
  478. roadmap/infrastructure/coordination/initialization.py +348 -0
  479. roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
  480. roadmap/infrastructure/coordination/issue_operations.py +456 -0
  481. roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
  482. roadmap/infrastructure/coordination/milestone_operations.py +201 -0
  483. roadmap/infrastructure/coordination/project_coordinator.py +113 -0
  484. roadmap/infrastructure/coordination/project_operations.py +144 -0
  485. roadmap/infrastructure/coordination/team_coordinator.py +107 -0
  486. roadmap/infrastructure/coordination/user_operations.py +150 -0
  487. roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
  488. roadmap/infrastructure/coordination_gateway.py +152 -0
  489. roadmap/infrastructure/git/__init__.py +6 -0
  490. roadmap/infrastructure/git/git_integration_ops.py +234 -0
  491. roadmap/infrastructure/git_gateway.py +35 -0
  492. roadmap/infrastructure/github_gateway.py +52 -0
  493. roadmap/infrastructure/maintenance/__init__.py +5 -0
  494. roadmap/infrastructure/maintenance/cleanup.py +483 -0
  495. roadmap/infrastructure/observability/__init__.py +7 -0
  496. roadmap/infrastructure/observability/health.py +196 -0
  497. roadmap/infrastructure/observability/health_formatter.py +73 -0
  498. roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
  499. roadmap/infrastructure/persistence_gateway.py +164 -0
  500. roadmap/infrastructure/security/__init__.py +1 -0
  501. roadmap/infrastructure/security/credentials.py +489 -0
  502. roadmap/infrastructure/sync_gateway.py +94 -0
  503. roadmap/infrastructure/validation/__init__.py +7 -0
  504. roadmap/infrastructure/validation/file_enumeration.py +219 -0
  505. roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
  506. roadmap/infrastructure/validation/github_validator.py +101 -0
  507. roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
  508. roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
  509. roadmap/infrastructure/validation_gateway.py +96 -0
  510. roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
  511. roadmap/settings.py +345 -0
  512. roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
  513. roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
  514. roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
  515. roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
  516. roadmap/version.py +381 -0
  517. roadmap_cli-0.1.1.dist-info/METADATA +448 -0
  518. roadmap_cli-0.1.1.dist-info/RECORD +521 -0
  519. roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
  520. roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
  521. roadmap_cli-0.1.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,178 @@
1
+ """Three-way merge logic for intelligent sync conflict resolution.
2
+
3
+ Implements the core algorithm: base vs local vs remote comparison
4
+ to automatically resolve most conflicts without data loss.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+ from typing import Any
10
+
11
+
12
+ class MergeStatus(StrEnum):
13
+ """Result of merging a field."""
14
+
15
+ CLEAN = "clean" # Auto-resolved, no conflict
16
+ CONFLICT = "conflict" # Both sides changed differently
17
+
18
+
19
+ @dataclass
20
+ class FieldMergeResult:
21
+ """Result of merging a single field."""
22
+
23
+ value: Any
24
+ status: MergeStatus
25
+ reason: str # Why this decision was made
26
+
27
+ def is_conflict(self) -> bool:
28
+ """Check if this is a conflict."""
29
+ return self.status == MergeStatus.CONFLICT
30
+
31
+
32
+ class ThreeWayMerger:
33
+ """Implements three-way merge logic.
34
+
35
+ Given base, local, and remote versions of a field, intelligently
36
+ determines the merged value using this algorithm:
37
+
38
+ - If only local changed: take local
39
+ - If only remote changed: take remote
40
+ - If both changed the same way: take either (no conflict)
41
+ - If neither changed: take either (no conflict)
42
+ - If both changed differently: CONFLICT (needs resolution)
43
+ """
44
+
45
+ def merge_field(
46
+ self,
47
+ field_name: str,
48
+ base: Any,
49
+ local: Any,
50
+ remote: Any,
51
+ ) -> FieldMergeResult:
52
+ """Merge a single field using three-way logic.
53
+
54
+ Args:
55
+ field_name: Name of the field (for debugging)
56
+ base: The agreed-upon value from last sync
57
+ local: Current local value
58
+ remote: Current remote value
59
+
60
+ Returns:
61
+ FieldMergeResult with merged value and status
62
+ """
63
+ # Case 1: Neither changed (everything stayed at base)
64
+ if local == base and remote == base:
65
+ return FieldMergeResult(
66
+ value=local,
67
+ status=MergeStatus.CLEAN,
68
+ reason=f"{field_name}: no changes",
69
+ )
70
+
71
+ # Case 2: Only local changed (remote stayed at base)
72
+ if local != base and remote == base:
73
+ return FieldMergeResult(
74
+ value=local,
75
+ status=MergeStatus.CLEAN,
76
+ reason=f"{field_name}: only local changed",
77
+ )
78
+
79
+ # Case 3: Only remote changed (local stayed at base)
80
+ if remote != base and local == base:
81
+ return FieldMergeResult(
82
+ value=remote,
83
+ status=MergeStatus.CLEAN,
84
+ reason=f"{field_name}: only remote changed",
85
+ )
86
+
87
+ # Case 4: Both changed, but to the same value
88
+ if local == remote:
89
+ return FieldMergeResult(
90
+ value=local,
91
+ status=MergeStatus.CLEAN,
92
+ reason=f"{field_name}: both changed to same value",
93
+ )
94
+
95
+ # Case 5: Both changed differently - TRUE CONFLICT
96
+ return FieldMergeResult(
97
+ value=None, # Can't auto-resolve
98
+ status=MergeStatus.CONFLICT,
99
+ reason=f"{field_name}: both sides changed differently (base={base}, local={local}, remote={remote})",
100
+ )
101
+
102
+ def merge_issue(
103
+ self,
104
+ issue_id: str,
105
+ base: dict[str, Any],
106
+ local: dict[str, Any],
107
+ remote: dict[str, Any],
108
+ ) -> tuple[dict[str, Any], list[str]]:
109
+ """Merge all fields of an issue.
110
+
111
+ Returns:
112
+ (merged_fields, list_of_conflicted_field_names)
113
+
114
+ Note: Conflicted fields are NOT included in merged_fields.
115
+ They must be resolved separately by ConflictResolver.
116
+ """
117
+ merged = {}
118
+ conflicts = []
119
+
120
+ # Get all field names from all three versions
121
+ all_fields = set(base.keys()) | set(local.keys()) | set(remote.keys())
122
+
123
+ for field in all_fields:
124
+ result = self.merge_field(
125
+ field,
126
+ base.get(field),
127
+ local.get(field),
128
+ remote.get(field),
129
+ )
130
+
131
+ if result.is_conflict():
132
+ conflicts.append(field)
133
+ # Don't include conflicted fields in merged
134
+ # They'll be handled by ConflictResolver
135
+ else:
136
+ merged[field] = result.value
137
+
138
+ return merged, conflicts
139
+
140
+ def merge_issues(
141
+ self,
142
+ issues: dict[str, dict[str, Any]],
143
+ base_issues: dict[str, dict[str, Any]],
144
+ local_issues: dict[str, dict[str, Any]],
145
+ remote_issues: dict[str, dict[str, Any]],
146
+ ) -> tuple[dict[str, tuple[dict, list[str]]], list[str]]:
147
+ """Merge multiple issues.
148
+
149
+ Returns:
150
+ (issue_results, deleted_issues)
151
+
152
+ where issue_results[issue_id] = (merged_fields, conflict_fields)
153
+ and deleted_issues is a list of issues removed on remote
154
+ """
155
+ results = {}
156
+ deleted = []
157
+
158
+ # Get all issue IDs from all three versions
159
+ all_issues = (
160
+ set(base_issues.keys())
161
+ | set(local_issues.keys())
162
+ | set(remote_issues.keys())
163
+ )
164
+
165
+ for issue_id in all_issues:
166
+ base = base_issues.get(issue_id, {})
167
+ local = local_issues.get(issue_id, {})
168
+ remote = remote_issues.get(issue_id, {})
169
+
170
+ # If issue was deleted remotely and not modified locally, skip it
171
+ if not remote and base and not local:
172
+ deleted.append(issue_id)
173
+ continue
174
+
175
+ merged, conflicts = self.merge_issue(issue_id, base, local, remote)
176
+ results[issue_id] = (merged, conflicts)
177
+
178
+ return results, deleted
@@ -0,0 +1 @@
1
+ """Package initialization file."""
@@ -0,0 +1,113 @@
1
+ """Configuration management service for roadmap application.
2
+
3
+ Consolidates settings and credentials management into a single service layer,
4
+ providing unified access to application configuration and secrets.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import structlog
11
+
12
+ from roadmap.core.interfaces import CredentialProvider
13
+ from roadmap.settings import settings
14
+
15
+ logger = structlog.get_logger()
16
+
17
+
18
+ class ConfigurationService:
19
+ """Service for managing application configuration and credentials."""
20
+
21
+ def __init__(self, credential_provider: CredentialProvider | None = None):
22
+ """Initialize configuration service.
23
+
24
+ Args:
25
+ credential_provider: Optional credential provider implementation.
26
+ If None, defaults to infrastructure CredentialManager.
27
+ """
28
+ if credential_provider is None:
29
+ from roadmap.infrastructure.security.credentials import CredentialManager
30
+
31
+ credential_provider = CredentialManager()
32
+
33
+ self.credential_manager = credential_provider
34
+ self._settings = settings
35
+
36
+ # ==================== Configuration Access ====================
37
+
38
+ def get_setting(self, key: str, default: Any = None) -> Any:
39
+ """Get a configuration setting by key.
40
+
41
+ Args:
42
+ key: Configuration key (dot notation supported, e.g. 'database.path')
43
+ default: Default value if setting not found
44
+
45
+ Returns:
46
+ Configuration value or default
47
+ """
48
+ try:
49
+ return self._settings.get(key, default)
50
+ except Exception as e:
51
+ logger.debug(
52
+ "config_get_failed",
53
+ operation="get_config",
54
+ key=key,
55
+ error=str(e),
56
+ action="Returning default value",
57
+ )
58
+ return default
59
+
60
+ def get_github_config(self) -> dict[str, Any]:
61
+ """Get GitHub integration configuration.
62
+
63
+ Returns:
64
+ GitHub configuration dictionary
65
+ """
66
+ return {
67
+ "enabled": self.get_setting("github.enabled", False),
68
+ "repo": self.get_setting("github.repo", ""),
69
+ "sync_labels": self.get_setting("github.sync_labels", True),
70
+ }
71
+
72
+ def get_sync_config(self) -> dict[str, Any]:
73
+ """Get sync operation configuration.
74
+
75
+ Returns:
76
+ Sync configuration dictionary including duplicate detection thresholds
77
+ """
78
+ return {
79
+ "enable_duplicate_detection": self.get_setting(
80
+ "sync.enable_duplicate_detection", True
81
+ ),
82
+ "duplicate_title_threshold": self.get_setting(
83
+ "sync.duplicate_title_threshold", 0.90
84
+ ),
85
+ "duplicate_content_threshold": self.get_setting(
86
+ "sync.duplicate_content_threshold", 0.85
87
+ ),
88
+ "duplicate_auto_resolve_threshold": self.get_setting(
89
+ "sync.duplicate_auto_resolve_threshold", 0.95
90
+ ),
91
+ }
92
+
93
+ # ==================== Credential Management ====================
94
+
95
+ def get_github_token(self) -> str | None:
96
+ """Retrieve GitHub token securely.
97
+
98
+ Returns:
99
+ GitHub token if found, None otherwise
100
+ """
101
+ return self.credential_manager.get_token()
102
+
103
+ # ==================== Configuration Paths ====================
104
+
105
+ def get_config_paths(self) -> dict[str, Path]:
106
+ """Get configuration file paths.
107
+
108
+ Returns:
109
+ Dictionary of configuration file paths
110
+ """
111
+ from roadmap.settings import get_config_paths
112
+
113
+ return get_config_paths()
@@ -0,0 +1,418 @@
1
+ """Critical path analysis for issue dependencies.
2
+
3
+ Calculates which issues are on the critical path and impact project timeline.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from datetime import UTC, datetime, timedelta
8
+
9
+ from roadmap.core.domain.issue import Issue
10
+ from roadmap.core.domain.milestone import Milestone
11
+
12
+
13
+ @dataclass
14
+ class PathNode:
15
+ """Represents an issue in the critical path analysis."""
16
+
17
+ issue_id: str
18
+ issue_title: str
19
+ duration_hours: float
20
+ dependencies: list[str] = field(default_factory=list)
21
+ start_date: datetime | None = None
22
+ end_date: datetime | None = None
23
+ slack_time: float = 0.0 # Time available before blocking other tasks
24
+ is_critical: bool = False
25
+
26
+
27
+ @dataclass
28
+ class CriticalPathResult:
29
+ """Results from critical path analysis."""
30
+
31
+ critical_path: list[PathNode]
32
+ total_duration: float
33
+ critical_issue_ids: list[str]
34
+ blocking_issues: dict[str, list[str]] # issue_id -> list of blocked issue_ids
35
+ project_end_date: datetime | None = None
36
+ issues_by_criticality: dict[str, list[str]] = field(default_factory=dict)
37
+
38
+
39
+ class CriticalPathCalculator:
40
+ """Calculates critical path for issues based on dependencies."""
41
+
42
+ def __init__(self):
43
+ """Initialize calculator."""
44
+ pass
45
+
46
+ def calculate_critical_path(
47
+ self, issues: list[Issue], milestone: Milestone | None = None
48
+ ) -> CriticalPathResult:
49
+ """Calculate critical path from issues with dependencies.
50
+
51
+ Args:
52
+ issues: List of issues to analyze
53
+ milestone: Optional milestone context for date calculations
54
+
55
+ Returns:
56
+ CriticalPathResult with critical path information
57
+ """
58
+ if not issues:
59
+ return CriticalPathResult(
60
+ critical_path=[],
61
+ total_duration=0.0,
62
+ critical_issue_ids=[],
63
+ blocking_issues={},
64
+ )
65
+
66
+ # Build dependency graph
67
+ graph = self._build_dependency_graph(issues)
68
+
69
+ # Calculate earliest start/end dates
70
+ node_data = self._calculate_schedule(issues, graph)
71
+
72
+ # Find critical path
73
+ critical_path = self._find_critical_path(node_data, graph)
74
+
75
+ # Analyze blocking relationships
76
+ blocking_issues = self._analyze_blocking(issues)
77
+
78
+ # Calculate slack time
79
+ self._calculate_slack(node_data, critical_path)
80
+
81
+ # Determine project end date
82
+ project_end_date = self._calculate_project_end_date(node_data, milestone)
83
+
84
+ return CriticalPathResult(
85
+ critical_path=critical_path,
86
+ total_duration=sum(node.duration_hours for node in critical_path),
87
+ critical_issue_ids=[node.issue_id for node in critical_path],
88
+ blocking_issues=blocking_issues,
89
+ project_end_date=project_end_date,
90
+ issues_by_criticality=self._group_by_criticality(issues, critical_path),
91
+ )
92
+
93
+ def _build_dependency_graph(self, issues: list[Issue]) -> dict[str, list[str]]:
94
+ """Build dependency graph from issues.
95
+
96
+ Args:
97
+ issues: List of issues
98
+
99
+ Returns:
100
+ Dictionary mapping issue_id to list of dependent issue_ids
101
+ """
102
+ graph = {issue.id: issue.depends_on or [] for issue in issues}
103
+ return graph
104
+
105
+ def _calculate_schedule(
106
+ self, issues: list[Issue], graph: dict[str, list[str]]
107
+ ) -> dict[str, PathNode]:
108
+ """Calculate earliest start and end times.
109
+
110
+ Args:
111
+ issues: List of issues
112
+ graph: Dependency graph
113
+
114
+ Returns:
115
+ Dictionary mapping issue_id to PathNode
116
+ """
117
+ nodes = {}
118
+ for issue in issues:
119
+ duration = self._estimate_duration(issue)
120
+ nodes[issue.id] = PathNode(
121
+ issue_id=issue.id,
122
+ issue_title=issue.title,
123
+ duration_hours=duration,
124
+ dependencies=graph.get(issue.id, []),
125
+ )
126
+
127
+ # Calculate earliest start/end times using topological sort
128
+ start_times = {}
129
+ visiting = set() # Track nodes currently being processed to detect cycles
130
+
131
+ def calculate_start_time(issue_id: str) -> float:
132
+ if issue_id in start_times:
133
+ return start_times[issue_id]
134
+
135
+ # Detect circular dependencies
136
+ if issue_id in visiting:
137
+ # Return 0 for circular refs (they shouldn't exist, but graceful fallback)
138
+ return 0.0
139
+
140
+ visiting.add(issue_id)
141
+
142
+ try:
143
+ dependencies = graph.get(issue_id, [])
144
+ if not dependencies:
145
+ start_times[issue_id] = 0.0
146
+ return 0.0
147
+
148
+ # Max end time of all dependencies
149
+ max_end_time = 0.0
150
+ for dep_id in dependencies:
151
+ if dep_id in nodes:
152
+ dep_start = calculate_start_time(dep_id)
153
+ dep_duration = nodes[dep_id].duration_hours
154
+ max_end_time = max(max_end_time, dep_start + dep_duration)
155
+
156
+ start_times[issue_id] = max_end_time
157
+ return max_end_time
158
+ finally:
159
+ visiting.discard(issue_id)
160
+
161
+ # Calculate all start times
162
+ for issue_id in nodes:
163
+ start_time = calculate_start_time(issue_id)
164
+ nodes[issue_id].start_date = datetime.now(UTC) + timedelta(hours=start_time)
165
+ nodes[issue_id].end_date = datetime.now(UTC) + timedelta(
166
+ hours=start_time + nodes[issue_id].duration_hours
167
+ )
168
+
169
+ return nodes
170
+
171
+ def _find_critical_path(
172
+ self, nodes: dict[str, PathNode], graph: dict[str, list[str]]
173
+ ) -> list[PathNode]:
174
+ """Find the critical path (longest duration path through dependencies).
175
+
176
+ Args:
177
+ nodes: Dictionary of nodes with schedule info
178
+ graph: Dependency graph
179
+
180
+ Returns:
181
+ List of PathNode objects on critical path, ordered by dependency
182
+ """
183
+ if not nodes:
184
+ return []
185
+
186
+ # Find leaf nodes (no blocks)
187
+ leaf_nodes = [
188
+ issue_id
189
+ for issue_id, _ in nodes.items()
190
+ if not any(issue_id in graph.get(other_id, []) for other_id in nodes)
191
+ ]
192
+
193
+ if not leaf_nodes:
194
+ # If no clear leaf, use nodes with latest end dates
195
+ leaf_nodes = sorted(
196
+ nodes.keys(),
197
+ key=lambda x: nodes[x].end_date or datetime.now(UTC),
198
+ reverse=True,
199
+ )[:1]
200
+
201
+ # Trace back from leaf nodes to find longest path
202
+ critical_paths = []
203
+
204
+ for leaf_id in leaf_nodes:
205
+ path = self._trace_path_backward(leaf_id, nodes, graph)
206
+ critical_paths.append(path)
207
+
208
+ # Return longest path
209
+ critical_path = max(critical_paths, key=lambda p: len(p), default=[])
210
+
211
+ # Mark critical nodes
212
+ for node in critical_path:
213
+ node.is_critical = True
214
+
215
+ return critical_path
216
+
217
+ def _trace_path_backward(
218
+ self, issue_id: str, nodes: dict[str, PathNode], graph: dict[str, list[str]]
219
+ ) -> list[PathNode]:
220
+ """Trace path backward from a node to root dependencies.
221
+
222
+ Args:
223
+ issue_id: Starting issue ID
224
+ nodes: Node dictionary
225
+ graph: Dependency graph
226
+
227
+ Returns:
228
+ List of nodes in dependency chain
229
+ """
230
+ path = []
231
+ current_id = issue_id
232
+ visited = set() # Track visited nodes to prevent infinite loops
233
+
234
+ while current_id in nodes:
235
+ # Detect circular dependencies
236
+ if current_id in visited:
237
+ break
238
+
239
+ visited.add(current_id)
240
+ path.append(nodes[current_id])
241
+ dependencies = graph.get(current_id, [])
242
+
243
+ if not dependencies:
244
+ break
245
+
246
+ # Follow the dependency with longest duration
247
+ current_id = max(
248
+ dependencies,
249
+ key=lambda d: nodes.get(d, PathNode("", "", 0)).duration_hours,
250
+ )
251
+
252
+ return list(reversed(path))
253
+
254
+ def _estimate_duration(self, issue: Issue) -> float:
255
+ """Estimate duration from issue estimated_hours field.
256
+
257
+ Args:
258
+ issue: Issue to estimate
259
+
260
+ Returns:
261
+ Duration in hours
262
+ """
263
+ if not issue.estimated_hours:
264
+ return 4.0 # Default 4 hours
265
+
266
+ return float(issue.estimated_hours)
267
+
268
+ def _calculate_slack(
269
+ self, nodes: dict[str, PathNode], critical_path: list[PathNode]
270
+ ) -> None:
271
+ """Calculate slack time for non-critical paths.
272
+
273
+ Args:
274
+ nodes: Dictionary of all nodes
275
+ critical_path: List of critical path nodes
276
+ """
277
+ # Critical path nodes have no slack
278
+ critical_ids = {node.issue_id for node in critical_path}
279
+
280
+ if critical_path:
281
+ max_end_time = max(
282
+ (node.end_date or datetime.now(UTC)) for node in critical_path
283
+ )
284
+
285
+ for node_id, node in nodes.items():
286
+ if node_id not in critical_ids and node.end_date:
287
+ slack_hours = (max_end_time - node.end_date).total_seconds() / 3600
288
+ node.slack_time = max(0.0, slack_hours)
289
+
290
+ def _calculate_project_end_date(
291
+ self,
292
+ nodes: dict[str, PathNode],
293
+ milestone: Milestone | None = None,
294
+ ) -> datetime | None:
295
+ """Calculate expected project completion date.
296
+
297
+ Args:
298
+ nodes: Dictionary of nodes
299
+ milestone: Optional milestone with due date
300
+
301
+ Returns:
302
+ Expected project end date
303
+ """
304
+ if not nodes:
305
+ return milestone.due_date if milestone else None
306
+
307
+ # Project ends when all critical path items complete
308
+ latest_end = max(
309
+ (node.end_date for node in nodes.values() if node.end_date),
310
+ default=None,
311
+ )
312
+
313
+ if latest_end and milestone and milestone.due_date:
314
+ return min(latest_end, milestone.due_date)
315
+
316
+ return latest_end or (milestone.due_date if milestone else None)
317
+
318
+ def _analyze_blocking(self, issues: list[Issue]) -> dict[str, list[str]]:
319
+ """Analyze which issues block others.
320
+
321
+ Args:
322
+ issues: List of issues
323
+
324
+ Returns:
325
+ Dictionary mapping issue_id to list of blocked issue_ids
326
+ """
327
+ blocking = {}
328
+
329
+ for issue in issues:
330
+ if issue.blocks:
331
+ blocking[issue.id] = issue.blocks
332
+
333
+ return blocking
334
+
335
+ def _group_by_criticality(
336
+ self, issues: list[Issue], critical_path: list[PathNode]
337
+ ) -> dict[str, list[str]]:
338
+ """Group issues by criticality level.
339
+
340
+ Args:
341
+ issues: List of all issues
342
+ critical_path: List of critical path nodes
343
+
344
+ Returns:
345
+ Dictionary with criticality groups
346
+ """
347
+ critical_ids = {node.issue_id for node in critical_path}
348
+ blocking_issues = set()
349
+ blocked_issues = set()
350
+
351
+ for issue in issues:
352
+ if issue.blocks:
353
+ blocking_issues.add(issue.id)
354
+ if issue.depends_on:
355
+ blocked_issues.add(issue.id)
356
+
357
+ return {
358
+ "critical": list(critical_ids),
359
+ "blocking": list(blocking_issues),
360
+ "blocked": list(blocked_issues),
361
+ "independent": [
362
+ issue.id
363
+ for issue in issues
364
+ if issue.id not in critical_ids
365
+ and issue.id not in blocking_issues
366
+ and issue.id not in blocked_issues
367
+ ],
368
+ }
369
+
370
+ def find_blocking_issues(
371
+ self, issues: list[Issue], target_issue_id: str
372
+ ) -> list[Issue]:
373
+ """Find all issues blocking a specific issue.
374
+
375
+ Args:
376
+ issues: List of all issues
377
+ target_issue_id: ID of issue to check
378
+
379
+ Returns:
380
+ List of issues blocking the target
381
+ """
382
+ issue_map = {issue.id: issue for issue in issues}
383
+ target = issue_map.get(target_issue_id)
384
+
385
+ if not target or not target.depends_on:
386
+ return []
387
+
388
+ blocking = []
389
+ for dep_id in target.depends_on:
390
+ if dep_id in issue_map:
391
+ blocking.append(issue_map[dep_id])
392
+
393
+ return blocking
394
+
395
+ def find_blocked_issues(
396
+ self, issues: list[Issue], blocker_issue_id: str
397
+ ) -> list[Issue]:
398
+ """Find all issues blocked by a specific issue.
399
+
400
+ Args:
401
+ issues: List of all issues
402
+ blocker_issue_id: ID of blocking issue
403
+
404
+ Returns:
405
+ List of issues blocked by the blocker
406
+ """
407
+ issue_map = {issue.id: issue for issue in issues}
408
+ blocker = issue_map.get(blocker_issue_id)
409
+
410
+ if not blocker or not blocker.blocks:
411
+ return []
412
+
413
+ blocked = []
414
+ for blocked_id in blocker.blocks:
415
+ if blocked_id in issue_map:
416
+ blocked.append(issue_map[blocked_id])
417
+
418
+ return blocked