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,137 @@
1
+ """Archive issue command - move completed issues to archive."""
2
+
3
+ import click
4
+
5
+ from roadmap.adapters.cli.cli_command_helpers import require_initialized
6
+ from roadmap.adapters.cli.issues.archive_class import IssueArchive
7
+ from roadmap.common.console import get_console
8
+ from roadmap.common.logging import (
9
+ log_command,
10
+ verbose_output,
11
+ )
12
+
13
+
14
+ @click.command()
15
+ @click.argument("issue_id", required=False)
16
+ @click.option(
17
+ "--all-closed",
18
+ is_flag=True,
19
+ help="Archive all closed issues",
20
+ )
21
+ @click.option(
22
+ "--orphaned",
23
+ is_flag=True,
24
+ help="Archive issues with no milestone assigned",
25
+ )
26
+ @click.option(
27
+ "--list",
28
+ "list_archived",
29
+ is_flag=True,
30
+ help="List archived issues",
31
+ )
32
+ @click.option(
33
+ "--dry-run",
34
+ is_flag=True,
35
+ help="Preview what would be archived without actually doing it",
36
+ )
37
+ @click.option(
38
+ "--force",
39
+ is_flag=True,
40
+ help="Skip confirmation prompt",
41
+ )
42
+ @click.option(
43
+ "--verbose",
44
+ "-v",
45
+ is_flag=True,
46
+ help="Show detailed debug information",
47
+ )
48
+ @click.pass_context
49
+ @verbose_output
50
+ @log_command("issue_archive", entity_type="issue", track_duration=True)
51
+ @require_initialized
52
+ def archive_issue(
53
+ ctx: click.Context,
54
+ issue_id: str | None,
55
+ all_closed: bool,
56
+ orphaned: bool,
57
+ list_archived: bool,
58
+ dry_run: bool,
59
+ force: bool,
60
+ verbose: bool, # noqa: F841
61
+ ):
62
+ """Archive an issue by moving it to .roadmap/archive/issues/.
63
+
64
+ This is a non-destructive operation that preserves issue data while
65
+ cleaning up the active workspace. Archived issues can be restored
66
+ if needed.
67
+
68
+ Examples:
69
+ roadmap issue archive 8a00a17e
70
+ roadmap issue archive --all-closed
71
+ roadmap issue archive --orphaned --dry-run
72
+ roadmap issue archive --list
73
+ """
74
+ core = ctx.obj["core"]
75
+ console = get_console()
76
+
77
+ archive = IssueArchive(core, console)
78
+
79
+ if list_archived:
80
+ # List archived issues
81
+ from pathlib import Path
82
+
83
+ from roadmap.adapters.persistence.parser import IssueParser
84
+
85
+ roadmap_dir = Path.cwd() / ".roadmap"
86
+ archive_dir = roadmap_dir / "archive" / "issues"
87
+
88
+ if not archive_dir.exists():
89
+ console.print("📋 No archived issues.", style="yellow")
90
+ return
91
+
92
+ archived_files = list(archive_dir.rglob("*.md"))
93
+ if not archived_files:
94
+ console.print("📋 No archived issues.", style="yellow")
95
+ return
96
+
97
+ console.print("\n📦 Archived Issues:\n", style="bold blue")
98
+ for file_path in sorted(archived_files):
99
+ try:
100
+ issue = IssueParser.parse_issue_file(file_path)
101
+ milestone = issue.milestone or "No milestone"
102
+ console.print(
103
+ f" • {issue.id[:8]} - {issue.title} [{milestone}] ({issue.status.value})",
104
+ style="cyan",
105
+ )
106
+ except Exception:
107
+ console.print(
108
+ f" • {file_path.name} (error reading file)", style="yellow"
109
+ )
110
+ return
111
+
112
+ # Validate arguments
113
+ if not issue_id and not all_closed and not orphaned:
114
+ console.print(
115
+ "❌ Error: Specify an issue ID, --all-closed, or --orphaned",
116
+ style="bold red",
117
+ )
118
+ ctx.exit(1)
119
+
120
+ if sum([bool(issue_id), all_closed, orphaned]) > 1:
121
+ console.print(
122
+ "❌ Error: Specify only one of: issue ID, --all-closed, or --orphaned",
123
+ style="bold red",
124
+ )
125
+ ctx.exit(1)
126
+
127
+ try:
128
+ archive.execute(
129
+ entity_id=issue_id,
130
+ all_closed=all_closed,
131
+ orphaned=orphaned,
132
+ dry_run=dry_run,
133
+ force=force,
134
+ )
135
+ except Exception as e:
136
+ console.print(f"❌ Archive operation failed: {str(e)}", style="bold red")
137
+ ctx.exit(1)
@@ -0,0 +1,253 @@
1
+ """IssueArchive command implementation using base class."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from roadmap.adapters.cli.crud import BaseArchive, EntityType
7
+
8
+
9
+ class IssueArchive(BaseArchive):
10
+ """Archive issue command implementation."""
11
+
12
+ entity_type = EntityType.ISSUE
13
+
14
+ def get_entities_to_archive(
15
+ self,
16
+ entity_id: str | None = None,
17
+ all_closed: bool = False,
18
+ orphaned: bool = False,
19
+ **kwargs,
20
+ ) -> list[Any]:
21
+ """Get issues to archive based on criteria.
22
+
23
+ Args:
24
+ entity_id: Specific issue ID to archive
25
+ all_closed: Archive all closed issues
26
+ orphaned: Archive issues with no milestone
27
+ **kwargs: Additional arguments
28
+
29
+ Returns:
30
+ List of issues to archive
31
+ """
32
+ all_issues = self.core.issues.list()
33
+
34
+ if entity_id:
35
+ issue = self.core.issues.get(entity_id)
36
+ return [issue] if issue else []
37
+ elif all_closed:
38
+ return [i for i in all_issues if i.status.value == "closed"]
39
+ elif orphaned:
40
+ return [i for i in all_issues if not i.milestone]
41
+
42
+ return []
43
+
44
+ def validate_entity_before_archive(
45
+ self, entity: Any, **kwargs
46
+ ) -> tuple[bool, str | None]:
47
+ """Validate issue before archiving.
48
+
49
+ Args:
50
+ entity: The issue to validate
51
+ **kwargs: Additional arguments (e.g., force flag)
52
+
53
+ Returns:
54
+ Tuple of (is_valid, error_message)
55
+ """
56
+ # Allow archiving if force flag is set
57
+ if kwargs.get("force"):
58
+ return True, None
59
+
60
+ if entity.status.value != "closed":
61
+ return False, f"not closed (status: {entity.status.value})"
62
+ return True, None
63
+
64
+ def find_entity_files(self, entities: list[Any]) -> list[Path]:
65
+ """Find issue files for entities.
66
+
67
+ Args:
68
+ entities: List of issues
69
+
70
+ Returns:
71
+ List of file paths
72
+ """
73
+ roadmap_dir = Path.cwd() / ".roadmap"
74
+ issues_dir = roadmap_dir / "issues"
75
+
76
+ files = []
77
+ for entity in entities:
78
+ # Search recursively for issue file by ID prefix
79
+ matching = list(issues_dir.rglob(f"{str(entity.id)[:8]}*.md"))
80
+ if matching:
81
+ files.append(matching[0])
82
+ return files
83
+
84
+ def _determine_archive_path(self, archive_dir: Path, issue_file: Path) -> Path:
85
+ """Determine archive path preserving folder structure.
86
+
87
+ Args:
88
+ archive_dir: The archive root directory
89
+ issue_file: The issue file path
90
+
91
+ Returns:
92
+ The destination path for archiving
93
+ """
94
+ roadmap_dir = Path.cwd() / ".roadmap"
95
+ issues_dir = roadmap_dir / "issues"
96
+
97
+ try:
98
+ rel_path = issue_file.relative_to(issues_dir)
99
+ except ValueError as e:
100
+ from roadmap.common.logging import get_logger
101
+
102
+ logger = get_logger(__name__)
103
+ logger.debug(
104
+ "issue_relative_path_failed",
105
+ file=str(issue_file),
106
+ error=str(e),
107
+ action="calculate_archive_path",
108
+ )
109
+ return archive_dir / issue_file.name
110
+
111
+ # If file is directly in issues_dir (no parent folder), put in archive root
112
+ if len(rel_path.parts) == 1:
113
+ return archive_dir / issue_file.name
114
+
115
+ # Preserve the parent folder structure
116
+ dest_dir = archive_dir / rel_path.parent
117
+ dest_dir.mkdir(parents=True, exist_ok=True)
118
+ return dest_dir / issue_file.name
119
+
120
+ def execute(self, entity_id: str | None = None, **kwargs) -> bool:
121
+ """Execute archive operation.
122
+
123
+ Archives issues by moving files and setting the archived flag.
124
+
125
+ Args:
126
+ entity_id: Optional issue ID
127
+ **kwargs: Filter options (all_closed, orphaned, etc.)
128
+
129
+ Returns:
130
+ True if successful
131
+ """
132
+ try:
133
+ entities = self.get_entities_to_archive(entity_id, **kwargs)
134
+
135
+ if not entities:
136
+ self.console.print(
137
+ f"No {self.entity_type.value}s to archive",
138
+ style="yellow",
139
+ )
140
+ return False
141
+
142
+ # Validate entities
143
+ invalid_entities = []
144
+ for entity in entities:
145
+ is_valid, error_msg = self.validate_entity_before_archive(
146
+ entity, **kwargs
147
+ )
148
+ if not is_valid:
149
+ invalid_entities.append((entity, error_msg))
150
+ self.console.print(
151
+ f"⚠️ Cannot archive {entity.id}: {error_msg}",
152
+ style="yellow",
153
+ )
154
+
155
+ if invalid_entities:
156
+ invalid_entity_ids = {e.id for e, _ in invalid_entities}
157
+ entities = [e for e in entities if e.id not in invalid_entity_ids]
158
+ if not entities:
159
+ return False
160
+
161
+ # Find files
162
+ files_to_archive = self.find_entity_files(entities)
163
+
164
+ if not files_to_archive:
165
+ self.console.print(
166
+ f"Could not find files for {len(entities)} {self.entity_type.value}(s)",
167
+ style="yellow",
168
+ )
169
+ return False
170
+
171
+ # Archive with folder structure preservation
172
+ from roadmap.adapters.cli.crud.crud_helpers import (
173
+ display_archive_success,
174
+ get_archive_dir,
175
+ )
176
+
177
+ archive_dir = get_archive_dir(self.entity_type)
178
+ archive_dir.mkdir(parents=True, exist_ok=True)
179
+
180
+ archived_files = []
181
+ failed_count = 0
182
+
183
+ for file_path in files_to_archive:
184
+ try:
185
+ archive_file = self._determine_archive_path(archive_dir, file_path)
186
+ if archive_file.exists():
187
+ failed_count += 1
188
+ continue
189
+
190
+ # Ensure parent directory exists and use rename (mv) for atomicity
191
+ archive_file.parent.mkdir(parents=True, exist_ok=True)
192
+ file_path.rename(archive_file)
193
+ archived_files.append(archive_file)
194
+
195
+ except Exception as e:
196
+ self.console.print(
197
+ f"⚠️ Failed to archive {file_path.name}: {str(e)}",
198
+ style="yellow",
199
+ )
200
+ failed_count += 1
201
+
202
+ # Update state (set archived flag and update cache)
203
+ self.post_archive_hook(archived_files, entities, **kwargs)
204
+
205
+ # Display results
206
+ display_archive_success(
207
+ self.entity_type,
208
+ len(archived_files),
209
+ failed_count,
210
+ console=self.console,
211
+ )
212
+
213
+ return True
214
+
215
+ except Exception as e:
216
+ self.console.print(
217
+ f"❌ Archive operation failed: {str(e)}",
218
+ style="red",
219
+ )
220
+ import click
221
+
222
+ raise click.ClickException(str(e)) from e
223
+
224
+ def post_archive_hook(
225
+ self, archived_files: list[Path], entities: list[Any], **kwargs
226
+ ) -> None:
227
+ """Handle post-archive state updates.
228
+
229
+ Sets the archived flag on entities after moving files.
230
+
231
+ Args:
232
+ archived_files: Files that were archived
233
+ entities: Entities that were archived
234
+ **kwargs: Additional arguments
235
+ """
236
+ # Set archived flag on entities
237
+ from roadmap.core.models import IssueUpdateServiceParams
238
+
239
+ for entity in entities:
240
+ try:
241
+ params = IssueUpdateServiceParams(
242
+ issue_id=entity.id,
243
+ archived=True,
244
+ )
245
+ self.core.issues.update_issue(params)
246
+ except Exception as e:
247
+ self.console.print(
248
+ f"⚠️ Warning: Failed to mark issue {entity.id} as archived: {e}",
249
+ style="yellow",
250
+ )
251
+
252
+ # Clear the list cache after archiving to ensure archived issues don't appear in list output
253
+ self.core.issues._ops.issue_service._list_issues_cache.clear()
@@ -0,0 +1,39 @@
1
+ """Block issue command - thin wrapper around update.
2
+
3
+ This command is syntactic sugar for: roadmap issue update <ID> --status blocked
4
+ """
5
+
6
+ import click
7
+
8
+ from roadmap.adapters.cli.cli_command_helpers import require_initialized
9
+ from roadmap.common.cli_errors import handle_cli_errors
10
+ from roadmap.core.domain import Status
11
+
12
+ from .issue_status_helpers import StatusChangeConfig, apply_status_change
13
+
14
+
15
+ @click.command("block")
16
+ @click.argument("issue_id")
17
+ @click.option("--reason", "-r", help="Reason for blocking")
18
+ @click.pass_context
19
+ @handle_cli_errors(command_name="issue block")
20
+ @require_initialized
21
+ def block_issue(
22
+ ctx: click.Context,
23
+ issue_id: str,
24
+ reason: str,
25
+ ):
26
+ """Mark an issue as blocked (sets status to blocked).
27
+
28
+ Syntactic sugar for: roadmap issue update <ID> --status blocked
29
+ """
30
+ core = ctx.obj["core"]
31
+
32
+ config = StatusChangeConfig(
33
+ status=Status.BLOCKED,
34
+ emoji="🚫",
35
+ title_verb="Blocked",
36
+ title_style="bold red",
37
+ status_display="🚫 Blocked",
38
+ )
39
+ apply_status_change(core, issue_id, config, reason)
@@ -0,0 +1,216 @@
1
+ """Close issue command - thin wrapper around update.
2
+
3
+ This command is syntactic sugar for: roadmap issue update <ID> --status closed
4
+
5
+ SEMANTICS:
6
+ - Close: Changes status to 'closed', keeps files in place (no file movement), archived=false
7
+ - Archive: Moves files to archive directory AND sets archived=true flag (signals sync to delete remotely)
8
+
9
+ This separation allows for flexible status management without workspace reorganization.
10
+ """
11
+
12
+ from datetime import UTC, datetime
13
+
14
+ import click
15
+ from structlog import get_logger
16
+
17
+ from roadmap.adapters.cli.cli_command_helpers import require_initialized
18
+ from roadmap.adapters.cli.cli_error_handlers import handle_cli_error
19
+ from roadmap.common.console import get_console
20
+ from roadmap.common.formatters.text.operations import (
21
+ format_operation_failure,
22
+ format_operation_success,
23
+ )
24
+ from roadmap.common.logging import (
25
+ log_command,
26
+ track_database_operation,
27
+ )
28
+ from roadmap.core.domain import Status
29
+
30
+ console = get_console()
31
+ logger = get_logger()
32
+
33
+
34
+ def _parse_completion_date(date_str: str) -> datetime | None:
35
+ """Parse completion date from string.
36
+
37
+ Args:
38
+ date_str: Date string to parse (YYYY-MM-DD HH:MM or YYYY-MM-DD)
39
+
40
+ Returns:
41
+ Parsed datetime or None if parsing fails
42
+ """
43
+ if not date_str:
44
+ return None
45
+
46
+ try:
47
+ return datetime.strptime(date_str, "%Y-%m-%d %H:%M")
48
+ except ValueError as e:
49
+ from roadmap.common.logging import get_logger
50
+
51
+ logger = get_logger(__name__)
52
+ logger.debug(
53
+ "datetime_parse_failed",
54
+ date_str=date_str,
55
+ error=str(e),
56
+ action="parse_close_date",
57
+ )
58
+ try:
59
+ return datetime.strptime(date_str, "%Y-%m-%d")
60
+ except ValueError as e:
61
+ logger.debug(
62
+ "completion_date_parse_failed", date_str=date_str, error=str(e)
63
+ )
64
+ return None
65
+
66
+
67
+ def _resolve_end_date(
68
+ record_time: bool, date: str
69
+ ) -> tuple[datetime | None, str | None]:
70
+ """Resolve completion datetime and validation error message, if any."""
71
+ if not record_time:
72
+ return None, None
73
+
74
+ if date:
75
+ end_date = _parse_completion_date(date)
76
+ if end_date is None:
77
+ return None, "❌ Invalid date format. Use YYYY-MM-DD or YYYY-MM-DD HH:MM"
78
+ return end_date, None
79
+
80
+ return datetime.now(UTC), None
81
+
82
+
83
+ def _build_close_update_kwargs(end_date: datetime | None) -> dict:
84
+ """Build keyword arguments used for closing an issue."""
85
+ update_kwargs = {
86
+ "status": Status.CLOSED,
87
+ "progress_percentage": 100.0,
88
+ }
89
+ if end_date:
90
+ update_kwargs["actual_end_date"] = end_date
91
+ return update_kwargs
92
+
93
+
94
+ def _build_close_extra_details(
95
+ updated_issue, reason: str, end_date: datetime | None
96
+ ) -> dict[str, str]:
97
+ """Build success details for close operation output."""
98
+ extra_details: dict[str, str] = {
99
+ "Status": "Closed",
100
+ "Progress": "100%",
101
+ }
102
+ if reason:
103
+ extra_details["Reason"] = reason
104
+ if not end_date:
105
+ return extra_details
106
+
107
+ extra_details["Completed"] = end_date.strftime("%Y-%m-%d %H:%M")
108
+ start_date = updated_issue.actual_start_date
109
+ if not start_date:
110
+ return extra_details
111
+
112
+ duration = end_date - start_date
113
+ hours = duration.total_seconds() / 3600
114
+ extra_details["Duration"] = f"{hours:.1f} hours"
115
+
116
+ if not updated_issue.estimated_hours:
117
+ return extra_details
118
+
119
+ diff = hours - updated_issue.estimated_hours
120
+ if abs(diff) <= 0.5:
121
+ extra_details["Variance"] = "On target"
122
+ elif diff > 0:
123
+ extra_details["Variance"] = f"Over by {diff:.1f} hours"
124
+ else:
125
+ extra_details["Variance"] = f"Under by {abs(diff):.1f} hours"
126
+
127
+ return extra_details
128
+
129
+
130
+ @click.command("close")
131
+ @click.argument("issue_id")
132
+ @click.option("--reason", "-r", help="Reason for closing the issue")
133
+ @click.option(
134
+ "--record-time",
135
+ "-t",
136
+ is_flag=True,
137
+ help="Record actual completion time",
138
+ )
139
+ @click.option(
140
+ "--date",
141
+ help="Completion date (YYYY-MM-DD HH:MM, defaults to now)",
142
+ )
143
+ @click.pass_context
144
+ @log_command("issue_close", entity_type="issue", track_duration=True)
145
+ @require_initialized
146
+ def close_issue(
147
+ ctx: click.Context,
148
+ issue_id: str,
149
+ reason: str,
150
+ record_time: bool,
151
+ date: str,
152
+ ):
153
+ """Close an issue (sets status to closed and progress to 100%).
154
+
155
+ Syntactic sugar for: roadmap issue update <ID> --status closed
156
+ """
157
+ core = ctx.obj["core"]
158
+
159
+ try:
160
+ end_date, date_error = _resolve_end_date(record_time, date)
161
+ if date_error:
162
+ console.print(date_error, style="bold red")
163
+ return
164
+
165
+ update_kwargs = _build_close_update_kwargs(end_date)
166
+
167
+ # Use wrapper for status change display
168
+ from roadmap.adapters.cli.cli_command_helpers import ensure_entity_exists
169
+
170
+ # Verify issue exists before updating
171
+ ensure_entity_exists(core, "issue", issue_id)
172
+
173
+ # Update the issue
174
+ with track_database_operation(
175
+ "update", "issue", entity_id=issue_id, warn_threshold_ms=2000
176
+ ):
177
+ updated_issue = core.issues.update(issue_id, **update_kwargs)
178
+
179
+ if updated_issue:
180
+ extra_details = _build_close_extra_details(updated_issue, reason, end_date)
181
+
182
+ # Use formatter
183
+ lines = format_operation_success(
184
+ emoji="✅",
185
+ action="Closed",
186
+ entity_title=updated_issue.title,
187
+ entity_id=issue_id,
188
+ extra_details=extra_details,
189
+ )
190
+ for line in lines:
191
+ console.print(line, style="bold green" if "Closed" in line else "cyan")
192
+ else:
193
+ lines = format_operation_failure(
194
+ action="close",
195
+ entity_id=issue_id,
196
+ error="Failed to update status",
197
+ )
198
+ for line in lines:
199
+ console.print(line, style="bold red")
200
+
201
+ except Exception as e:
202
+ handle_cli_error(
203
+ error=e,
204
+ operation="close_issue",
205
+ entity_type="issue",
206
+ entity_id=issue_id,
207
+ context={"reason": reason},
208
+ fatal=True,
209
+ )
210
+ lines = format_operation_failure(
211
+ action="close",
212
+ entity_id=issue_id,
213
+ error=str(e),
214
+ )
215
+ for line in lines:
216
+ console.print(line, style="bold red")