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,309 @@
1
+ """Service for issue creation operations.
2
+
3
+ Consolidates business logic for creating issues, including assignee resolution,
4
+ Git branch creation, and formatted display of created issues.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ from typing import Any
10
+
11
+ import click
12
+ import structlog
13
+
14
+ from roadmap.common.console import get_console
15
+
16
+ logger = structlog.get_logger()
17
+
18
+
19
+ class IssueCreationService:
20
+ """Service for creating issues with all supporting operations."""
21
+
22
+ def __init__(self, core):
23
+ """Initialize service with core instance.
24
+
25
+ Args:
26
+ core: RoadmapCore instance
27
+ """
28
+ self.core = core
29
+ self._console = get_console()
30
+
31
+ # ─────────────────────────────────────────────────────────────────────────
32
+ # Assignee Resolution
33
+ # ─────────────────────────────────────────────────────────────────────────
34
+
35
+ def resolve_and_validate_assignee(
36
+ self, assignee: str | None = None, auto_detect: bool = True
37
+ ) -> str | None:
38
+ """Resolve and validate an assignee.
39
+
40
+ Auto-detects from Git if not provided. Validates against team members
41
+ and returns canonical name.
42
+
43
+ Args:
44
+ assignee: Assignee name to resolve (optional)
45
+ auto_detect: Whether to auto-detect from Git if not provided
46
+
47
+ Returns:
48
+ Canonical assignee name or None if not assigned
49
+
50
+ Raises:
51
+ click.Abort: If assignee validation fails
52
+ """
53
+ # Track whether assignee was explicitly provided (for validation logic)
54
+ explicitly_provided = assignee is not None
55
+
56
+ # Auto-detect from Git if not provided
57
+ if not assignee and auto_detect:
58
+ git_user = self.core.git.get_current_user()
59
+ if git_user:
60
+ self._console.print(
61
+ f"🔍 Auto-detected assignee from Git: {git_user}", style="dim"
62
+ )
63
+ assignee = git_user
64
+
65
+ if not assignee:
66
+ return None
67
+
68
+ # Only validate if explicitly provided (not auto-detected)
69
+ if explicitly_provided:
70
+ is_valid, result = self.core.team.validate_assignee(assignee)
71
+ if not is_valid:
72
+ self._console.print(f"[ERROR] Invalid assignee: {result}")
73
+ raise click.Abort()
74
+
75
+ if result and "Warning:" in result:
76
+ self._console.print(f"⚠️ {result}", style="bold yellow")
77
+ return assignee
78
+
79
+ # Get canonical name
80
+ canonical = self.core.team.get_canonical_assignee(assignee)
81
+ if canonical != assignee:
82
+ self._console.print(
83
+ f"🔄 Resolved '{assignee}' to '{canonical}'", style="dim"
84
+ )
85
+
86
+ return canonical
87
+ else:
88
+ # Auto-detected assignee - use as-is without validation
89
+ return assignee
90
+
91
+ # ─────────────────────────────────────────────────────────────────────────
92
+ # Git Branch Creation
93
+ # ─────────────────────────────────────────────────────────────────────────
94
+
95
+ def create_branch_for_issue(
96
+ self,
97
+ issue,
98
+ branch_name: str | None = None,
99
+ checkout: bool = True,
100
+ force: bool = False,
101
+ ) -> tuple[bool, str | None]:
102
+ """Create a Git branch for an issue.
103
+
104
+ Attempts multiple strategies to create and checkout the branch, with
105
+ fallbacks for various failure scenarios.
106
+
107
+ Args:
108
+ issue: Issue object to create branch for
109
+ branch_name: Explicit branch name (optional)
110
+ checkout: Whether to checkout the branch
111
+ force: Whether to force override uncommitted changes
112
+
113
+ Returns:
114
+ Tuple of (success: bool, branch_name: Optional[str])
115
+ """
116
+ if not hasattr(self.core, "git") or not self.core.git.is_git_repository():
117
+ self._console.print(
118
+ "⚠️ Not in a Git repository, skipping branch creation", style="yellow"
119
+ )
120
+ return False, None
121
+
122
+ # Determine branch name
123
+ resolved_name = branch_name or self.core.git.suggest_branch_name(issue.id)
124
+
125
+ # Try primary method
126
+ if self._try_safe_create_branch(issue, checkout, force):
127
+ self._show_branch_success_message(resolved_name, checkout)
128
+ return True, resolved_name
129
+
130
+ # Check for uncommitted changes
131
+ if self._has_uncommitted_changes():
132
+ self._console.print(
133
+ "⚠️ Working tree has uncommitted changes — branch creation skipped. Use --force to override.",
134
+ style="yellow",
135
+ )
136
+ return False, resolved_name
137
+
138
+ # Try fallback strategies
139
+ if self._try_direct_git_command(resolved_name, checkout):
140
+ return True, resolved_name
141
+
142
+ if self._try_subprocess_git(resolved_name, checkout):
143
+ return True, resolved_name
144
+
145
+ # All strategies failed
146
+ self._console.print(
147
+ "⚠️ Failed to create or checkout branch. See git for details.",
148
+ style="yellow",
149
+ )
150
+ return False, resolved_name
151
+
152
+ def _try_safe_create_branch(self, issue, checkout: bool, force: bool) -> bool:
153
+ """Try creating branch using core.git method."""
154
+ try:
155
+ return self.core.git.create_branch_for_issue(
156
+ issue, checkout=checkout, force=force
157
+ )
158
+ except TypeError:
159
+ # Try without force parameter
160
+ try:
161
+ return self.core.git.create_branch_for_issue(issue, checkout=checkout)
162
+ except TypeError:
163
+ # Try with just issue
164
+ try:
165
+ return self.core.git.create_branch_for_issue(issue)
166
+ except Exception as e:
167
+ logger.debug(
168
+ "create_branch_fallback_failed",
169
+ operation="create_branch_for_issue",
170
+ error=str(e),
171
+ action="Returning False",
172
+ )
173
+ return False
174
+
175
+ def _has_uncommitted_changes(self) -> bool:
176
+ """Check if there are uncommitted changes."""
177
+ try:
178
+ status_output = (
179
+ self.core.git._run_git_command(["status", "--porcelain"]) or ""
180
+ )
181
+ return bool(status_output.strip())
182
+ except Exception as e:
183
+ logger.debug(
184
+ "git_status_check_failed",
185
+ operation="check_git_status",
186
+ error=str(e),
187
+ action="Assuming no changes",
188
+ )
189
+ return False
190
+
191
+ def _try_direct_git_command(self, branch_name: str, checkout: bool) -> bool:
192
+ """Try creating branch using direct git command via core.git."""
193
+ try:
194
+ result = self.core.git._run_git_command(["checkout", "-b", branch_name])
195
+ if result is not None:
196
+ self._show_branch_success_message(branch_name, checkout)
197
+ return True
198
+
199
+ # Check if branch exists
200
+ exists = self.core.git._run_git_command(
201
+ ["rev-parse", "--verify", branch_name]
202
+ )
203
+ if exists:
204
+ self._show_branch_success_message(branch_name, checkout)
205
+ return True
206
+ except Exception as e:
207
+ logger.debug(
208
+ "direct_git_command_failed",
209
+ operation="try_direct_git_command",
210
+ branch=branch_name,
211
+ error=str(e),
212
+ action="Falling back to subprocess",
213
+ )
214
+ return False
215
+
216
+ def _try_subprocess_git(self, branch_name: str, checkout: bool) -> bool:
217
+ """Try creating branch using subprocess as last resort."""
218
+ try:
219
+ cwd = getattr(self.core, "root_path", None) or os.getcwd()
220
+ subprocess.run(
221
+ ["git", "checkout", "-b", branch_name],
222
+ cwd=cwd,
223
+ check=True,
224
+ capture_output=True,
225
+ text=True,
226
+ )
227
+ self._show_branch_success_message(branch_name, checkout)
228
+ return True
229
+ except Exception as e:
230
+ logger.debug(
231
+ "branch_creation_failed",
232
+ branch_name=branch_name,
233
+ error=str(e),
234
+ action="create_branch",
235
+ )
236
+ return False
237
+
238
+ def _show_branch_success_message(self, branch_name: str, checkout: bool) -> None:
239
+ """Display success message for branch creation."""
240
+ self._console.print(f"🌿 Created Git branch: {branch_name}", style="green")
241
+ if checkout:
242
+ self._console.print(f"✅ Checked out branch: {branch_name}", style="green")
243
+
244
+ # ─────────────────────────────────────────────────────────────────────────
245
+ # Display Formatting
246
+ # ─────────────────────────────────────────────────────────────────────────
247
+
248
+ def format_created_issue_display(self, issue, milestone: str | None = None) -> None:
249
+ """Display formatted information about a newly created issue.
250
+
251
+ Args:
252
+ issue: Created issue object
253
+ milestone: Optional milestone name
254
+ """
255
+ self._console.print(f"✅ Created issue: {issue.title}", style="bold green")
256
+ self._console.print(f" ID: {issue.id}", style="bold cyan")
257
+ self._console.print(f" Type: {issue.issue_type.value.title()}", style="blue")
258
+ self._console.print(f" Priority: {issue.priority.value}", style="yellow")
259
+
260
+ if milestone:
261
+ self._console.print(f" Milestone: {milestone}", style="blue")
262
+
263
+ if self._should_show_assignee(issue.assignee):
264
+ self._console.print(f" Assignee: {issue.assignee}", style="magenta")
265
+
266
+ self._print_estimate_if_present(issue)
267
+ self._print_dependency_list(
268
+ issue, field_name="depends_on", label="Depends on", style="orange1"
269
+ )
270
+ self._print_dependency_list(
271
+ issue, field_name="blocks", label="Blocks", style="red1"
272
+ )
273
+
274
+ if hasattr(issue, "filename"):
275
+ self._console.print(
276
+ f" File: .roadmap/issues/{issue.filename}", style="dim"
277
+ )
278
+
279
+ def _should_show_assignee(self, assignee: object) -> bool:
280
+ """Return True when assignee should be displayed in success output."""
281
+ if not assignee:
282
+ return False
283
+ assignee_text = str(assignee)
284
+ return "<Mock" not in assignee_text
285
+
286
+ def _print_estimate_if_present(self, issue: Any) -> None:
287
+ """Print estimated effort if issue contains estimate information."""
288
+ if hasattr(issue, "estimated_hours") and issue.estimated_hours:
289
+ self._console.print(
290
+ f" Estimated: {issue.estimated_time_display}", style="green"
291
+ )
292
+
293
+ def _print_dependency_list(
294
+ self,
295
+ issue: Any,
296
+ *,
297
+ field_name: str,
298
+ label: str,
299
+ style: str,
300
+ ) -> None:
301
+ """Print a dependency list field when present and well-formed."""
302
+ values = getattr(issue, field_name, None)
303
+ if not values:
304
+ return
305
+ try:
306
+ if isinstance(values, list | tuple):
307
+ self._console.print(f" {label}: {', '.join(values)}", style=style)
308
+ except (TypeError, AttributeError):
309
+ return
@@ -0,0 +1,302 @@
1
+ """Issue filtering service for issue queries and filtering.
2
+
3
+ Provides utilities for filtering issues based on various criteria and calculating workload.
4
+ """
5
+
6
+ from datetime import UTC
7
+ from typing import TYPE_CHECKING
8
+
9
+ from roadmap.core.domain import Issue, Priority, Status
10
+
11
+ if TYPE_CHECKING:
12
+ from roadmap.infrastructure.coordination.core import RoadmapCore # noqa: F401
13
+
14
+
15
+ class IssueFilterValidator:
16
+ """Validates filter combinations for issue queries."""
17
+
18
+ @staticmethod
19
+ def validate_filters(
20
+ assignee: str | None,
21
+ my_issues: bool,
22
+ backlog: bool,
23
+ unassigned: bool,
24
+ next_milestone: bool,
25
+ milestone: str | None,
26
+ ) -> tuple[bool, str | None]:
27
+ """Validate that filter combinations are valid.
28
+
29
+ Returns:
30
+ Tuple of (is_valid, error_message)
31
+ """
32
+ # Check for conflicting assignee filters
33
+ assignee_filters = [assignee is not None, my_issues]
34
+ if sum(bool(f) for f in assignee_filters) > 1:
35
+ return False, "Cannot combine --assignee and --my-issues filters"
36
+
37
+ # Check for conflicting milestone filters
38
+ exclusive_filters = [backlog, unassigned, next_milestone, milestone is not None]
39
+ if sum(bool(f) for f in exclusive_filters) > 1:
40
+ return (
41
+ False,
42
+ "Cannot combine --backlog, --unassigned, --next-milestone, and --milestone filters",
43
+ )
44
+
45
+ return True, None
46
+
47
+
48
+ class IssueQueryService:
49
+ """Service for querying issues with different filter criteria."""
50
+
51
+ def __init__(self, core: "RoadmapCore"): # noqa: F821
52
+ """Initialize IssueQueryService.
53
+
54
+ Args:
55
+ core: Core roadmap instance.
56
+ """
57
+ self.core = core
58
+
59
+ def _get_issues_for_assignee_filters(
60
+ self, my_issues: bool, assignee: str | None
61
+ ) -> tuple[list[Issue], str] | None:
62
+ """Get issues for assignee-based filters. Returns None if no match."""
63
+ if my_issues:
64
+ return self.core.team.get_my_issues(), "my"
65
+ if assignee:
66
+ return self.core.team.get_assigned_issues(
67
+ assignee
68
+ ), f"assigned to {assignee}"
69
+ return None
70
+
71
+ def _get_issues_for_collection_filters(
72
+ self,
73
+ backlog: bool,
74
+ unassigned: bool,
75
+ next_milestone: bool,
76
+ milestone: str | None,
77
+ ) -> tuple[list[Issue], str] | None:
78
+ """Get issues for collection-based filters. Returns None if no match."""
79
+ if backlog or unassigned:
80
+ return self.core.issues.get_backlog(), "backlog"
81
+
82
+ if next_milestone:
83
+ next_ms = self.core.milestones.get_next()
84
+ if not next_ms:
85
+ return [], "" # Handled by caller
86
+ return (
87
+ self.core.issues.get_by_milestone(next_ms.name),
88
+ f"next milestone ({next_ms.name})",
89
+ )
90
+
91
+ if milestone:
92
+ return (
93
+ self.core.issues.get_by_milestone(milestone),
94
+ f"milestone '{milestone}'",
95
+ )
96
+ return None
97
+
98
+ def _get_overdue_issues(self) -> tuple[list[Issue], str]:
99
+ """Get overdue issues."""
100
+ from datetime import datetime
101
+
102
+ all_issues = self.core.issues.list()
103
+ now = datetime.now(UTC)
104
+ overdue_issues: list[Issue] = []
105
+ for issue in all_issues:
106
+ if not issue.due_date:
107
+ continue
108
+
109
+ due = issue.due_date
110
+ if isinstance(due, datetime):
111
+ if due.tzinfo:
112
+ due_aware = due.astimezone(UTC)
113
+ else:
114
+ due_aware = due.replace(tzinfo=UTC)
115
+ else:
116
+ due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
117
+
118
+ if due_aware < now:
119
+ overdue_issues.append(issue)
120
+
121
+ return overdue_issues, "overdue"
122
+
123
+ def get_filtered_issues(
124
+ self,
125
+ milestone: str | None = None,
126
+ backlog: bool = False,
127
+ unassigned: bool = False,
128
+ next_milestone: bool = False,
129
+ assignee: str | None = None,
130
+ my_issues: bool = False,
131
+ overdue: bool = False,
132
+ ) -> tuple[list[Issue], str]:
133
+ """Get issues based on primary filter criteria.
134
+
135
+ Returns:
136
+ Tuple of (issues, filter_description)
137
+ """
138
+ # Try assignee filters first
139
+ result = self._get_issues_for_assignee_filters(my_issues, assignee)
140
+ if result is not None:
141
+ return result
142
+
143
+ # Try collection filters
144
+ result = self._get_issues_for_collection_filters(
145
+ backlog, unassigned, next_milestone, milestone
146
+ )
147
+ if result is not None:
148
+ return result
149
+
150
+ # Handle overdue filter
151
+ if overdue:
152
+ return self._get_overdue_issues()
153
+
154
+ # Show all issues
155
+ return self.core.issues.list(), "all"
156
+
157
+ def apply_additional_filters(
158
+ self,
159
+ issues: list[Issue],
160
+ filter_description: str,
161
+ open_only: bool = False,
162
+ blocked_only: bool = False,
163
+ status: str | None = None,
164
+ priority: str | None = None,
165
+ issue_type: str | None = None,
166
+ overdue: bool = False,
167
+ ) -> tuple[list[Issue], str]:
168
+ """Apply additional filters to issue list.
169
+
170
+ Returns:
171
+ Tuple of (filtered_issues, updated_description)
172
+ """
173
+ result = issues
174
+ description = filter_description
175
+
176
+ # Apply each filter in sequence
177
+ if open_only:
178
+ result, description = self._filter_by_open_status(result, description)
179
+
180
+ if blocked_only:
181
+ result, description = self._filter_by_blocked_status(result, description)
182
+
183
+ if status:
184
+ result, description = self._filter_by_status(result, description, status)
185
+
186
+ if priority:
187
+ result, description = self._filter_by_priority(
188
+ result, description, priority
189
+ )
190
+
191
+ if issue_type:
192
+ result, description = self._filter_by_type(result, description, issue_type)
193
+
194
+ if overdue:
195
+ result, description = self._filter_by_overdue(result, description)
196
+
197
+ return result, description
198
+
199
+ @staticmethod
200
+ def _filter_by_open_status(
201
+ issues: list[Issue], description: str
202
+ ) -> tuple[list[Issue], str]:
203
+ """Filter to only open issues (not closed)."""
204
+ filtered = [i for i in issues if i.status != Status.CLOSED]
205
+ return filtered, description + " open"
206
+
207
+ @staticmethod
208
+ def _filter_by_blocked_status(
209
+ issues: list[Issue], description: str
210
+ ) -> tuple[list[Issue], str]:
211
+ """Filter to only blocked issues."""
212
+ filtered = [i for i in issues if i.status == Status.BLOCKED]
213
+ return filtered, description + " blocked"
214
+
215
+ @staticmethod
216
+ def _filter_by_status(
217
+ issues: list[Issue], description: str, status: str
218
+ ) -> tuple[list[Issue], str]:
219
+ """Filter by specific status."""
220
+ filtered = [i for i in issues if i.status == Status(status)]
221
+ return filtered, description + f" {status}"
222
+
223
+ @staticmethod
224
+ def _filter_by_priority(
225
+ issues: list[Issue], description: str, priority: str
226
+ ) -> tuple[list[Issue], str]:
227
+ """Filter by priority level."""
228
+ filtered = [i for i in issues if i.priority == Priority(priority)]
229
+ return filtered, description + f" {priority} priority"
230
+
231
+ @staticmethod
232
+ def _filter_by_type(
233
+ issues: list[Issue], description: str, issue_type: str
234
+ ) -> tuple[list[Issue], str]:
235
+ """Filter by issue type."""
236
+ from roadmap.core.domain import IssueType
237
+
238
+ filtered = [i for i in issues if i.issue_type == IssueType(issue_type)]
239
+ return filtered, description + f" {issue_type}"
240
+
241
+ @staticmethod
242
+ def _filter_by_overdue(
243
+ issues: list[Issue], description: str
244
+ ) -> tuple[list[Issue], str]:
245
+ """Filter to only overdue issues."""
246
+ from datetime import datetime
247
+
248
+ now = datetime.now(UTC)
249
+ filtered: list[Issue] = []
250
+ for i in issues:
251
+ if not i.due_date:
252
+ continue
253
+
254
+ due = i.due_date
255
+ if isinstance(due, datetime):
256
+ if due.tzinfo:
257
+ due_aware = due.astimezone(UTC)
258
+ else:
259
+ due_aware = due.replace(tzinfo=UTC)
260
+ else:
261
+ due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
262
+
263
+ if due_aware < now:
264
+ filtered.append(i)
265
+
266
+ return filtered, description + " overdue"
267
+
268
+
269
+ class WorkloadCalculator:
270
+ """Calculates time estimates and workload breakdowns."""
271
+
272
+ @staticmethod
273
+ def calculate_workload(issues: list[Issue]) -> dict:
274
+ """Calculate workload statistics for a list of issues.
275
+
276
+ Returns:
277
+ Dictionary with total_hours, status_breakdown
278
+ """
279
+ total_hours = sum(issue.estimated_hours or 0 for issue in issues)
280
+
281
+ status_counts = {}
282
+ for issue in issues:
283
+ status = issue.status.value
284
+ if status not in status_counts:
285
+ status_counts[status] = {"count": 0, "hours": 0}
286
+ status_counts[status]["count"] += 1
287
+ status_counts[status]["hours"] += issue.estimated_hours or 0
288
+
289
+ return {
290
+ "total_hours": total_hours,
291
+ "status_breakdown": status_counts,
292
+ }
293
+
294
+ @staticmethod
295
+ def format_time_display(hours: float) -> str:
296
+ """Format time in hours to human-readable display."""
297
+ if hours < 1:
298
+ return f"{hours * 60:.0f}m"
299
+ elif hours <= 24:
300
+ return f"{hours:.1f}h"
301
+ else:
302
+ return f"{hours / 8:.1f}d"