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,765 @@
1
+ """Issue service - handles all issue-related operations.
2
+
3
+ The IssueService manages:
4
+ - Issue creation with metadata
5
+ - Issue listing with filtering (milestone, status, assignee)
6
+ - Getting specific issues by ID
7
+ - Updating issue fields
8
+ - Deleting issues
9
+ - Status transitions
10
+
11
+ Extracted from core.py to separate business logic.
12
+ """
13
+
14
+ from roadmap.common.cache import SessionCache
15
+ from roadmap.common.constants import IssueType, Priority, Status
16
+ from roadmap.common.errors import OperationType, safe_operation
17
+ from roadmap.common.logging import get_logger
18
+ from roadmap.common.logging.error_logging import (
19
+ log_database_error,
20
+ log_error_with_context,
21
+ )
22
+ from roadmap.common.observability.instrumentation import traced
23
+ from roadmap.common.result import Err, Ok
24
+ from roadmap.common.services import log_entry, log_event, log_exit, log_metric
25
+ from roadmap.common.utils.timezone_utils import now_utc
26
+ from roadmap.core.domain.issue import Issue
27
+ from roadmap.core.models import (
28
+ NOT_PROVIDED,
29
+ IssueCreateServiceParams,
30
+ IssueUpdateServiceParams,
31
+ )
32
+ from roadmap.core.repositories import IssueRepository
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ class IssueService:
38
+ """Service for managing issues."""
39
+
40
+ # Cache TTL in seconds
41
+ _cache_ttl = 60
42
+
43
+ def __init__(self, repository: IssueRepository):
44
+ """Initialize issue service.
45
+
46
+ Args:
47
+ repository: IssueRepository implementation for data persistence
48
+ """
49
+ self.repository = repository
50
+ # Cache for list_issues results (instance-level to avoid test pollution)
51
+ self._list_issues_cache = SessionCache()
52
+
53
+ @traced("create_issue")
54
+ @safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
55
+ def create_issue(self, params: IssueCreateServiceParams) -> Issue:
56
+ """Create a new issue with provided metadata.
57
+
58
+ Args:
59
+ params: IssueCreateServiceParams containing all creation parameters
60
+ - title: Issue title/summary
61
+ - priority: Priority level (CRITICAL, HIGH, MEDIUM, LOW)
62
+ - issue_type: Type of issue (BUG, FEATURE, TASK, etc.)
63
+ - milestone: Associated milestone name
64
+ - labels: List of labels/tags
65
+ - assignee: Assigned user or team member
66
+ - estimate: Estimated effort in hours
67
+ - depends_on: List of issue IDs this depends on
68
+ - blocks: List of issue IDs this blocks
69
+
70
+ Returns:
71
+ Newly created Issue object
72
+ """
73
+ log_entry("create_issue", title=params.title, priority=params.priority)
74
+ logger.info(
75
+ "creating_issue",
76
+ title=params.title,
77
+ priority=params.priority,
78
+ issue_type=params.issue_type,
79
+ )
80
+ log_event("issue_creation_started", issue_title=params.title)
81
+
82
+ # Convert string values to enums (if they're not already)
83
+ try:
84
+ priority = (
85
+ Priority[params.priority.upper()]
86
+ if isinstance(params.priority, str)
87
+ else params.priority or Priority.MEDIUM
88
+ )
89
+ except (AttributeError, KeyError):
90
+ priority = Priority.MEDIUM
91
+
92
+ try:
93
+ issue_type = (
94
+ IssueType[params.issue_type.upper()]
95
+ if isinstance(params.issue_type, str)
96
+ else params.issue_type or IssueType.OTHER
97
+ )
98
+ except (AttributeError, KeyError):
99
+ issue_type = IssueType.OTHER
100
+
101
+ issue = Issue(
102
+ title=params.title,
103
+ priority=priority,
104
+ issue_type=issue_type,
105
+ milestone=params.milestone,
106
+ labels=params.labels or [],
107
+ assignee=params.assignee,
108
+ estimated_hours=params.estimate,
109
+ depends_on=params.depends_on or [],
110
+ blocks=params.blocks or [],
111
+ content=params.content
112
+ or f"# {params.title}\n\n## Description\n\nBrief description of the issue or feature request.\n\n## Acceptance Criteria\n\n- [ ] Criterion 1\n- [ ] Criterion 2\n- [ ] Criterion 3",
113
+ )
114
+
115
+ # Persist using repository abstraction
116
+ try:
117
+ self.repository.save(issue)
118
+ except Exception as e:
119
+ log_database_error(
120
+ e,
121
+ operation="create",
122
+ entity_type="Issue",
123
+ entity_id=issue.id,
124
+ )
125
+ raise
126
+
127
+ # Invalidate cache after successful creation
128
+ self._list_issues_cache.clear()
129
+
130
+ log_event("issue_created", issue_id=issue.id)
131
+ log_exit("create_issue", issue_id=issue.id)
132
+ return issue
133
+
134
+ def _check_milestone_filter(self, issue: Issue, milestone: str | None) -> bool:
135
+ """Check milestone filter.
136
+
137
+ Args:
138
+ issue: Issue to check
139
+ milestone: Required milestone or None
140
+
141
+ Returns:
142
+ True if milestone matches or no filter
143
+ """
144
+ return not milestone or issue.milestone == milestone
145
+
146
+ def _check_status_filter(self, issue: Issue, status: Status | None) -> bool:
147
+ """Check status filter.
148
+
149
+ Args:
150
+ issue: Issue to check
151
+ status: Required status or None
152
+
153
+ Returns:
154
+ True if status matches or no filter
155
+ """
156
+ return not status or issue.status == status
157
+
158
+ def _check_priority_filter(self, issue: Issue, priority: Priority | None) -> bool:
159
+ """Check priority filter.
160
+
161
+ Args:
162
+ issue: Issue to check
163
+ priority: Required priority or None
164
+
165
+ Returns:
166
+ True if priority matches or no filter
167
+ """
168
+ return not priority or issue.priority == priority
169
+
170
+ def _check_type_filter(self, issue: Issue, issue_type: IssueType | None) -> bool:
171
+ """Check issue type filter.
172
+
173
+ Args:
174
+ issue: Issue to check
175
+ issue_type: Required issue type or None
176
+
177
+ Returns:
178
+ True if type matches or no filter
179
+ """
180
+ return not issue_type or issue.issue_type == issue_type
181
+
182
+ def _check_assignee_filter(self, issue: Issue, assignee: str | None) -> bool:
183
+ """Check assignee filter.
184
+
185
+ Args:
186
+ issue: Issue to check
187
+ assignee: Required assignee or None
188
+
189
+ Returns:
190
+ True if assignee matches or no filter
191
+ """
192
+ return not assignee or issue.assignee == assignee
193
+
194
+ def _matches_all_filters(
195
+ self,
196
+ issue: Issue,
197
+ milestone: str | None,
198
+ status: Status | None,
199
+ priority: Priority | None,
200
+ issue_type: IssueType | None,
201
+ assignee: str | None,
202
+ ) -> bool:
203
+ """Check if issue matches all provided filter criteria."""
204
+ return (
205
+ self._check_milestone_filter(issue, milestone)
206
+ and self._check_status_filter(issue, status)
207
+ and self._check_priority_filter(issue, priority)
208
+ and self._check_type_filter(issue, issue_type)
209
+ and self._check_assignee_filter(issue, assignee)
210
+ )
211
+
212
+ def _get_priority_order(self) -> dict:
213
+ """Get priority ordering for sorting."""
214
+ return {
215
+ Priority.CRITICAL: 0,
216
+ Priority.HIGH: 1,
217
+ Priority.MEDIUM: 2,
218
+ Priority.LOW: 3,
219
+ }
220
+
221
+ def _sort_issues_by_priority_and_date(self, issues: list[Issue]) -> list[Issue]:
222
+ """Sort issues by priority then creation date."""
223
+ priority_order = self._get_priority_order()
224
+ issues.sort(key=lambda x: (priority_order.get(x.priority, 999), x.created))
225
+ return issues
226
+
227
+ @traced("list_issues")
228
+ def list_issues(
229
+ self,
230
+ milestone: str | None = None,
231
+ status: Status | None = None,
232
+ priority: Priority | None = None,
233
+ issue_type: IssueType | None = None,
234
+ assignee: str | None = None,
235
+ ) -> list[Issue]:
236
+ """List issues with optional filtering.
237
+
238
+ All filters are combined with AND logic (all must match).
239
+
240
+ Args:
241
+ milestone: Filter by milestone name
242
+ status: Filter by status (OPEN, IN_PROGRESS, BLOCKED, REVIEW, DONE)
243
+ priority: Filter by priority level
244
+ issue_type: Filter by issue type
245
+ assignee: Filter by assignee
246
+
247
+ Returns:
248
+ List of Issue objects matching all filters, sorted by priority then date
249
+ """
250
+ log_entry("list_issues", milestone=milestone, status=status)
251
+
252
+ # Create cache key from filter parameters
253
+ cache_key = (milestone, status, priority, issue_type, assignee)
254
+ # Convert to string for cache key
255
+ cache_key_str = str(cache_key)
256
+
257
+ # Check cache first
258
+ cached = self._list_issues_cache.get(cache_key_str)
259
+ if cached is not None:
260
+ log_metric("cache_hit", 1, operation="list_issues")
261
+ log_exit("list_issues", issue_count=len(cached), from_cache=True)
262
+ return cached
263
+
264
+ # Get issues from repository
265
+ try:
266
+ issues = self.repository.list()
267
+ except Exception as e:
268
+ log_database_error(
269
+ e,
270
+ operation="list",
271
+ entity_type="Issue",
272
+ )
273
+ logger.warning(
274
+ "returning_empty_issue_list_due_to_error",
275
+ severity="operational",
276
+ )
277
+ return []
278
+
279
+ log_metric("issues_enumerated", len(issues))
280
+
281
+ # Apply filters
282
+ try:
283
+ filtered_issues = [
284
+ issue
285
+ for issue in issues
286
+ if self._matches_all_filters(
287
+ issue, milestone, status, priority, issue_type, assignee
288
+ )
289
+ ]
290
+ except Exception as e:
291
+ log_error_with_context(
292
+ e,
293
+ operation="filter_issues",
294
+ entity_type="Issue",
295
+ additional_context={
296
+ "milestone": milestone,
297
+ "status": status,
298
+ "priority": priority,
299
+ },
300
+ )
301
+ return issues # Return unfiltered list as fallback
302
+
303
+ # Sort by priority then by creation date
304
+ sorted_issues = self._sort_issues_by_priority_and_date(filtered_issues)
305
+ log_metric("issues_filtered", len(issues), filtered=len(sorted_issues))
306
+
307
+ # Cache the result with TTL
308
+ self._list_issues_cache.set(cache_key_str, sorted_issues, ttl=self._cache_ttl)
309
+
310
+ log_exit("list_issues", issue_count=len(sorted_issues))
311
+ return sorted_issues
312
+
313
+ @traced("list_all_including_archived")
314
+ def list_all_including_archived(self) -> list[Issue]:
315
+ """List all issues including archived ones.
316
+
317
+ Used by sync operations that need to compare both active and archived issues.
318
+
319
+ Returns:
320
+ List of all Issue objects (both active and archived)
321
+ """
322
+ log_entry("list_all_including_archived")
323
+
324
+ try:
325
+ issues = self.repository.list_all_including_archived()
326
+ except Exception as e:
327
+ log_database_error(
328
+ e,
329
+ operation="list_all_including_archived",
330
+ entity_type="Issue",
331
+ )
332
+ logger.warning(
333
+ "returning_empty_issue_list_due_to_error",
334
+ severity="operational",
335
+ )
336
+ return []
337
+
338
+ log_metric("all_issues_enumerated", len(issues))
339
+ log_exit("list_all_including_archived", issue_count=len(issues))
340
+ return issues
341
+
342
+ @traced("get_issue")
343
+ def get_issue(self, issue_id: str) -> Issue | None:
344
+ """Get a specific issue by ID.
345
+
346
+ Args:
347
+ issue_id: Issue identifier (ID prefix used in filename)
348
+
349
+ Returns:
350
+ Issue object if found, None otherwise.
351
+
352
+ Note:
353
+ If multiple copies exist (due to migration), prefers milestone-specific
354
+ subdirectories (v.X.X.X) over root directory.
355
+ """
356
+ log_entry("get_issue", issue_id=issue_id)
357
+ # Get issue from repository
358
+ try:
359
+ issue = self.repository.get(issue_id)
360
+ except Exception as e:
361
+ log_database_error(
362
+ e,
363
+ operation="read",
364
+ entity_type="Issue",
365
+ entity_id=issue_id,
366
+ )
367
+ return None
368
+
369
+ if issue and hasattr(issue, "file_path"):
370
+ # Store the original file path so updates preserve the location
371
+ issue.file_path = str(issue.file_path)
372
+ log_event("issue_found", issue_id=issue_id, title=issue.title)
373
+ else:
374
+ log_event("issue_not_found", issue_id=issue_id)
375
+
376
+ log_exit("get_issue", found=issue is not None)
377
+ return issue
378
+
379
+ @safe_operation(OperationType.UPDATE, "Issue")
380
+ @traced("update_issue")
381
+ def update_issue(self, params: IssueUpdateServiceParams) -> Issue | None:
382
+ """Update an existing issue with new field values.
383
+
384
+ Args:
385
+ params: IssueUpdateServiceParams with fields to update
386
+
387
+ Returns:
388
+ Updated Issue object if found, None otherwise
389
+ """
390
+ log_entry(
391
+ "update_issue",
392
+ issue_id=params.issue_id,
393
+ fields=[
394
+ "title",
395
+ "status",
396
+ "priority",
397
+ "assignee",
398
+ "milestone",
399
+ "content",
400
+ "estimate",
401
+ "reason",
402
+ ],
403
+ )
404
+ logger.info(
405
+ "updating_issue",
406
+ issue_id=params.issue_id,
407
+ update_fields=[
408
+ "title",
409
+ "status",
410
+ "priority",
411
+ "assignee",
412
+ "milestone",
413
+ "content",
414
+ "estimate",
415
+ ],
416
+ )
417
+
418
+ issue = self.get_issue(params.issue_id)
419
+ if not issue:
420
+ log_event("issue_not_found", issue_id=params.issue_id)
421
+ log_exit("update_issue", success=False)
422
+ return None
423
+
424
+ # Update fields if provided (not NOT_PROVIDED)
425
+ if params.title is not NOT_PROVIDED:
426
+ issue.title = params.title
427
+ log_event("issue_field_updated", issue_id=params.issue_id, field="title")
428
+ if params.status is not NOT_PROVIDED:
429
+ issue.status = Status(params.status)
430
+ log_event("issue_field_updated", issue_id=params.issue_id, field="status")
431
+ if params.priority is not NOT_PROVIDED:
432
+ try:
433
+ priority = (
434
+ Priority[params.priority.upper()]
435
+ if isinstance(params.priority, str)
436
+ else params.priority
437
+ )
438
+ except (AttributeError, KeyError):
439
+ # Invalid priority - keep current priority
440
+ logger.warning(
441
+ "invalid_priority_in_update",
442
+ issue_id=params.issue_id,
443
+ priority=params.priority,
444
+ severity="data_error",
445
+ )
446
+ priority = issue.priority
447
+ issue.priority = priority
448
+ log_event("issue_field_updated", issue_id=params.issue_id, field="priority")
449
+ if params.assignee is not NOT_PROVIDED:
450
+ issue.assignee = params.assignee
451
+ log_event("issue_field_updated", issue_id=params.issue_id, field="assignee")
452
+ if params.milestone is not NOT_PROVIDED:
453
+ issue.milestone = params.milestone
454
+ log_event(
455
+ "issue_field_updated", issue_id=params.issue_id, field="milestone"
456
+ )
457
+ if params.content is not NOT_PROVIDED:
458
+ issue.content = params.content
459
+ log_event("issue_field_updated", issue_id=params.issue_id, field="content")
460
+ if params.estimate is not NOT_PROVIDED:
461
+ issue.estimated_hours = params.estimate
462
+ log_event("issue_field_updated", issue_id=params.issue_id, field="estimate")
463
+ if params.archived is not NOT_PROVIDED:
464
+ issue.archived = params.archived
465
+ log_event("issue_field_updated", issue_id=params.issue_id, field="archived")
466
+
467
+ # Update timestamp
468
+ issue.updated = now_utc()
469
+
470
+ # Save updated issue through repository
471
+ try:
472
+ self.repository.save(issue)
473
+ except Exception as e:
474
+ log_database_error(
475
+ e,
476
+ operation="update",
477
+ entity_type="Issue",
478
+ entity_id=params.issue_id,
479
+ )
480
+ raise
481
+
482
+ log_event("issue_saved", issue_id=params.issue_id)
483
+
484
+ # Invalidate cache after successful update
485
+ self._list_issues_cache.clear()
486
+
487
+ log_exit("update_issue", issue_id=params.issue_id, success=True)
488
+
489
+ return issue
490
+
491
+ @safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
492
+ @traced("delete_issue")
493
+ def delete_issue(self, issue_id: str) -> bool:
494
+ """Delete an issue.
495
+
496
+ Args:
497
+ issue_id: Issue identifier
498
+
499
+ Returns:
500
+ True if deleted successfully, False if not found
501
+ """
502
+ log_entry("delete_issue", issue_id=issue_id)
503
+ logger.info("deleting_issue", issue_id=issue_id)
504
+ # Delete issue through repository
505
+ try:
506
+ deleted = self.repository.delete(issue_id)
507
+ except Exception as e:
508
+ log_database_error(
509
+ e,
510
+ operation="delete",
511
+ entity_type="Issue",
512
+ entity_id=issue_id,
513
+ )
514
+ raise
515
+
516
+ if deleted:
517
+ log_event("issue_deleted", issue_id=issue_id)
518
+ # Invalidate cache after successful deletion
519
+ self._list_issues_cache.clear()
520
+ log_exit("delete_issue", success=True)
521
+ else:
522
+ log_event("issue_not_found_delete", issue_id=issue_id)
523
+ log_exit("delete_issue", success=False)
524
+ return deleted
525
+
526
+ @safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
527
+ @traced("delete_many_issues")
528
+ def delete_many_issues(self, issue_ids: list[str]) -> int:
529
+ """Delete multiple issues in a single batch operation.
530
+
531
+ Batch deletion is much more efficient than individual deletes
532
+ as it performs all deletes in one transaction and clears the
533
+ cache only once, avoiding repeated enumeration overhead.
534
+
535
+ Args:
536
+ issue_ids: List of issue identifiers to delete
537
+
538
+ Returns:
539
+ Number of issues successfully deleted
540
+ """
541
+ if not issue_ids:
542
+ return 0
543
+
544
+ log_entry("delete_many_issues", count=len(issue_ids))
545
+ logger.info("deleting_many_issues", count=len(issue_ids))
546
+
547
+ try:
548
+ deleted_count = self.repository.delete_many(issue_ids)
549
+ except Exception as e:
550
+ log_database_error(
551
+ e,
552
+ operation="delete_many",
553
+ entity_type="Issue",
554
+ )
555
+ raise
556
+
557
+ if deleted_count > 0:
558
+ log_event("issues_deleted", count=deleted_count, requested=len(issue_ids))
559
+ # Invalidate cache after successful bulk deletion
560
+ self._list_issues_cache.clear()
561
+ log_exit("delete_many_issues", success=True)
562
+ else:
563
+ log_event("no_issues_deleted", requested=len(issue_ids))
564
+ log_exit("delete_many_issues", success=False)
565
+
566
+ return deleted_count
567
+
568
+ def close_issue(self, issue_id: str) -> Issue | None:
569
+ """Close/mark issue as complete.
570
+
571
+ Args:
572
+ issue_id: Issue identifier
573
+
574
+ Returns:
575
+ Closed Issue object if found, None otherwise
576
+ """
577
+ params = IssueUpdateServiceParams(issue_id=issue_id, status="closed")
578
+ return self.update_issue(params)
579
+
580
+ @traced("merge_issues")
581
+ def merge_issues(
582
+ self, canonical_id: str, duplicate_id: str
583
+ ) -> Ok[Issue] | Err[str]:
584
+ """Merge duplicate issue into canonical issue.
585
+
586
+ Transfers all data from duplicate to canonical:
587
+ - Comments and discussion threads
588
+ - Labels (union of both)
589
+ - Remote IDs (merge both backends)
590
+ - Updated timestamp (take latest)
591
+ - Keep canonical: created date, ID, content (local priority)
592
+
593
+ Args:
594
+ canonical_id: ID of issue to keep
595
+ duplicate_id: ID of issue to merge away
596
+
597
+ Returns:
598
+ Ok(merged_issue) on success, Err(message) on failure
599
+ """
600
+ log_entry("merge_issues", canonical_id=canonical_id, duplicate_id=duplicate_id)
601
+
602
+ # Get both issues
603
+ canonical = self.get_issue(canonical_id)
604
+ if not canonical:
605
+ msg = f"Canonical issue {canonical_id} not found"
606
+ logger.warning("merge_issues_canonical_not_found", issue_id=canonical_id)
607
+ log_exit("merge_issues", success=False)
608
+ return Err(msg)
609
+
610
+ duplicate = self.get_issue(duplicate_id)
611
+ if not duplicate:
612
+ msg = f"Duplicate issue {duplicate_id} not found"
613
+ logger.warning("merge_issues_duplicate_not_found", issue_id=duplicate_id)
614
+ log_exit("merge_issues", success=False)
615
+ return Err(msg)
616
+
617
+ try:
618
+ # Merge data: union labels, merge remote_ids, combine comments
619
+ merged = Issue(
620
+ id=canonical.id,
621
+ title=canonical.title, # Keep canonical title
622
+ headline=canonical.headline,
623
+ content=canonical.content, # Keep canonical content (local priority)
624
+ status=canonical.status, # Keep canonical status
625
+ priority=canonical.priority,
626
+ issue_type=canonical.issue_type,
627
+ milestone=canonical.milestone,
628
+ labels=list(set(canonical.labels + duplicate.labels)), # Union labels
629
+ assignee=canonical.assignee, # Keep canonical assignee
630
+ estimated_hours=canonical.estimated_hours,
631
+ due_date=canonical.due_date,
632
+ depends_on=list(set(canonical.depends_on + duplicate.depends_on)),
633
+ blocks=list(set(canonical.blocks + duplicate.blocks)),
634
+ actual_start_date=canonical.actual_start_date,
635
+ actual_end_date=canonical.actual_end_date,
636
+ progress_percentage=canonical.progress_percentage,
637
+ handoff_notes=canonical.handoff_notes,
638
+ previous_assignee=canonical.previous_assignee,
639
+ handoff_date=canonical.handoff_date,
640
+ git_branches=list(set(canonical.git_branches + duplicate.git_branches)),
641
+ git_commits=canonical.git_commits
642
+ + duplicate.git_commits, # Append commits
643
+ completed_date=canonical.completed_date or duplicate.completed_date,
644
+ comments=canonical.comments + duplicate.comments, # Append comments
645
+ created=canonical.created, # Keep canonical's earliest created
646
+ updated=max(
647
+ canonical.updated, duplicate.updated
648
+ ), # Take latest updated
649
+ remote_ids={
650
+ **canonical.remote_ids,
651
+ **duplicate.remote_ids, # Merge remote IDs
652
+ },
653
+ )
654
+
655
+ # Persist merged issue
656
+ self.repository.save(merged)
657
+ self._list_issues_cache.clear()
658
+
659
+ logger.info(
660
+ "issues_merged",
661
+ canonical_id=canonical_id,
662
+ duplicate_id=duplicate_id,
663
+ merged_title=merged.title,
664
+ )
665
+ log_event(
666
+ "issues_merged", canonical_id=canonical_id, duplicate_id=duplicate_id
667
+ )
668
+ log_exit("merge_issues", success=True)
669
+ return Ok(merged)
670
+
671
+ except Exception as e:
672
+ log_database_error(
673
+ e,
674
+ operation="merge",
675
+ entity_type="Issue",
676
+ entity_id=canonical_id,
677
+ )
678
+ logger.error(
679
+ "merge_issues_failed",
680
+ canonical_id=canonical_id,
681
+ duplicate_id=duplicate_id,
682
+ error=str(e),
683
+ )
684
+ log_exit("merge_issues", success=False)
685
+ return Err(f"Failed to merge issues: {str(e)}")
686
+
687
+ @traced("archive_issue")
688
+ def archive_issue(
689
+ self,
690
+ issue_id: str,
691
+ duplicate_of_id: str | None = None,
692
+ resolution_type: str = "duplicate",
693
+ ) -> Ok[Issue] | Err[str]:
694
+ """Archive an issue (soft delete) with metadata.
695
+
696
+ Sets status to ARCHIVED and adds metadata about why it was archived.
697
+ Uses repository.mark_archived() for proper database handling.
698
+
699
+ Args:
700
+ issue_id: ID of issue to archive
701
+ duplicate_of_id: ID of the canonical issue this is a duplicate of
702
+ resolution_type: Type of resolution (default: "duplicate")
703
+
704
+ Returns:
705
+ Ok(archived_issue) on success, Err(message) on failure
706
+ """
707
+ log_entry(
708
+ "archive_issue",
709
+ issue_id=issue_id,
710
+ duplicate_of_id=duplicate_of_id,
711
+ resolution_type=resolution_type,
712
+ )
713
+
714
+ issue = self.get_issue(issue_id)
715
+ if not issue:
716
+ msg = f"Issue {issue_id} not found"
717
+ logger.warning("archive_issue_not_found", issue_id=issue_id)
718
+ log_exit("archive_issue", success=False)
719
+ return Err(msg)
720
+
721
+ try:
722
+ # Update domain model status
723
+ issue.status = Status.ARCHIVED
724
+
725
+ # Store resolution metadata
726
+ if not issue.github_sync_metadata:
727
+ issue.github_sync_metadata = {}
728
+ issue.github_sync_metadata["archived_at"] = now_utc().isoformat()
729
+ issue.github_sync_metadata["resolution_type"] = resolution_type
730
+ if duplicate_of_id:
731
+ issue.github_sync_metadata["duplicate_of_id"] = duplicate_of_id
732
+
733
+ issue.updated = now_utc()
734
+
735
+ # Persist updated metadata and status
736
+ self.repository.save(issue)
737
+ self._list_issues_cache.clear()
738
+
739
+ logger.info(
740
+ "issue_archived",
741
+ issue_id=issue_id,
742
+ duplicate_of_id=duplicate_of_id,
743
+ resolution_type=resolution_type,
744
+ )
745
+ log_event(
746
+ "issue_archived", issue_id=issue_id, duplicate_of_id=duplicate_of_id
747
+ )
748
+ log_exit("archive_issue", success=True)
749
+ return Ok(issue)
750
+
751
+ except Exception as e:
752
+ log_database_error(
753
+ e,
754
+ operation="archive",
755
+ entity_type="Issue",
756
+ entity_id=issue_id,
757
+ )
758
+ logger.error(
759
+ "archive_issue_failed",
760
+ issue_id=issue_id,
761
+ duplicate_of_id=duplicate_of_id,
762
+ error=str(e),
763
+ )
764
+ log_exit("archive_issue", success=False)
765
+ return Err(f"Failed to archive issue: {str(e)}")