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,456 @@
1
+ """Issue Operations Module - Handles all issue-related CRUD operations.
2
+
3
+ This module encapsulates issue management responsibilities extracted from RoadmapCore,
4
+ including creating, reading, updating, and deleting issues, as well as issue assignment
5
+ and milestone-related queries.
6
+
7
+ Responsibilities:
8
+ - Issue CRUD operations (create, list, get, update, delete)
9
+ - Issue assignment to milestones
10
+ - Issue queries (backlog, by milestone, grouped by milestone)
11
+ - Issue movement between milestones
12
+ """
13
+
14
+ from difflib import get_close_matches
15
+ from pathlib import Path
16
+
17
+ from roadmap.adapters.persistence.parser import IssueParser
18
+ from roadmap.common.cache import SessionCache
19
+ from roadmap.common.utils.timezone_utils import now_utc
20
+ from roadmap.core.domain import (
21
+ Issue,
22
+ IssueType,
23
+ Priority,
24
+ Status,
25
+ )
26
+ from roadmap.core.models import IssueCreateServiceParams, IssueUpdateServiceParams
27
+ from roadmap.core.services import IssueService
28
+
29
+
30
+ class IssueOperations:
31
+ """Manager for issue-related operations."""
32
+
33
+ # Cache TTL in seconds
34
+ _cache_ttl = 60
35
+
36
+ def __init__(self, issue_service: IssueService, issues_dir: Path):
37
+ """Initialize issue operations manager.
38
+
39
+ Args:
40
+ issue_service: The IssueService instance for database operations
41
+ issues_dir: Path to the issues directory
42
+ """
43
+ self.issue_service = issue_service
44
+ self.issues_dir = issues_dir
45
+ # Cache for get_issues_by_milestone results (instance-level to avoid test pollution)
46
+ self._milestone_cache = SessionCache()
47
+
48
+ def create_issue(
49
+ self,
50
+ title: str,
51
+ priority: Priority = Priority.MEDIUM,
52
+ issue_type: IssueType = IssueType.OTHER,
53
+ milestone: str | None = None,
54
+ labels: list[str] | None = None,
55
+ assignee: str | None = None,
56
+ estimated_hours: float | None = None,
57
+ depends_on: list[str] | None = None,
58
+ blocks: list[str] | None = None,
59
+ status: str | None = None,
60
+ content: str | None = None,
61
+ ) -> Issue:
62
+ """Create a new issue.
63
+
64
+ Args:
65
+ title: Issue title
66
+ priority: Issue priority (default: MEDIUM)
67
+ issue_type: Type of issue (default: OTHER)
68
+ milestone: Milestone name (optional)
69
+ labels: List of labels (optional)
70
+ assignee: Assigned user (optional)
71
+ estimated_hours: Estimated hours to complete (optional)
72
+ depends_on: List of issue IDs this depends on (optional)
73
+ blocks: List of issue IDs this blocks (optional)
74
+ status: Issue status (optional, defaults to TODO)
75
+ content: Issue content/description (optional)
76
+
77
+ Returns:
78
+ Created Issue object
79
+ """
80
+ # Create service params dataclass from individual arguments
81
+ params = IssueCreateServiceParams(
82
+ title=title,
83
+ priority=priority.value
84
+ if isinstance(priority, Priority)
85
+ else str(priority),
86
+ issue_type=issue_type.value
87
+ if isinstance(issue_type, IssueType)
88
+ else str(issue_type),
89
+ milestone=milestone,
90
+ labels=labels or [],
91
+ assignee=assignee,
92
+ content=content,
93
+ estimate=estimated_hours,
94
+ depends_on=depends_on or [],
95
+ blocks=blocks or [],
96
+ status=status or "todo",
97
+ )
98
+
99
+ result = self.issue_service.create_issue(params)
100
+
101
+ # Invalidate milestone cache after creation
102
+ self._milestone_cache.clear()
103
+
104
+ return result
105
+
106
+ def list_issues(
107
+ self,
108
+ milestone: str | None = None,
109
+ status: Status | None = None,
110
+ priority: Priority | None = None,
111
+ issue_type: IssueType | None = None,
112
+ assignee: str | None = None,
113
+ ) -> list[Issue]:
114
+ """List issues with optional filtering.
115
+
116
+ Args:
117
+ milestone: Filter by milestone name (optional)
118
+ status: Filter by issue status (optional)
119
+ priority: Filter by priority (optional)
120
+ issue_type: Filter by issue type (optional)
121
+ assignee: Filter by assignee (optional)
122
+
123
+ Returns:
124
+ List of Issue objects matching the filters
125
+ """
126
+ return self.issue_service.list_issues(
127
+ milestone=milestone,
128
+ status=status,
129
+ priority=priority,
130
+ issue_type=issue_type,
131
+ assignee=assignee,
132
+ )
133
+
134
+ def list_all_including_archived(self) -> list[Issue]:
135
+ """List all issues including archived ones.
136
+
137
+ Used by sync operations that need to compare both active and archived issues.
138
+
139
+ Returns:
140
+ List of all Issue objects (both active and archived)
141
+ """
142
+ return self.issue_service.list_all_including_archived()
143
+
144
+ def get_issue(self, issue_id: str) -> Issue | None:
145
+ """Get a specific issue by ID.
146
+
147
+ Args:
148
+ issue_id: The issue ID to retrieve
149
+
150
+ Returns:
151
+ Issue object if found, None otherwise
152
+ """
153
+ return self.issue_service.get_issue(issue_id)
154
+
155
+ def _build_update_params(
156
+ self, issue_id: str, updates: dict
157
+ ) -> IssueUpdateServiceParams:
158
+ """Build IssueUpdateServiceParams from a raw updates dict."""
159
+ params_dict: dict = {"issue_id": issue_id}
160
+
161
+ if "title" in updates:
162
+ params_dict["title"] = updates["title"]
163
+ if "priority" in updates:
164
+ priority = updates["priority"]
165
+ params_dict["priority"] = (
166
+ priority.value if isinstance(priority, Priority) else priority
167
+ )
168
+ if "status" in updates:
169
+ status = updates["status"]
170
+ params_dict["status"] = (
171
+ status.value if isinstance(status, Status) else status
172
+ )
173
+ if "assignee" in updates:
174
+ params_dict["assignee"] = updates["assignee"]
175
+ if "milestone" in updates:
176
+ params_dict["milestone"] = updates["milestone"] # Can be None!
177
+ if "description" in updates:
178
+ params_dict["content"] = updates["description"]
179
+ if "estimate" in updates:
180
+ params_dict["estimate"] = updates["estimate"]
181
+ if "reason" in updates:
182
+ params_dict["reason"] = updates["reason"]
183
+
184
+ return IssueUpdateServiceParams(**params_dict)
185
+
186
+ _HANDLED_UPDATE_FIELDS = frozenset(
187
+ {
188
+ "issue_id",
189
+ "title",
190
+ "priority",
191
+ "status",
192
+ "assignee",
193
+ "milestone",
194
+ "content",
195
+ "estimate",
196
+ "reason",
197
+ }
198
+ )
199
+
200
+ def _apply_extra_fields_to_result(self, result: Issue, updates: dict) -> None:
201
+ """Apply update fields not handled by the service layer, then save to file."""
202
+ for field, value in updates.items():
203
+ if field not in self._HANDLED_UPDATE_FIELDS and hasattr(result, field):
204
+ setattr(result, field, value)
205
+
206
+ # Save extra fields to disk when the service layer didn't already do so.
207
+ # Milestone updates are written by the repository's own save(); skip to avoid
208
+ # overwriting a potentially relocated file path.
209
+ if result.file_path and "milestone" not in updates:
210
+ IssueParser.save_issue_file(result, Path(result.file_path))
211
+
212
+ def update_issue(self, issue_id: str, **updates) -> Issue | None:
213
+ """Update an existing issue.
214
+
215
+ Args:
216
+ issue_id: The issue ID to update
217
+ **updates: Fields to update
218
+
219
+ Returns:
220
+ Updated Issue object if found, None otherwise
221
+ """
222
+ params = self._build_update_params(issue_id, updates)
223
+ result = self.issue_service.update_issue(params)
224
+
225
+ if result is not None:
226
+ self._apply_extra_fields_to_result(result, updates)
227
+ self._milestone_cache.clear()
228
+
229
+ return result
230
+
231
+ def delete_issue(self, issue_id: str) -> bool:
232
+ """Delete an issue.
233
+
234
+ Args:
235
+ issue_id: The issue ID to delete
236
+
237
+ Returns:
238
+ True if issue was deleted, False if not found
239
+ """
240
+ result = self.issue_service.delete_issue(issue_id)
241
+
242
+ # Invalidate milestone cache after deletion
243
+ if result:
244
+ self._milestone_cache.clear()
245
+
246
+ return result
247
+
248
+ def assign_issue_to_milestone(self, issue_id: str, milestone_name: str) -> bool:
249
+ """Assign an issue to a milestone.
250
+
251
+ Args:
252
+ issue_id: The issue ID to assign
253
+ milestone_name: The milestone name to assign to
254
+
255
+ Returns:
256
+ True if assignment successful, False if issue or milestone not found
257
+ """
258
+ issue = self.get_issue(issue_id)
259
+ if not issue:
260
+ return False
261
+
262
+ # Validate milestone exists by checking file using same logic as Milestone.filename
263
+ safe_name = "".join(
264
+ c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
265
+ ).strip()
266
+ safe_name = safe_name.replace(" ", "-").lower()
267
+ milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
268
+ if not milestone_file.exists():
269
+ return False
270
+
271
+ # Use the repository's update method which handles file movement correctly
272
+ return self.update_issue(issue_id, milestone=milestone_name) is not None
273
+
274
+ def get_backlog_issues(self) -> list[Issue]:
275
+ """Get all issues not assigned to any milestone (backlog).
276
+
277
+ Returns:
278
+ List of Issue objects in the backlog
279
+ """
280
+ all_issues = self.list_issues()
281
+ return [issue for issue in all_issues if issue.is_backlog]
282
+
283
+ def get_milestone_issues(self, milestone_name: str) -> list[Issue]:
284
+ """Get all issues assigned to a specific milestone.
285
+
286
+ Args:
287
+ milestone_name: The milestone name to filter by
288
+
289
+ Returns:
290
+ List of Issue objects assigned to the milestone
291
+ """
292
+ all_issues = self.list_issues()
293
+ return [issue for issue in all_issues if issue.milestone == milestone_name]
294
+
295
+ def get_issues_by_milestone(self) -> dict[str, list[Issue]]:
296
+ """Get all issues grouped by milestone, including backlog.
297
+
298
+ Returns:
299
+ Dictionary mapping milestone names to lists of Issue objects,
300
+ with a "Backlog" key for unassigned issues
301
+ """
302
+ # Check cache first
303
+ cached = self._milestone_cache.get("all_milestones")
304
+ if cached is not None:
305
+ return cached
306
+
307
+ all_issues = self.list_issues()
308
+ grouped: dict[str, list[Issue]] = {"Backlog": []}
309
+
310
+ # Add backlog issues
311
+ for issue in all_issues:
312
+ if issue.is_backlog:
313
+ grouped["Backlog"].append(issue)
314
+ else:
315
+ milestone_name = issue.milestone
316
+ if milestone_name is None:
317
+ # Issues without milestone go to Backlog
318
+ grouped["Backlog"].append(issue)
319
+ else:
320
+ if milestone_name not in grouped:
321
+ grouped[milestone_name] = []
322
+ grouped[milestone_name].append(issue)
323
+
324
+ # Cache the result with TTL
325
+ self._milestone_cache.set("all_milestones", grouped, ttl=self._cache_ttl)
326
+
327
+ return grouped
328
+
329
+ def move_issue_to_milestone(
330
+ self, issue_id: str, milestone_name: str | None
331
+ ) -> bool:
332
+ """Move an issue to a milestone or to backlog if milestone_name is None.
333
+
334
+ Args:
335
+ issue_id: The issue ID to move
336
+ milestone_name: The target milestone name, or None for backlog
337
+
338
+ Returns:
339
+ True if move successful, False if issue not found or milestone invalid
340
+ """
341
+ issue = self.get_issue(issue_id)
342
+ if not issue:
343
+ return False
344
+
345
+ # Validate milestone exists if milestone_name is provided (None is valid for backlog)
346
+ if milestone_name is not None:
347
+ # Check if milestone file exists
348
+ safe_name = "".join(
349
+ c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
350
+ ).strip()
351
+ safe_name = safe_name.replace(" ", "-").lower()
352
+ milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
353
+ if not milestone_file.exists():
354
+ return False
355
+
356
+ # Update issue milestone using the repository method which handles file movement
357
+ updated_issue = self.update_issue(issue_id, milestone=milestone_name)
358
+
359
+ # Invalidate milestone cache after moving
360
+ self._milestone_cache.clear()
361
+
362
+ return updated_issue is not None
363
+
364
+ def batch_assign_to_milestone(
365
+ self,
366
+ issue_ids: list[str],
367
+ milestone_name: str,
368
+ preloaded_issues: list[Issue] | None = None,
369
+ ) -> tuple[int, int]:
370
+ """Batch assign multiple issues to a milestone in a single pass.
371
+
372
+ This method is more efficient than calling assign_issue_to_milestone
373
+ in a loop, as it validates the milestone once. If preloaded_issues
374
+ is provided, it uses those instead of scanning the filesystem.
375
+
376
+ Args:
377
+ issue_ids: List of issue IDs to assign
378
+ milestone_name: The milestone name to assign to
379
+ preloaded_issues: Optional pre-loaded list of issues (avoids filesystem scan)
380
+
381
+ Returns:
382
+ Tuple of (successful_count, failed_count)
383
+ """
384
+ # Validate milestone exists once upfront
385
+ safe_name = "".join(
386
+ c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
387
+ ).strip()
388
+ safe_name = safe_name.replace(" ", "-").lower()
389
+ milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
390
+ if not milestone_file.exists():
391
+ return 0, len(issue_ids)
392
+
393
+ # Load all issues once (or use preloaded)
394
+ if preloaded_issues is not None:
395
+ all_issues = preloaded_issues
396
+ else:
397
+ all_issues = self.list_issues()
398
+
399
+ issue_by_id = {issue.id: issue for issue in all_issues}
400
+
401
+ successful = 0
402
+ failed = 0
403
+
404
+ # Update and save only the issues we need to modify
405
+ for issue_id in issue_ids:
406
+ if issue_id not in issue_by_id:
407
+ failed += 1
408
+ continue
409
+
410
+ issue = issue_by_id[issue_id]
411
+ issue.milestone = milestone_name
412
+ issue.updated = now_utc()
413
+
414
+ # Save the issue
415
+ if hasattr(issue, "file_path") and issue.file_path:
416
+ issue_path = Path(issue.file_path)
417
+ else:
418
+ issue_path = self.issues_dir / issue.filename
419
+
420
+ try:
421
+ IssueParser.save_issue_file(issue, issue_path)
422
+ successful += 1
423
+ except Exception:
424
+ failed += 1
425
+
426
+ # Invalidate caches after batch assignment
427
+ if successful > 0:
428
+ self._milestone_cache.clear()
429
+
430
+ return successful, failed
431
+
432
+ def get_similar_milestone_names(
433
+ self, milestone_name: str, max_results: int = 3
434
+ ) -> list[str]:
435
+ """Find milestone names similar to the given name.
436
+
437
+ Useful for suggesting corrections when a user typos a milestone name.
438
+
439
+ Args:
440
+ milestone_name: The milestone name to find similar matches for
441
+ max_results: Maximum number of suggestions to return
442
+
443
+ Returns:
444
+ List of similar milestone names found
445
+ """
446
+ # Get all available milestone names
447
+ milestones_dir = self.issues_dir.parent / "milestones"
448
+ if not milestones_dir.exists():
449
+ return []
450
+
451
+ available_milestones = [f.stem for f in milestones_dir.glob("*.md")]
452
+
453
+ # Find close matches
454
+ return get_close_matches(
455
+ milestone_name, available_milestones, n=max_results, cutoff=0.6
456
+ )
@@ -0,0 +1,135 @@
1
+ """Milestone Coordinator - Coordinates milestone-related operations.
2
+
3
+ Extracted from RoadmapCore to reduce god object complexity.
4
+ Provides a focused API for all milestone-related concerns.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from roadmap.core.domain import Milestone
13
+ from roadmap.infrastructure.coordination.milestone_operations import MilestoneOperations
14
+ from roadmap.infrastructure.validation.milestone_consistency_validator import (
15
+ MilestoneConsistencyValidator,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from roadmap.infrastructure.coordination.core import RoadmapCore
20
+
21
+
22
+ class MilestoneCoordinator:
23
+ """Coordinates all milestone-related operations."""
24
+
25
+ def __init__(
26
+ self,
27
+ milestone_ops: MilestoneOperations,
28
+ milestones_dir,
29
+ core: RoadmapCore | None = None,
30
+ ):
31
+ """Initialize coordinator with milestone operations manager.
32
+
33
+ Args:
34
+ milestone_ops: MilestoneOperations instance
35
+ milestones_dir: Path to milestones directory
36
+ core: RoadmapCore instance for initialization checks
37
+ """
38
+ self._ops = milestone_ops
39
+ self._consistency_validator = MilestoneConsistencyValidator(milestones_dir)
40
+ self._core = core
41
+
42
+ # CRUD Operations
43
+ def create(
44
+ self,
45
+ name: str,
46
+ headline: str = "",
47
+ due_date: datetime | None = None,
48
+ status: str | None = None,
49
+ project_id: str | None = None,
50
+ ) -> Milestone:
51
+ """Create a new milestone."""
52
+ milestone = self._ops.create_milestone(
53
+ name=name, headline=headline, due_date=due_date, status=status
54
+ )
55
+
56
+ # Assign to project if provided
57
+ if project_id:
58
+ milestone.project_id = project_id
59
+
60
+ return milestone
61
+
62
+ def list(self) -> list[Milestone]:
63
+ """List all milestones."""
64
+ return self._ops.list_milestones()
65
+
66
+ def get(self, name: str) -> Milestone | None:
67
+ """Get a specific milestone by name (searches by YAML name field, not filename)."""
68
+ return self._ops.get_milestone(name)
69
+
70
+ def update(
71
+ self,
72
+ name: str,
73
+ headline: str | None = None,
74
+ due_date: datetime | None = None,
75
+ clear_due_date: bool = False,
76
+ status: str | None = None,
77
+ project_id: str | None = None,
78
+ ) -> bool:
79
+ """Update a milestone's properties.
80
+
81
+ Args:
82
+ name: Name of the milestone to update
83
+ headline: New headline (None to keep current)
84
+ due_date: New due date (None to keep current)
85
+ clear_due_date: If True, remove the due date
86
+ status: New status (None to keep current)
87
+ project_id: Project ID to assign milestone to
88
+
89
+ Returns:
90
+ True if milestone was updated, False if not found
91
+ """
92
+ return self._ops.update_milestone(
93
+ name=name,
94
+ headline=headline,
95
+ due_date=due_date,
96
+ clear_due_date=clear_due_date,
97
+ status=status,
98
+ project_id=project_id,
99
+ )
100
+
101
+ def delete(self, name: str) -> bool:
102
+ """Delete a milestone and unassign all issues from it.
103
+
104
+ Args:
105
+ name: Name of the milestone to delete
106
+
107
+ Returns:
108
+ True if milestone was deleted, False if not found
109
+ """
110
+ return self._ops.delete_milestone(name)
111
+
112
+ def get_progress(self, milestone_name: str) -> dict[str, Any]:
113
+ """Get progress statistics for a milestone."""
114
+ return self._ops.get_milestone_progress(milestone_name)
115
+
116
+ def get_next(self) -> Milestone | None:
117
+ """Get the next upcoming milestone based on due date."""
118
+ return self._ops.get_next_milestone()
119
+
120
+ # Consistency and Validation
121
+ def validate_naming_consistency(self) -> list[dict[str, str]]:
122
+ """Check for inconsistencies between milestone filenames and name fields.
123
+
124
+ Returns:
125
+ List of dictionaries with inconsistency details
126
+ """
127
+ return self._consistency_validator.validate()
128
+
129
+ def fix_naming_consistency(self) -> dict[str, list[str]]:
130
+ """Fix milestone filename inconsistencies by renaming files to match name fields.
131
+
132
+ Returns:
133
+ Dictionary with 'renamed' and 'errors' lists
134
+ """
135
+ return self._consistency_validator.fix()