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,144 @@
1
+ """Service for issue update operations.
2
+
3
+ Consolidates business logic for updating issues, including building update
4
+ dictionaries from CLI parameters, resolving assignees, and displaying results.
5
+ """
6
+
7
+ from roadmap.common.console import get_console
8
+ from roadmap.common.errors.exceptions import ValidationError
9
+ from roadmap.common.update_constants import DISPLAYABLE_UPDATE_FIELDS
10
+ from roadmap.core.domain import Priority, Status
11
+
12
+
13
+ class IssueUpdateService:
14
+ """Service for updating issues with all supporting operations."""
15
+
16
+ def __init__(self, core):
17
+ """Initialize service with core instance.
18
+
19
+ Args:
20
+ core: RoadmapCore instance
21
+ """
22
+ self.core = core
23
+ self._console = get_console()
24
+
25
+ # ─────────────────────────────────────────────────────────────────────────
26
+ # Update Dictionary Building
27
+ # ─────────────────────────────────────────────────────────────────────────
28
+
29
+ def build_update_dict(
30
+ self,
31
+ title: str | None = None,
32
+ priority: str | None = None,
33
+ status: str | None = None,
34
+ assignee: str | None = None,
35
+ milestone: str | None = None,
36
+ description: str | None = None,
37
+ estimate: float | None = None,
38
+ ) -> dict:
39
+ """Build update dictionary from provided parameters.
40
+
41
+ Validates and normalizes all update fields, resolving assignees and
42
+ converting enum strings to proper types.
43
+
44
+ Args:
45
+ title: New title (optional)
46
+ priority: New priority as string (optional)
47
+ status: New status as string (optional)
48
+ assignee: New assignee (optional)
49
+ milestone: New milestone (optional)
50
+ description: New description (optional)
51
+ estimate: New estimate in hours (optional)
52
+
53
+ Returns:
54
+ Dictionary of updates ready to apply to an issue
55
+ """
56
+ updates = {}
57
+
58
+ if title:
59
+ updates["title"] = title
60
+ if priority:
61
+ updates["priority"] = Priority(priority)
62
+ if status:
63
+ updates["status"] = Status(status)
64
+ if assignee is not None:
65
+ assignee_value = self.resolve_assignee_for_update(assignee)
66
+ updates["assignee"] = assignee_value
67
+ if milestone:
68
+ updates["milestone"] = milestone
69
+ if description:
70
+ updates["description"] = description
71
+ if estimate is not None:
72
+ updates["estimated_hours"] = estimate
73
+
74
+ return updates
75
+
76
+ # ─────────────────────────────────────────────────────────────────────────
77
+ # Assignee Resolution for Updates
78
+ # ─────────────────────────────────────────────────────────────────────────
79
+
80
+ def resolve_assignee_for_update(self, assignee: str):
81
+ """Resolve and validate assignee for update operation.
82
+
83
+ Args:
84
+ assignee: Assignee to resolve
85
+
86
+ Returns:
87
+ - Resolved assignee string
88
+ - None for unassignment (empty string)
89
+ - False if validation failed
90
+ """
91
+ # Convert empty string to None for proper unassignment
92
+ if assignee == "":
93
+ return None
94
+
95
+ # Validate assignee
96
+ is_valid, result = self.core.validate_assignee(assignee)
97
+ if not is_valid:
98
+ raise ValidationError(
99
+ domain_message=f"Assignee validation failed: {result}",
100
+ user_message=f"Invalid assignee: {result}",
101
+ )
102
+ elif result and "Warning:" in result:
103
+ self._console.print(f"⚠️ {result}", style="bold yellow")
104
+ return assignee
105
+ else:
106
+ canonical_assignee = self.core.team.get_canonical_assignee(assignee)
107
+ if canonical_assignee != assignee:
108
+ self._console.print(
109
+ f"🔄 Resolved '{assignee}' to '{canonical_assignee}'",
110
+ style="dim",
111
+ )
112
+ return canonical_assignee
113
+
114
+ # ─────────────────────────────────────────────────────────────────────────
115
+ # Display Results
116
+ # ─────────────────────────────────────────────────────────────────────────
117
+
118
+ def display_update_result(
119
+ self, updated_issue, updates: dict, reason: str | None = None
120
+ ) -> None:
121
+ """Display the results of an issue update.
122
+
123
+ Args:
124
+ updated_issue: The updated issue object
125
+ updates: Dictionary of fields that were updated
126
+ reason: Optional reason/comment for the update
127
+ """
128
+ self._console.print(
129
+ f"✅ Updated issue: {updated_issue.title}", style="bold green"
130
+ )
131
+ self._console.print(f" ID: {updated_issue.id}", style="cyan")
132
+
133
+ # Show what was updated
134
+ for field, value in updates.items():
135
+ if field == "estimated_hours":
136
+ display_value = updated_issue.estimated_time_display
137
+ self._console.print(f" estimate: {display_value}", style="cyan")
138
+ elif field in DISPLAYABLE_UPDATE_FIELDS:
139
+ # Format enum values to show just the string value
140
+ display_value = value.value if hasattr(value, "value") else value
141
+ self._console.print(f" {field}: {display_value}", style="cyan")
142
+
143
+ if reason:
144
+ self._console.print(f" reason: {reason}", style="dim")
@@ -0,0 +1,174 @@
1
+ """Service for starting work on issues.
2
+
3
+ Consolidates business logic for updating issue status, setting start dates,
4
+ and optionally creating Git branches.
5
+ """
6
+
7
+ from datetime import UTC, datetime
8
+
9
+ import structlog
10
+
11
+ from roadmap.common.console import get_console
12
+ from roadmap.core.domain import Status
13
+
14
+ logger = structlog.get_logger()
15
+
16
+
17
+ class StartIssueService:
18
+ """Service for starting work on issues."""
19
+
20
+ def __init__(self, core):
21
+ """Initialize service with core instance.
22
+
23
+ Args:
24
+ core: RoadmapCore instance
25
+ """
26
+ self.core = core
27
+ self._console = get_console()
28
+
29
+ # ─────────────────────────────────────────────────────────────────────────
30
+ # Start Date Parsing
31
+ # ─────────────────────────────────────────────────────────────────────────
32
+
33
+ @staticmethod
34
+ def parse_start_date(date_str: str | None) -> datetime:
35
+ """Parse start date from string input.
36
+
37
+ Args:
38
+ date_str: Date string in format YYYY-MM-DD or YYYY-MM-DD HH:MM
39
+
40
+ Returns:
41
+ Parsed datetime, or current time if not provided or invalid
42
+
43
+ Raises:
44
+ ValueError: If date string is invalid
45
+ """
46
+ if not date_str:
47
+ return datetime.now(UTC)
48
+
49
+ # Try full datetime format first
50
+ try:
51
+ return datetime.strptime(date_str, "%Y-%m-%d %H:%M")
52
+ except ValueError as e:
53
+ logger.debug(
54
+ "date_parse_failed",
55
+ date_str=date_str,
56
+ error=str(e),
57
+ action="parse_datetime",
58
+ )
59
+
60
+ # Try date-only format
61
+ try:
62
+ return datetime.strptime(date_str, "%Y-%m-%d")
63
+ except ValueError as e:
64
+ raise ValueError(
65
+ f"Invalid date format: {date_str}. Use YYYY-MM-DD or YYYY-MM-DD HH:MM"
66
+ ) from e
67
+
68
+ # ─────────────────────────────────────────────────────────────────────────
69
+ # Start Work Workflow
70
+ # ─────────────────────────────────────────────────────────────────────────
71
+
72
+ def start_work(self, issue_id: str, start_date: datetime) -> object | None:
73
+ """Update issue to start work.
74
+
75
+ Sets status to IN_PROGRESS, sets actual start date, and initializes
76
+ progress tracking.
77
+
78
+ Args:
79
+ issue_id: ID of issue to start
80
+ start_date: Datetime to set as start time
81
+
82
+ Returns:
83
+ Updated issue if successful, None otherwise
84
+ """
85
+ return self.core.issues.update(
86
+ issue_id,
87
+ actual_start_date=start_date,
88
+ status=Status.IN_PROGRESS,
89
+ progress_percentage=0.0,
90
+ )
91
+
92
+ # ─────────────────────────────────────────────────────────────────────────
93
+ # Branch Creation Decision
94
+ # ─────────────────────────────────────────────────────────────────────────
95
+
96
+ def should_create_branch(self, git_branch_flag: bool) -> bool:
97
+ """Determine if git branch should be created based on flag and config.
98
+
99
+ Currently only respects explicit --create-branch flag.
100
+ Auto-creation can be re-enabled via config in the future.
101
+
102
+ Args:
103
+ git_branch_flag: Whether --create-branch flag was passed
104
+
105
+ Returns:
106
+ Whether to create a branch
107
+ """
108
+ if git_branch_flag:
109
+ return True
110
+
111
+ # Check config for auto_branch default (disabled for now - can be re-enabled later)
112
+ return False
113
+
114
+ # ─────────────────────────────────────────────────────────────────────────
115
+ # Display Results
116
+ # ─────────────────────────────────────────────────────────────────────────
117
+
118
+ def display_started(self, issue, start_date: datetime) -> None:
119
+ """Display success message for started issue.
120
+
121
+ Args:
122
+ issue: Issue that was started
123
+ start_date: Datetime when work started
124
+ """
125
+ self._console.print(f"🚀 Started work on: {issue.title}", style="bold green")
126
+ self._console.print(
127
+ f" Started: {start_date.strftime('%Y-%m-%d %H:%M')}", style="cyan"
128
+ )
129
+ self._console.print(" Status: In Progress", style="yellow")
130
+
131
+ def display_branch_created(self, branch_name: str, checkout: bool) -> None:
132
+ """Display git branch creation success.
133
+
134
+ Args:
135
+ branch_name: Name of created branch
136
+ checkout: Whether branch was checked out
137
+ """
138
+ self._console.print(
139
+ f"🌿 Created Git branch: {branch_name}",
140
+ style="green",
141
+ )
142
+ if checkout:
143
+ self._console.print(
144
+ f"✅ Checked out branch: {branch_name}",
145
+ style="green",
146
+ )
147
+
148
+ def display_branch_warning(self) -> None:
149
+ """Display warning when branch creation fails."""
150
+ try:
151
+ status_output = (
152
+ self.core.git._run_git_command(["status", "--porcelain"]) or ""
153
+ )
154
+ if status_output.strip():
155
+ self._console.print(
156
+ "⚠️ Working tree has uncommitted changes — branch creation skipped. Use --force to override.",
157
+ style="yellow",
158
+ )
159
+ else:
160
+ self._console.print(
161
+ "⚠️ Failed to create or checkout branch. See git for details.",
162
+ style="yellow",
163
+ )
164
+ except Exception as e:
165
+ logger.debug(
166
+ "branch_creation_failed",
167
+ operation="create_branch",
168
+ error=str(e),
169
+ action="Showing user message",
170
+ )
171
+ self._console.print(
172
+ "⚠️ Failed to create or checkout branch. See git for details.",
173
+ style="yellow",
174
+ )
@@ -0,0 +1,18 @@
1
+ """Issue-related helper utilities for filtering, updating, and management.
2
+
3
+ DEPRECATED: This module is deprecated. Import directly from issue_filter_service instead.
4
+ Example: from roadmap.core.services.issue.issue_filter_service import IssueQueryService
5
+ """
6
+
7
+ # Backward compatibility re-exports
8
+ from roadmap.core.services.issue.issue_filter_service import (
9
+ IssueFilterValidator,
10
+ IssueQueryService,
11
+ WorkloadCalculator,
12
+ )
13
+
14
+ __all__ = [
15
+ "IssueFilterValidator",
16
+ "IssueQueryService",
17
+ "WorkloadCalculator",
18
+ ]
@@ -0,0 +1,296 @@
1
+ """Issue filtering logic extracted from complex CLI command."""
2
+
3
+ from datetime import UTC
4
+
5
+ from roadmap.core.domain import Issue, Priority, Status
6
+ from roadmap.infrastructure.coordination.core import RoadmapCore
7
+
8
+
9
+ class IssueFilterValidator:
10
+ """Validates filter combinations for issue queries."""
11
+
12
+ @staticmethod
13
+ def validate_filters(
14
+ assignee: str | None,
15
+ my_issues: bool,
16
+ backlog: bool,
17
+ unassigned: bool,
18
+ next_milestone: bool,
19
+ milestone: str | None,
20
+ ) -> tuple[bool, str | None]:
21
+ """Validate that filter combinations are valid.
22
+
23
+ Returns:
24
+ Tuple of (is_valid, error_message)
25
+ """
26
+ # Check for conflicting assignee filters
27
+ assignee_filters = [assignee is not None, my_issues]
28
+ if sum(bool(f) for f in assignee_filters) > 1:
29
+ return False, "Cannot combine --assignee and --my-issues filters"
30
+
31
+ # Check for conflicting milestone filters
32
+ exclusive_filters = [backlog, unassigned, next_milestone, milestone is not None]
33
+ if sum(bool(f) for f in exclusive_filters) > 1:
34
+ return (
35
+ False,
36
+ "Cannot combine --backlog, --unassigned, --next-milestone, and --milestone filters",
37
+ )
38
+
39
+ return True, None
40
+
41
+
42
+ class IssueQueryService:
43
+ """Service for querying issues with different filter criteria."""
44
+
45
+ def __init__(self, core: RoadmapCore):
46
+ """Initialize IssueQueryService.
47
+
48
+ Args:
49
+ core: Core roadmap instance.
50
+ """
51
+ self.core = core
52
+
53
+ def _get_issues_for_assignee_filters(
54
+ self, my_issues: bool, assignee: str | None
55
+ ) -> tuple[list[Issue], str] | None:
56
+ """Get issues for assignee-based filters. Returns None if no match."""
57
+ if my_issues:
58
+ return self.core.team.get_my_issues(), "my"
59
+ if assignee:
60
+ return self.core.team.get_assigned_issues(
61
+ assignee
62
+ ), f"assigned to {assignee}"
63
+ return None
64
+
65
+ def _get_issues_for_collection_filters(
66
+ self,
67
+ backlog: bool,
68
+ unassigned: bool,
69
+ next_milestone: bool,
70
+ milestone: str | None,
71
+ ) -> tuple[list[Issue], str] | None:
72
+ """Get issues for collection-based filters. Returns None if no match."""
73
+ if backlog or unassigned:
74
+ return self.core.issues.get_backlog(), "backlog"
75
+
76
+ if next_milestone:
77
+ next_ms = self.core.milestones.get_next()
78
+ if not next_ms:
79
+ return [], "" # Handled by caller
80
+ return (
81
+ self.core.issues.get_by_milestone(next_ms.name),
82
+ f"next milestone ({next_ms.name})",
83
+ )
84
+
85
+ if milestone:
86
+ return (
87
+ self.core.issues.get_by_milestone(milestone),
88
+ f"milestone '{milestone}'",
89
+ )
90
+ return None
91
+
92
+ def _get_overdue_issues(self) -> tuple[list[Issue], str]:
93
+ """Get overdue issues."""
94
+ from datetime import datetime
95
+
96
+ all_issues = self.core.issues.list()
97
+ now = datetime.now(UTC)
98
+ overdue_issues: list[Issue] = []
99
+ for issue in all_issues:
100
+ if not issue.due_date:
101
+ continue
102
+
103
+ due = issue.due_date
104
+ if isinstance(due, datetime):
105
+ if due.tzinfo:
106
+ due_aware = due.astimezone(UTC)
107
+ else:
108
+ due_aware = due.replace(tzinfo=UTC)
109
+ else:
110
+ due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
111
+
112
+ if due_aware < now:
113
+ overdue_issues.append(issue)
114
+
115
+ return overdue_issues, "overdue"
116
+
117
+ def get_filtered_issues(
118
+ self,
119
+ milestone: str | None = None,
120
+ backlog: bool = False,
121
+ unassigned: bool = False,
122
+ next_milestone: bool = False,
123
+ assignee: str | None = None,
124
+ my_issues: bool = False,
125
+ overdue: bool = False,
126
+ ) -> tuple[list[Issue], str]:
127
+ """Get issues based on primary filter criteria.
128
+
129
+ Returns:
130
+ Tuple of (issues, filter_description)
131
+ """
132
+ # Try assignee filters first
133
+ result = self._get_issues_for_assignee_filters(my_issues, assignee)
134
+ if result is not None:
135
+ return result
136
+
137
+ # Try collection filters
138
+ result = self._get_issues_for_collection_filters(
139
+ backlog, unassigned, next_milestone, milestone
140
+ )
141
+ if result is not None:
142
+ return result
143
+
144
+ # Handle overdue filter
145
+ if overdue:
146
+ return self._get_overdue_issues()
147
+
148
+ # Show all issues
149
+ return self.core.issues.list(), "all"
150
+
151
+ def apply_additional_filters(
152
+ self,
153
+ issues: list[Issue],
154
+ filter_description: str,
155
+ open_only: bool = False,
156
+ blocked_only: bool = False,
157
+ status: str | None = None,
158
+ priority: str | None = None,
159
+ issue_type: str | None = None,
160
+ overdue: bool = False,
161
+ ) -> tuple[list[Issue], str]:
162
+ """Apply additional filters to issue list.
163
+
164
+ Returns:
165
+ Tuple of (filtered_issues, updated_description)
166
+ """
167
+ result = issues
168
+ description = filter_description
169
+
170
+ # Apply each filter in sequence
171
+ if open_only:
172
+ result, description = self._filter_by_open_status(result, description)
173
+
174
+ if blocked_only:
175
+ result, description = self._filter_by_blocked_status(result, description)
176
+
177
+ if status:
178
+ result, description = self._filter_by_status(result, description, status)
179
+
180
+ if priority:
181
+ result, description = self._filter_by_priority(
182
+ result, description, priority
183
+ )
184
+
185
+ if issue_type:
186
+ result, description = self._filter_by_type(result, description, issue_type)
187
+
188
+ if overdue:
189
+ result, description = self._filter_by_overdue(result, description)
190
+
191
+ return result, description
192
+
193
+ @staticmethod
194
+ def _filter_by_open_status(
195
+ issues: list[Issue], description: str
196
+ ) -> tuple[list[Issue], str]:
197
+ """Filter to only open issues (not closed)."""
198
+ filtered = [i for i in issues if i.status != Status.CLOSED]
199
+ return filtered, description + " open"
200
+
201
+ @staticmethod
202
+ def _filter_by_blocked_status(
203
+ issues: list[Issue], description: str
204
+ ) -> tuple[list[Issue], str]:
205
+ """Filter to only blocked issues."""
206
+ filtered = [i for i in issues if i.status == Status.BLOCKED]
207
+ return filtered, description + " blocked"
208
+
209
+ @staticmethod
210
+ def _filter_by_status(
211
+ issues: list[Issue], description: str, status: str
212
+ ) -> tuple[list[Issue], str]:
213
+ """Filter by specific status."""
214
+ filtered = [i for i in issues if i.status == Status(status)]
215
+ return filtered, description + f" {status}"
216
+
217
+ @staticmethod
218
+ def _filter_by_priority(
219
+ issues: list[Issue], description: str, priority: str
220
+ ) -> tuple[list[Issue], str]:
221
+ """Filter by priority level."""
222
+ filtered = [i for i in issues if i.priority == Priority(priority)]
223
+ return filtered, description + f" {priority} priority"
224
+
225
+ @staticmethod
226
+ def _filter_by_type(
227
+ issues: list[Issue], description: str, issue_type: str
228
+ ) -> tuple[list[Issue], str]:
229
+ """Filter by issue type."""
230
+ from roadmap.core.domain import IssueType
231
+
232
+ filtered = [i for i in issues if i.issue_type == IssueType(issue_type)]
233
+ return filtered, description + f" {issue_type}"
234
+
235
+ @staticmethod
236
+ def _filter_by_overdue(
237
+ issues: list[Issue], description: str
238
+ ) -> tuple[list[Issue], str]:
239
+ """Filter to only overdue issues."""
240
+ from datetime import datetime
241
+
242
+ now = datetime.now(UTC)
243
+ filtered: list[Issue] = []
244
+ for i in issues:
245
+ if not i.due_date:
246
+ continue
247
+
248
+ due = i.due_date
249
+ if isinstance(due, datetime):
250
+ if due.tzinfo:
251
+ due_aware = due.astimezone(UTC)
252
+ else:
253
+ due_aware = due.replace(tzinfo=UTC)
254
+ else:
255
+ due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
256
+
257
+ if due_aware < now:
258
+ filtered.append(i)
259
+
260
+ return filtered, description + " overdue"
261
+
262
+
263
+ class WorkloadCalculator:
264
+ """Calculates time estimates and workload breakdowns."""
265
+
266
+ @staticmethod
267
+ def calculate_workload(issues: list[Issue]) -> dict:
268
+ """Calculate workload statistics for a list of issues.
269
+
270
+ Returns:
271
+ Dictionary with total_hours, status_breakdown
272
+ """
273
+ total_hours = sum(issue.estimated_hours or 0 for issue in issues)
274
+
275
+ status_counts = {}
276
+ for issue in issues:
277
+ status = issue.status.value
278
+ if status not in status_counts:
279
+ status_counts[status] = {"count": 0, "hours": 0}
280
+ status_counts[status]["count"] += 1
281
+ status_counts[status]["hours"] += issue.estimated_hours or 0
282
+
283
+ return {
284
+ "total_hours": total_hours,
285
+ "status_breakdown": status_counts,
286
+ }
287
+
288
+ @staticmethod
289
+ def format_time_display(hours: float) -> str:
290
+ """Format time in hours to human-readable display."""
291
+ if hours < 1:
292
+ return f"{hours * 60:.0f}m"
293
+ elif hours <= 24:
294
+ return f"{hours:.1f}h"
295
+ else:
296
+ return f"{hours / 8:.1f}d"