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,288 @@
1
+ """Enhanced YAML validation utilities for roadmap persistence."""
2
+
3
+ import shutil
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from roadmap.common.datetime_parser import parse_datetime
11
+ from roadmap.common.utils.file_utils import ensure_directory_exists
12
+ from roadmap.common.validation import validate_frontmatter_structure
13
+
14
+
15
+ class YAMLRecoveryManager:
16
+ """Manages YAML file recovery and validation."""
17
+
18
+ def __init__(self, backup_dir: Path | None = None):
19
+ """Initialize YAMLRecoveryManager.
20
+
21
+ Args:
22
+ backup_dir: Directory for storing backups. Defaults to .roadmap/backups.
23
+ """
24
+ self.backup_dir = backup_dir or Path(".roadmap/backups")
25
+ ensure_directory_exists(self.backup_dir)
26
+
27
+ def create_backup(self, file_path: Path) -> Path:
28
+ """Create a backup of a YAML file before modification."""
29
+ if not file_path.exists():
30
+ raise FileNotFoundError(f"Cannot backup non-existent file: {file_path}")
31
+
32
+ timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
33
+ backup_name = f"{file_path.stem}_{timestamp}.backup{file_path.suffix}"
34
+ backup_path = self.backup_dir / backup_name
35
+
36
+ shutil.copy2(file_path, backup_path)
37
+ return backup_path
38
+
39
+ def list_backups(self, file_path: Path) -> list[Path]:
40
+ """List all backups for a specific file."""
41
+ pattern = f"{file_path.stem}_*.backup{file_path.suffix}"
42
+ return sorted(self.backup_dir.glob(pattern), reverse=True)
43
+
44
+ def validate_yaml_syntax(
45
+ self, content: str, file_path: Path | None = None
46
+ ) -> tuple[bool, str | None]:
47
+ """Validate YAML syntax and return error details if invalid."""
48
+ try:
49
+ yaml.safe_load(content)
50
+ return True, None
51
+ except yaml.YAMLError as e:
52
+ error_msg = f"YAML syntax error: {e}"
53
+ if hasattr(e, "problem_mark") and e.problem_mark: # type: ignore
54
+ line_num = e.problem_mark.line + 1 # type: ignore
55
+ col_num = e.problem_mark.column + 1 # type: ignore
56
+ error_msg += f" at line {line_num}, column {col_num}"
57
+ return False, error_msg
58
+
59
+ def validate_frontmatter_structure(
60
+ self, frontmatter: dict[str, Any], expected_type: str
61
+ ) -> tuple[bool, list[str]]:
62
+ """Validate frontmatter structure for issues or milestones."""
63
+ # Use unified validation framework
64
+ is_valid, errors = validate_frontmatter_structure(frontmatter, expected_type)
65
+
66
+ # Add datetime validation (specific to persistence layer)
67
+ datetime_fields = ["created", "updated", "due_date"]
68
+ for field in datetime_fields:
69
+ if field in frontmatter and frontmatter[field] is not None:
70
+ if isinstance(frontmatter[field], str):
71
+ try:
72
+ parse_datetime(frontmatter[field], "file")
73
+ except ValueError:
74
+ errors.append(
75
+ f"Invalid datetime format for {field}: {frontmatter[field]}"
76
+ )
77
+ is_valid = False
78
+
79
+ return is_valid, errors
80
+
81
+ def attempt_recovery(self, file_path: Path) -> tuple[bool, str | None]:
82
+ """Attempt to recover a corrupted YAML file."""
83
+ try:
84
+ # Try to read the file
85
+ content = file_path.read_text(encoding="utf-8")
86
+
87
+ # Check if it's a frontmatter issue
88
+ if content.startswith("---"):
89
+ parts = content.split("---", 2)
90
+ if len(parts) >= 3:
91
+ frontmatter_content = parts[1]
92
+ markdown_content = parts[2].strip() if len(parts) > 2 else ""
93
+
94
+ # Try to fix common YAML issues
95
+ fixed_frontmatter = self._fix_common_yaml_issues(
96
+ frontmatter_content
97
+ )
98
+
99
+ # Validate the fixed YAML
100
+ is_valid, _ = self.validate_yaml_syntax(fixed_frontmatter)
101
+ if is_valid:
102
+ # Create backup before fixing
103
+ self.create_backup(file_path)
104
+
105
+ # Write the fixed content
106
+ fixed_content = (
107
+ f"---\n{fixed_frontmatter}\n---\n\n{markdown_content}"
108
+ )
109
+ file_path.write_text(fixed_content, encoding="utf-8")
110
+ return True, "File recovered with YAML fixes applied"
111
+
112
+ return False, "Could not automatically recover the file"
113
+
114
+ except Exception as e:
115
+ return False, f"Recovery failed: {e}"
116
+
117
+ def _is_already_quoted(self, value: str) -> bool:
118
+ """Check if value is already properly quoted.
119
+
120
+ Args:
121
+ value: Value to check
122
+
123
+ Returns:
124
+ True if value is already quoted
125
+ """
126
+ return (value.startswith('"') and value.endswith('"')) or (
127
+ value.startswith("'") and value.endswith("'")
128
+ )
129
+
130
+ def _is_yaml_literal(self, value: str) -> bool:
131
+ """Check if value is a YAML literal (bool, null, number, collection).
132
+
133
+ Args:
134
+ value: Value to check
135
+
136
+ Returns:
137
+ True if value is a YAML literal
138
+ """
139
+ return (
140
+ value in ["true", "false", "null"]
141
+ or value.isdigit()
142
+ or value.startswith("[")
143
+ or value.startswith("{")
144
+ )
145
+
146
+ def _needs_quoting(self, value: str) -> bool:
147
+ """Check if value contains special YAML characters requiring quoting.
148
+
149
+ Args:
150
+ value: Value to check
151
+
152
+ Returns:
153
+ True if value needs quoting
154
+ """
155
+ special_chars = [
156
+ ":",
157
+ "#",
158
+ "@",
159
+ "`",
160
+ "|",
161
+ ">",
162
+ "*",
163
+ "&",
164
+ "!",
165
+ "%",
166
+ "{",
167
+ "}",
168
+ "[",
169
+ "]",
170
+ ]
171
+ return any(char in value for char in special_chars)
172
+
173
+ def _quote_if_needed(self, value: str) -> str:
174
+ """Quote value if it needs quoting and isn't already quoted.
175
+
176
+ Args:
177
+ value: Value to potentially quote
178
+
179
+ Returns:
180
+ Quoted value if needed, otherwise original value
181
+ """
182
+ if value and not (
183
+ self._is_already_quoted(value) or self._is_yaml_literal(value)
184
+ ):
185
+ if self._needs_quoting(value):
186
+ return f'"{value}"'
187
+ return value
188
+
189
+ def _fix_yaml_line(self, line: str) -> str:
190
+ """Fix a single YAML line.
191
+
192
+ Args:
193
+ line: Single line of YAML to fix
194
+
195
+ Returns:
196
+ Fixed line
197
+ """
198
+ if not line.strip() or line.strip().startswith("#"):
199
+ return line
200
+
201
+ if ":" in line:
202
+ key, _, value = line.partition(":")
203
+ value = value.strip()
204
+ fixed_value = self._quote_if_needed(value)
205
+ return f"{key}: {fixed_value}"
206
+
207
+ return line
208
+
209
+ def _fix_common_yaml_issues(self, yaml_content: str) -> str:
210
+ """Fix common YAML formatting issues."""
211
+ lines = yaml_content.split("\n")
212
+ fixed_lines = [self._fix_yaml_line(line) for line in lines]
213
+ return "\n".join(fixed_lines)
214
+
215
+
216
+ class EnhancedYAMLPersistence:
217
+ """Enhanced YAML persistence with validation and recovery."""
218
+
219
+ def __init__(self, recovery_manager: YAMLRecoveryManager | None = None):
220
+ """Initialize EnhancedYAMLPersistence.
221
+
222
+ Args:
223
+ recovery_manager: YAMLRecoveryManager instance. Defaults to new instance.
224
+ """
225
+ self.recovery_manager = recovery_manager or YAMLRecoveryManager()
226
+
227
+ def safe_load_with_validation(
228
+ self, file_path: Path, expected_type: str
229
+ ) -> tuple[bool, dict[str, Any] | str]:
230
+ """Safely load and validate a YAML file with recovery options."""
231
+ try:
232
+ if not file_path.exists():
233
+ return False, f"File does not exist: {file_path}"
234
+
235
+ content = file_path.read_text(encoding="utf-8")
236
+
237
+ # Parse frontmatter
238
+ if not content.startswith("---"):
239
+ return False, "File does not contain YAML frontmatter"
240
+
241
+ parts = content.split("---", 2)
242
+ if len(parts) < 3:
243
+ return False, "Invalid frontmatter format"
244
+
245
+ frontmatter_content = parts[1]
246
+ markdown_content = parts[2].strip()
247
+
248
+ # Validate YAML syntax
249
+ is_valid, error = self.recovery_manager.validate_yaml_syntax(
250
+ frontmatter_content, file_path
251
+ )
252
+ if not is_valid:
253
+ # Attempt recovery
254
+ recovered, recovery_msg = self.recovery_manager.attempt_recovery(
255
+ file_path
256
+ )
257
+ if recovered:
258
+ # Retry loading after recovery
259
+ return self.safe_load_with_validation(file_path, expected_type)
260
+ else:
261
+ return (
262
+ False,
263
+ f"YAML validation failed: {error}. Recovery failed: {recovery_msg}",
264
+ )
265
+
266
+ # Parse YAML
267
+ loaded = yaml.safe_load(frontmatter_content)
268
+ frontmatter: dict = loaded if isinstance(loaded, dict) else {}
269
+
270
+ # Validate structure
271
+ is_valid, errors = self.recovery_manager.validate_frontmatter_structure(
272
+ frontmatter, expected_type
273
+ )
274
+ if not is_valid:
275
+ return False, f"Validation errors: {'; '.join(errors)}"
276
+
277
+ # Add content to frontmatter
278
+ frontmatter["content"] = markdown_content
279
+
280
+ return True, frontmatter
281
+
282
+ except Exception as e:
283
+ return False, f"Failed to load file: {e}"
284
+
285
+
286
+ # Global instances for easy access
287
+ recovery_manager = YAMLRecoveryManager()
288
+ enhanced_persistence = EnhancedYAMLPersistence(recovery_manager)
@@ -0,0 +1,15 @@
1
+ """Repository classes for persistence layer."""
2
+
3
+ from .issue_repository import IssueRepository
4
+ from .milestone_repository import MilestoneRepository
5
+ from .project_repository import ProjectRepository
6
+ from .remote_link_repository import RemoteLinkRepository
7
+ from .sync_state_repository import SyncStateRepository
8
+
9
+ __all__ = [
10
+ "ProjectRepository",
11
+ "MilestoneRepository",
12
+ "IssueRepository",
13
+ "SyncStateRepository",
14
+ "RemoteLinkRepository",
15
+ ]
@@ -0,0 +1,223 @@
1
+ """Repository for issue persistence operations."""
2
+
3
+ from typing import Any
4
+
5
+ from roadmap.common.errors import OperationType, safe_operation
6
+ from roadmap.common.logging import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class IssueRepository:
12
+ """Handles all issue-related database operations."""
13
+
14
+ def __init__(self, get_connection, transaction):
15
+ """Initialize repository with database connection methods.
16
+
17
+ Args:
18
+ get_connection: Callable that returns sqlite3 Connection
19
+ transaction: Context manager for database transactions
20
+ """
21
+ self._get_connection = get_connection
22
+ self._transaction = transaction
23
+
24
+ @safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
25
+ @safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
26
+ def create(self, issue_data: dict[str, Any]) -> str:
27
+ """Create a new issue.
28
+
29
+ Args:
30
+ issue_data: Dictionary with issue fields
31
+
32
+ Returns:
33
+ Issue ID
34
+ """
35
+ with self._transaction() as conn:
36
+ project_id = issue_data.get("project_id")
37
+ if project_id:
38
+ exists = conn.execute(
39
+ "SELECT 1 FROM projects WHERE id = ?",
40
+ (project_id,),
41
+ ).fetchone()
42
+ if not exists:
43
+ logger.warning(
44
+ "issue_create_missing_project",
45
+ issue_id=issue_data.get("id"),
46
+ project_id=project_id,
47
+ )
48
+ project_id = None
49
+
50
+ milestone_id = issue_data.get("milestone_id")
51
+ if milestone_id:
52
+ exists = conn.execute(
53
+ "SELECT 1 FROM milestones WHERE id = ?",
54
+ (milestone_id,),
55
+ ).fetchone()
56
+ if not exists:
57
+ logger.warning(
58
+ "issue_create_missing_milestone",
59
+ issue_id=issue_data.get("id"),
60
+ milestone_id=milestone_id,
61
+ )
62
+ milestone_id = None
63
+
64
+ conn.execute(
65
+ """
66
+ INSERT INTO issues (id, project_id, milestone_id, title, headline, description, status, priority, issue_type, assignee, estimate_hours, due_date, metadata)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
68
+ """,
69
+ (
70
+ issue_data["id"],
71
+ project_id,
72
+ milestone_id,
73
+ issue_data.get("title"),
74
+ issue_data.get("headline", ""),
75
+ issue_data.get("description"),
76
+ issue_data.get("status", "open"),
77
+ issue_data.get("priority", "medium"),
78
+ issue_data.get("issue_type", "task"),
79
+ issue_data.get("assignee"),
80
+ issue_data.get("estimate_hours"),
81
+ issue_data.get("due_date"),
82
+ issue_data.get("metadata"),
83
+ ),
84
+ )
85
+
86
+ logger.info("Created issue", issue_id=issue_data["id"])
87
+ return issue_data["id"]
88
+
89
+ def get(self, issue_id: str) -> dict[str, Any] | None:
90
+ """Get issue by ID.
91
+
92
+ Args:
93
+ issue_id: Issue identifier
94
+
95
+ Returns:
96
+ Issue data or None if not found
97
+ """
98
+ conn = self._get_connection()
99
+ row = conn.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
100
+
101
+ return dict(row) if row else None
102
+
103
+ @safe_operation(OperationType.UPDATE, "Issue")
104
+ @safe_operation(OperationType.UPDATE, "Issue")
105
+ def update(self, issue_id: str, updates: dict[str, Any]) -> bool:
106
+ """Update issue.
107
+
108
+ Args:
109
+ issue_id: Issue identifier
110
+ updates: Dictionary of fields to update
111
+
112
+ Returns:
113
+ True if successful, False otherwise
114
+ """
115
+ if not updates:
116
+ return False
117
+
118
+ set_clause = ", ".join(f"{key} = ?" for key in updates.keys())
119
+ values = list(updates.values()) + [issue_id]
120
+
121
+ with self._transaction() as conn:
122
+ cursor = conn.execute(
123
+ f"""
124
+ UPDATE issues SET {set_clause} WHERE id = ?
125
+ """,
126
+ values,
127
+ )
128
+
129
+ updated = cursor.rowcount > 0
130
+ if updated:
131
+ logger.info(
132
+ "Updated issue", issue_id=issue_id, updates=list(updates.keys())
133
+ )
134
+
135
+ return updated
136
+
137
+ @safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
138
+ @safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
139
+ def delete(self, issue_id: str) -> bool:
140
+ """Delete issue.
141
+
142
+ Args:
143
+ issue_id: Issue identifier
144
+
145
+ Returns:
146
+ True if successful, False otherwise
147
+ """
148
+ with self._transaction() as conn:
149
+ cursor = conn.execute("DELETE FROM issues WHERE id = ?", (issue_id,))
150
+
151
+ deleted = cursor.rowcount > 0
152
+ if deleted:
153
+ logger.info("Deleted issue", issue_id=issue_id)
154
+
155
+ return deleted
156
+
157
+ @safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
158
+ def delete_many(self, issue_ids: list[str]) -> int:
159
+ """Delete multiple issues in a single transaction.
160
+
161
+ Batch delete is much more efficient than deleting one-by-one
162
+ as it avoids repeated cache invalidations and enumeration.
163
+
164
+ Args:
165
+ issue_ids: List of issue identifiers to delete
166
+
167
+ Returns:
168
+ Number of issues successfully deleted
169
+ """
170
+ if not issue_ids:
171
+ return 0
172
+
173
+ with self._transaction() as conn:
174
+ placeholders = ",".join("?" * len(issue_ids))
175
+ cursor = conn.execute(
176
+ f"DELETE FROM issues WHERE id IN ({placeholders})",
177
+ issue_ids,
178
+ )
179
+
180
+ deleted_count = cursor.rowcount
181
+ if deleted_count > 0:
182
+ logger.info(
183
+ "Deleted multiple issues",
184
+ count=deleted_count,
185
+ issue_count=len(issue_ids),
186
+ )
187
+
188
+ return deleted_count
189
+
190
+ @safe_operation(OperationType.UPDATE, "Issue")
191
+ @safe_operation(OperationType.UPDATE, "Issue")
192
+ def mark_archived(self, issue_id: str, archived: bool = True) -> bool:
193
+ """Mark an issue as archived or unarchived.
194
+
195
+ Args:
196
+ issue_id: Issue identifier
197
+ archived: True to archive, False to unarchive
198
+
199
+ Returns:
200
+ True if successful, False otherwise
201
+ """
202
+ with self._transaction() as conn:
203
+ if archived:
204
+ cursor = conn.execute(
205
+ "UPDATE issues SET archived = 1, archived_at = CURRENT_TIMESTAMP WHERE id = ?",
206
+ (issue_id,),
207
+ )
208
+ else:
209
+ cursor = conn.execute(
210
+ "UPDATE issues SET archived = 0, archived_at = NULL WHERE id = ?",
211
+ (issue_id,),
212
+ )
213
+
214
+ updated = cursor.rowcount > 0
215
+ if updated:
216
+ action = "archived" if archived else "unarchived"
217
+ logger.info(
218
+ "issue_status_changed",
219
+ issue_id=issue_id,
220
+ action=action,
221
+ )
222
+
223
+ return updated
@@ -0,0 +1,140 @@
1
+ """Repository for milestone persistence operations."""
2
+
3
+ from typing import Any
4
+
5
+ from roadmap.common.errors import OperationType, safe_operation
6
+ from roadmap.common.logging import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class MilestoneRepository:
12
+ """Handles all milestone-related database operations."""
13
+
14
+ def __init__(self, get_connection, transaction):
15
+ """Initialize repository with database connection methods.
16
+
17
+ Args:
18
+ get_connection: Callable that returns sqlite3 Connection
19
+ transaction: Context manager for database transactions
20
+ """
21
+ self._get_connection = get_connection
22
+ self._transaction = transaction
23
+
24
+ @safe_operation(OperationType.CREATE, "Milestone", include_traceback=True)
25
+ @safe_operation(OperationType.CREATE, "Milestone", include_traceback=True)
26
+ def create(self, milestone_data: dict[str, Any]) -> str:
27
+ """Create a new milestone.
28
+
29
+ Args:
30
+ milestone_data: Dictionary with milestone fields
31
+
32
+ Returns:
33
+ Milestone ID
34
+ """
35
+ with self._transaction() as conn:
36
+ conn.execute(
37
+ """
38
+ INSERT INTO milestones (id, project_id, title, description, status, due_date, metadata)
39
+ VALUES (?, ?, ?, ?, ?, ?, ?)
40
+ """,
41
+ (
42
+ milestone_data["id"],
43
+ milestone_data.get("project_id"),
44
+ milestone_data.get("title"),
45
+ milestone_data.get("description"),
46
+ milestone_data.get("status", "open"),
47
+ milestone_data.get("due_date"),
48
+ milestone_data.get("metadata"),
49
+ ),
50
+ )
51
+
52
+ logger.info("Created milestone", milestone_id=milestone_data["id"])
53
+ return milestone_data["id"]
54
+
55
+ def get(self, milestone_id: str) -> dict[str, Any] | None:
56
+ """Get milestone by ID.
57
+
58
+ Args:
59
+ milestone_id: Milestone identifier
60
+
61
+ Returns:
62
+ Milestone data or None if not found
63
+ """
64
+ conn = self._get_connection()
65
+ row = conn.execute(
66
+ "SELECT * FROM milestones WHERE id = ?", (milestone_id,)
67
+ ).fetchone()
68
+
69
+ return dict(row) if row else None
70
+
71
+ @safe_operation(OperationType.UPDATE, "Milestone")
72
+ @safe_operation(OperationType.UPDATE, "Milestone")
73
+ def update(self, milestone_id: str, updates: dict[str, Any]) -> bool:
74
+ """Update milestone.
75
+
76
+ Args:
77
+ milestone_id: Milestone identifier
78
+ updates: Dictionary of fields to update
79
+
80
+ Returns:
81
+ True if successful, False otherwise
82
+ """
83
+ if not updates:
84
+ return False
85
+
86
+ set_clause = ", ".join(f"{key} = ?" for key in updates.keys())
87
+ values = list(updates.values()) + [milestone_id]
88
+
89
+ with self._transaction() as conn:
90
+ cursor = conn.execute(
91
+ f"""
92
+ UPDATE milestones SET {set_clause} WHERE id = ?
93
+ """,
94
+ values,
95
+ )
96
+
97
+ updated = cursor.rowcount > 0
98
+ if updated:
99
+ logger.info(
100
+ "Updated milestone",
101
+ milestone_id=milestone_id,
102
+ updates=list(updates.keys()),
103
+ )
104
+
105
+ return updated
106
+
107
+ @safe_operation(OperationType.UPDATE, "Milestone")
108
+ @safe_operation(OperationType.UPDATE, "Milestone")
109
+ def mark_archived(self, milestone_id: str, archived: bool = True) -> bool:
110
+ """Mark a milestone as archived or unarchived.
111
+
112
+ Args:
113
+ milestone_id: Milestone identifier
114
+ archived: True to archive, False to unarchive
115
+
116
+ Returns:
117
+ True if successful, False otherwise
118
+ """
119
+ with self._transaction() as conn:
120
+ if archived:
121
+ cursor = conn.execute(
122
+ "UPDATE milestones SET archived = 1, archived_at = CURRENT_TIMESTAMP WHERE id = ?",
123
+ (milestone_id,),
124
+ )
125
+ else:
126
+ cursor = conn.execute(
127
+ "UPDATE milestones SET archived = 0, archived_at = NULL WHERE id = ?",
128
+ (milestone_id,),
129
+ )
130
+
131
+ updated = cursor.rowcount > 0
132
+ if updated:
133
+ action = "archived" if archived else "unarchived"
134
+ logger.info(
135
+ "milestone_status_changed",
136
+ milestone_id=milestone_id,
137
+ action=action,
138
+ )
139
+
140
+ return updated