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,711 @@
1
+ """YAML-based repository implementations.
2
+
3
+ This module provides concrete implementations of repository protocols
4
+ using YAML file storage, maintaining backward compatibility with existing
5
+ file-based storage while decoupling services from implementation details.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ from pathlib import Path
12
+
13
+ from roadmap.adapters.persistence.parser import (
14
+ IssueParser,
15
+ MilestoneParser,
16
+ ProjectParser,
17
+ )
18
+ from roadmap.adapters.persistence.storage import StateManager
19
+ from roadmap.common.logging import get_logger
20
+ from roadmap.core.domain import Status
21
+ from roadmap.core.domain.issue import Issue
22
+ from roadmap.core.domain.milestone import Milestone
23
+ from roadmap.core.domain.project import Project
24
+ from roadmap.core.repositories import (
25
+ IssueRepository,
26
+ MilestoneRepository,
27
+ ProjectRepository,
28
+ )
29
+ from roadmap.infrastructure.validation.file_enumeration import FileEnumerationService
30
+
31
+ logger = get_logger(__name__)
32
+
33
+
34
+ class YAMLIssueRepository(IssueRepository):
35
+ """Issue repository using YAML file storage.
36
+
37
+ This repository implements a file-based storage system with the following
38
+ organizational contract:
39
+
40
+ **File Organization by Milestone:**
41
+ - Issues with no milestone → `.roadmap/issues/backlog/`
42
+ - Issues with milestone "M1" → `.roadmap/issues/M1/`
43
+ - Archived issues with no milestone → `.roadmap/archive/issues/backlog/`
44
+ - Archived issues with milestone "M1" → `.roadmap/archive/issues/M1/`
45
+
46
+ **Duplicate Prevention:**
47
+ - save() method cleans up stale copies in other directories before writing
48
+ - update() with milestone change uses atomic file moves
49
+ - Only one file per issue exists at any time
50
+
51
+ **Key Invariants:**
52
+ - Each issue has exactly one file in the filesystem
53
+ - File location matches issue.milestone value
54
+ - issue.file_path always points to actual file location
55
+ - No orphaned copies left in subdirectories after moves
56
+
57
+ **Error Handling:**
58
+ - If cleanup fails, logs warning but continues (fail-open)
59
+ - If save fails, raises exception (fail-safe)
60
+ - Atomic rename ensures consistency
61
+ """
62
+
63
+ def __init__(self, db: StateManager, issues_dir: Path):
64
+ """Initialize issue repository.
65
+
66
+ Args:
67
+ db: State manager for metadata operations
68
+ issues_dir: Path to issues directory containing issue markdown files
69
+ """
70
+ self.db = db
71
+ self.issues_dir = issues_dir
72
+
73
+ def get(self, issue_id: str) -> Issue | None:
74
+ """Get a specific issue by ID.
75
+
76
+ Args:
77
+ issue_id: Issue identifier
78
+
79
+ Returns:
80
+ Issue object if found, None otherwise
81
+ """
82
+
83
+ def id_matcher(issue: Issue) -> bool:
84
+ return issue.id.startswith(issue_id)
85
+
86
+ issues = FileEnumerationService.enumerate_with_filter(
87
+ self.issues_dir,
88
+ IssueParser.parse_issue_file,
89
+ id_matcher,
90
+ )
91
+ if not issues:
92
+ archive_dir = self.issues_dir.parent / "archive" / "issues"
93
+ if archive_dir.exists():
94
+ issues = FileEnumerationService.enumerate_with_filter(
95
+ archive_dir,
96
+ IssueParser.parse_issue_file,
97
+ id_matcher,
98
+ )
99
+ return issues[0] if issues else None
100
+
101
+ def list(
102
+ self, milestone: str | None = None, status: str | None = None
103
+ ) -> list[Issue]:
104
+ """List issues with optional filtering (active issues only).
105
+
106
+ Args:
107
+ milestone: Optional milestone filter
108
+ status: Optional status filter
109
+
110
+ Returns:
111
+ List of active Issue objects matching filters (not including archived)
112
+ """
113
+ # Get issues from active directory only
114
+ issues = FileEnumerationService.enumerate_and_parse(
115
+ self.issues_dir,
116
+ IssueParser.parse_issue_file,
117
+ )
118
+
119
+ if milestone:
120
+ issues = [i for i in issues if i.milestone == milestone]
121
+ if status:
122
+ issues = [i for i in issues if i.status.value == status]
123
+
124
+ return issues
125
+
126
+ def list_all_including_archived(
127
+ self, milestone: str | None = None, status: str | None = None
128
+ ) -> list[Issue]:
129
+ """List all issues including archived (for sync operations).
130
+
131
+ Args:
132
+ milestone: Optional milestone filter
133
+ status: Optional status filter
134
+
135
+ Returns:
136
+ List of all Issue objects matching filters (from both active and archived)
137
+ """
138
+ # Get issues from active directory
139
+ issues = FileEnumerationService.enumerate_and_parse(
140
+ self.issues_dir,
141
+ IssueParser.parse_issue_file,
142
+ )
143
+
144
+ # Also get issues from archive directory if it exists
145
+ archive_dir = self.issues_dir.parent / "archive" / "issues"
146
+ if archive_dir.exists():
147
+ archived_issues = FileEnumerationService.enumerate_and_parse(
148
+ archive_dir,
149
+ IssueParser.parse_issue_file,
150
+ )
151
+ issues.extend(archived_issues)
152
+
153
+ if milestone:
154
+ issues = [i for i in issues if i.milestone == milestone]
155
+ if status:
156
+ issues = [i for i in issues if i.status.value == status]
157
+
158
+ return issues
159
+
160
+ def save(self, issue: Issue) -> None:
161
+ """Save/create an issue.
162
+
163
+ Ensures single file location by cleaning up any stale copies in other directories.
164
+ This prevents duplicate files when issues are moved between milestones or when
165
+ the issue filename changes (e.g., due to title updates).
166
+
167
+ Args:
168
+ issue: Issue object to save
169
+ """
170
+ archived = self._is_archived_issue(issue)
171
+
172
+ # Determine target directory based on milestone and archive state
173
+ target_dir = self._get_milestone_dir(issue.milestone, archived=archived)
174
+
175
+ # Create directory if it doesn't exist
176
+ target_dir.mkdir(parents=True, exist_ok=True)
177
+
178
+ # Check if file exists in other locations and remove it
179
+ # This handles issue moves between milestones
180
+ issue_path_target = target_dir / issue.filename
181
+ stale_files_removed = 0
182
+
183
+ # Search for any existing files with this issue ID in both active and archive
184
+ # trees so archive state changes do not leave duplicate copies behind.
185
+ issue_id_prefix = issue.id
186
+ for existing_file in self._find_issue_files_by_id(issue_id_prefix):
187
+ if existing_file != issue_path_target:
188
+ try:
189
+ existing_file.unlink()
190
+ stale_files_removed += 1
191
+ logger.debug(
192
+ "removed_stale_issue_file_by_id",
193
+ issue_id=issue.id,
194
+ old_filename=existing_file.name,
195
+ removed_path=str(existing_file),
196
+ target_path=str(issue_path_target),
197
+ )
198
+ except (OSError, PermissionError) as e:
199
+ logger.warning(
200
+ "failed_to_remove_stale_issue_file_by_id",
201
+ issue_id=issue.id,
202
+ old_filename=existing_file.name,
203
+ path=str(existing_file),
204
+ error=str(e),
205
+ )
206
+
207
+ # Save file to proper location
208
+ IssueParser.save_issue_file(issue, issue_path_target)
209
+ logger.debug(
210
+ "issue_saved",
211
+ issue_id=issue.id,
212
+ issue_filename=issue.filename,
213
+ milestone=issue.milestone,
214
+ target_directory=str(target_dir),
215
+ stale_files_removed=stale_files_removed,
216
+ )
217
+
218
+ # Set the file_path on the issue object so it reflects the saved location
219
+ issue.file_path = str(issue_path_target)
220
+
221
+ def update(self, issue_id: str, updates: dict) -> Issue | None:
222
+ """Update specific fields of an issue.
223
+
224
+ When milestone changes, moves file to appropriate directory and removes
225
+ any stale copies from other locations. For non-milestone updates, saves
226
+ to current file location without moving.
227
+
228
+ Args:
229
+ issue_id: Issue identifier
230
+ updates: Dictionary of field updates (may include milestone to trigger move)
231
+
232
+ Returns:
233
+ Updated Issue object if found, None otherwise
234
+ """
235
+ issue = self.get(issue_id)
236
+ if not issue:
237
+ logger.debug("issue_not_found_for_update", issue_id=issue_id)
238
+ return None
239
+
240
+ # Track old state before modification
241
+ old_milestone = issue.milestone
242
+ old_filename = issue.filename
243
+
244
+ logger.debug(
245
+ "updating_issue",
246
+ issue_id=issue_id,
247
+ old_milestone=old_milestone,
248
+ update_fields=list(updates.keys()),
249
+ )
250
+
251
+ # Apply updates to issue object
252
+ for key, value in updates.items():
253
+ if hasattr(issue, key):
254
+ setattr(issue, key, value)
255
+
256
+ # Handle file system changes based on what was updated
257
+ if "milestone" in updates and old_milestone != issue.milestone:
258
+ self._handle_milestone_change(issue, old_milestone)
259
+ elif old_filename != issue.filename:
260
+ self._handle_filename_change(issue, old_filename)
261
+
262
+ # Save and return updated issue
263
+ self.save(issue)
264
+ return issue
265
+
266
+ def _handle_milestone_change(self, issue: Issue, old_milestone: str | None) -> None:
267
+ """Handle file move when issue's milestone changes.
268
+
269
+ Args:
270
+ issue: Updated issue object
271
+ old_milestone: Previous milestone value
272
+ """
273
+ archived = self._is_archived_issue(issue)
274
+ old_path = self._get_issue_path(
275
+ old_milestone, issue.filename, archived=archived
276
+ )
277
+ new_path = self._get_issue_path(
278
+ issue.milestone, issue.filename, archived=archived
279
+ )
280
+
281
+ # Create destination directory if needed
282
+ new_path.parent.mkdir(parents=True, exist_ok=True)
283
+
284
+ # Move file to new location
285
+ if old_path.exists():
286
+ try:
287
+ shutil.move(str(old_path), str(new_path))
288
+ except (OSError, PermissionError):
289
+ # If move fails, save to new location directly
290
+ IssueParser.save_issue_file(issue, new_path)
291
+ else:
292
+ # File doesn't exist at old location, save to new location
293
+ IssueParser.save_issue_file(issue, new_path)
294
+
295
+ # Clean up stale copies from other directories
296
+ self._cleanup_stale_files(issue.filename, new_path.parent)
297
+
298
+ # Update the issue's file path
299
+ issue.file_path = str(new_path)
300
+
301
+ logger.info(
302
+ "issue_milestone_updated",
303
+ issue_id=issue.id,
304
+ old_milestone=old_milestone,
305
+ new_milestone=issue.milestone,
306
+ new_path=str(new_path),
307
+ )
308
+
309
+ def _handle_filename_change(self, issue: Issue, old_filename: str) -> None:
310
+ """Handle file removal when issue's filename changes.
311
+
312
+ Args:
313
+ issue: Updated issue object
314
+ old_filename: Previous filename
315
+ """
316
+ # Get current directory for issue
317
+ target_dir = self._get_milestone_dir(
318
+ issue.milestone, archived=self._is_archived_issue(issue)
319
+ )
320
+
321
+ # Remove old file from target directory
322
+ old_path = target_dir / old_filename
323
+ if old_path.exists():
324
+ try:
325
+ old_path.unlink()
326
+ logger.debug(
327
+ "removed_old_filename_after_update",
328
+ issue_id=issue.id,
329
+ old_filename=old_filename,
330
+ )
331
+ except (OSError, PermissionError) as e:
332
+ logger.warning(
333
+ "failed_to_remove_old_filename",
334
+ issue_id=issue.id,
335
+ error=str(e),
336
+ )
337
+
338
+ # Also check for stale copies in other directories
339
+ self._cleanup_stale_files(old_filename, target_dir)
340
+
341
+ def _get_issue_path(
342
+ self, milestone: str | None, filename: str, archived: bool = False
343
+ ) -> Path:
344
+ """Get the full path for an issue file.
345
+
346
+ Args:
347
+ milestone: Issue milestone (None or "backlog" means backlog dir)
348
+ filename: Issue filename
349
+
350
+ Returns:
351
+ Full path to issue file
352
+ """
353
+ return self._get_milestone_dir(milestone, archived=archived) / filename
354
+
355
+ def _get_milestone_dir(self, milestone: str | None, archived: bool = False) -> Path:
356
+ """Get directory for a milestone.
357
+
358
+ Args:
359
+ milestone: Milestone name (None or "backlog" means backlog)
360
+
361
+ Returns:
362
+ Directory path
363
+ """
364
+ root_dir = self._get_issue_root(archived=archived)
365
+ if milestone and milestone != "backlog":
366
+ return root_dir / milestone
367
+ return root_dir / "backlog"
368
+
369
+ def _get_issue_root(self, archived: bool = False) -> Path:
370
+ """Get the root directory for active or archived issues."""
371
+ if archived:
372
+ return self.issues_dir.parent / "archive" / "issues"
373
+ return self.issues_dir
374
+
375
+ def _find_issue_files_by_id(self, issue_id_prefix: str) -> list[Path]:
376
+ """Find issue files across active and archive trees by ID prefix."""
377
+ issue_files: list[Path] = []
378
+ for root_dir in (
379
+ self._get_issue_root(archived=False),
380
+ self._get_issue_root(archived=True),
381
+ ):
382
+ if not root_dir.exists():
383
+ continue
384
+ issue_files.extend(root_dir.rglob(f"{issue_id_prefix}-*.md"))
385
+ return issue_files
386
+
387
+ def _is_archived_issue(self, issue: Issue) -> bool:
388
+ """Determine whether an issue should live under the archive tree."""
389
+ status = getattr(issue.status, "value", issue.status)
390
+ return bool(issue.archived or status == Status.ARCHIVED.value)
391
+
392
+ def _cleanup_stale_files(self, filename: str, exclude_dir: Path) -> None:
393
+ """Remove stale copies of a file from other directories.
394
+
395
+ Args:
396
+ filename: Filename to search for
397
+ exclude_dir: Directory to exclude from cleanup
398
+ """
399
+ for root_dir in (
400
+ self._get_issue_root(archived=False),
401
+ self._get_issue_root(archived=True),
402
+ ):
403
+ if not root_dir.exists():
404
+ continue
405
+ for subdir in root_dir.glob("*"):
406
+ if subdir.is_dir() and subdir != exclude_dir:
407
+ stale_path = subdir / filename
408
+ if stale_path.exists():
409
+ try:
410
+ stale_path.unlink()
411
+ except (OSError, PermissionError):
412
+ pass
413
+
414
+ def delete(self, issue_id: str) -> bool:
415
+ """Delete an issue.
416
+
417
+ Args:
418
+ issue_id: Issue identifier
419
+
420
+ Returns:
421
+ True if deleted, False if not found
422
+ """
423
+ issue = self.get(issue_id)
424
+ if not issue:
425
+ return False
426
+
427
+ issue_path = self._get_issue_path(
428
+ issue.milestone,
429
+ issue.filename,
430
+ archived=self._is_archived_issue(issue),
431
+ )
432
+
433
+ if issue_path.exists():
434
+ try:
435
+ issue_path.unlink()
436
+ except (OSError, PermissionError):
437
+ return False
438
+
439
+ return True
440
+
441
+ def delete_many(self, issue_ids: list[str]) -> int:
442
+ """Delete multiple issues in a single batch operation.
443
+
444
+ Args:
445
+ issue_ids: List of issue identifiers to delete
446
+
447
+ Returns:
448
+ Number of issues successfully deleted
449
+ """
450
+ if not issue_ids:
451
+ return 0
452
+
453
+ deleted_count = 0
454
+ for issue_id in issue_ids:
455
+ if self.delete(issue_id):
456
+ deleted_count += 1
457
+
458
+ return deleted_count
459
+
460
+
461
+ class YAMLMilestoneRepository(MilestoneRepository):
462
+ """Milestone repository using YAML file storage.
463
+
464
+ This repository stores milestone files in a flat structure:
465
+
466
+ **File Organization:**
467
+ - All milestones stored in `.roadmap/milestones/`
468
+ - Each milestone is one markdown file
469
+ - No subdirectories (unlike issues)
470
+ - Archived milestones → `.roadmap/archive/milestones/`
471
+
472
+ **Relationship to Issues:**
473
+ - Milestone names are used as directory names in `.roadmap/issues/{milestone_name}`
474
+ - Deleting milestone doesn't delete associated issues (only the milestone metadata)
475
+ - Issues maintain reference to milestone by name
476
+
477
+ **Update Behavior:**
478
+ - Updates do NOT move files
479
+ - All updates saved to same file location
480
+ - No stale file cleanup needed (flat structure)
481
+ """
482
+
483
+ def __init__(self, db: StateManager, milestones_dir: Path):
484
+ """Initialize milestone repository.
485
+
486
+ Args:
487
+ db: State manager for metadata operations
488
+ milestones_dir: Path to milestones directory
489
+ """
490
+ self.db = db
491
+ self.milestones_dir = milestones_dir
492
+
493
+ def get(self, milestone_id: str) -> Milestone | None:
494
+ """Get a specific milestone by ID or name.
495
+
496
+ Args:
497
+ milestone_id: Milestone identifier or name
498
+
499
+ Returns:
500
+ Milestone object if found, None otherwise
501
+ """
502
+
503
+ def id_matcher(milestone: Milestone) -> bool:
504
+ return milestone.name.lower().startswith(milestone_id.lower())
505
+
506
+ milestones = FileEnumerationService.enumerate_with_filter(
507
+ self.milestones_dir,
508
+ MilestoneParser.parse_milestone_file,
509
+ id_matcher,
510
+ warn_on_parse_error=True,
511
+ )
512
+ return milestones[0] if milestones else None
513
+
514
+ def list(self) -> list[Milestone]:
515
+ """List all milestones.
516
+
517
+ Returns:
518
+ List of all Milestone objects
519
+ """
520
+ return FileEnumerationService.enumerate_and_parse(
521
+ self.milestones_dir,
522
+ MilestoneParser.parse_milestone_file,
523
+ warn_on_parse_error=True,
524
+ )
525
+
526
+ def save(self, milestone: Milestone) -> None:
527
+ """Save/create a milestone.
528
+
529
+ Args:
530
+ milestone: Milestone object to save
531
+ """
532
+ milestone_path = self.milestones_dir / milestone.filename
533
+ MilestoneParser.save_milestone_file(milestone, milestone_path)
534
+
535
+ def update(self, milestone_id: str, updates: dict) -> Milestone | None:
536
+ """Update specific fields of a milestone.
537
+
538
+ Args:
539
+ milestone_id: Milestone identifier
540
+ updates: Dictionary of field updates
541
+
542
+ Returns:
543
+ Updated Milestone object if found, None otherwise
544
+ """
545
+ milestone = self.get(milestone_id)
546
+ if not milestone:
547
+ return None
548
+
549
+ # Update fields
550
+ for key, value in updates.items():
551
+ if hasattr(milestone, key):
552
+ setattr(milestone, key, value)
553
+
554
+ # Save back
555
+ self.save(milestone)
556
+ return milestone
557
+
558
+ def delete(self, milestone_id: str) -> bool:
559
+ """Delete a milestone.
560
+
561
+ Args:
562
+ milestone_id: Milestone identifier
563
+
564
+ Returns:
565
+ True if deleted, False if not found
566
+ """
567
+ milestone = self.get(milestone_id)
568
+ if not milestone:
569
+ return False
570
+
571
+ milestone_path = self.milestones_dir / milestone.filename
572
+ if milestone_path.exists():
573
+ try:
574
+ milestone_path.unlink()
575
+ except (OSError, PermissionError):
576
+ return False
577
+
578
+ return True
579
+
580
+
581
+ class YAMLProjectRepository(ProjectRepository):
582
+ """Project repository using YAML file storage.
583
+
584
+ This repository stores project files in a flat structure:
585
+
586
+ **File Organization:**
587
+ - All projects stored in `.roadmap/projects/`
588
+ - Each project is one markdown file
589
+ - No subdirectories
590
+ - Archived projects → `.roadmap/archive/projects/`
591
+
592
+ **Update Behavior:**
593
+ - Updates do NOT move files
594
+ - All updates saved to same file location
595
+ - No stale file cleanup needed (flat structure)
596
+
597
+ **Note:** Projects are distinct from issues and milestones. They can reference
598
+ milestones but don't affect the issue file organization.
599
+ """
600
+
601
+ def __init__(self, db: StateManager, projects_dir: Path):
602
+ """Initialize project repository.
603
+
604
+ Args:
605
+ db: State manager for metadata operations
606
+ projects_dir: Path to projects directory
607
+ """
608
+ self.db = db
609
+ self.projects_dir = projects_dir
610
+
611
+ def get(self, project_id: str) -> Project | None:
612
+ """Get a specific project by ID.
613
+
614
+ Args:
615
+ project_id: Project identifier
616
+
617
+ Returns:
618
+ Project object if found, None otherwise
619
+ """
620
+
621
+ def id_matcher(project: Project) -> bool:
622
+ return project.id.startswith(project_id)
623
+
624
+ projects = FileEnumerationService.enumerate_with_filter(
625
+ self.projects_dir,
626
+ ProjectParser.parse_project_file,
627
+ id_matcher,
628
+ )
629
+ return projects[0] if projects else None
630
+
631
+ def list(self) -> list[Project]:
632
+ """List all projects.
633
+
634
+ Returns:
635
+ List of all Project objects
636
+ """
637
+ projects = FileEnumerationService.enumerate_and_parse(
638
+ self.projects_dir,
639
+ ProjectParser.parse_project_file,
640
+ )
641
+ return sorted(projects, key=lambda x: x.created)
642
+
643
+ def save(self, project: Project) -> None:
644
+ """Save/create a project.
645
+
646
+ Args:
647
+ project: Project object to save
648
+ """
649
+ project_path = self.projects_dir / project.filename
650
+ ProjectParser.save_project_file(project, project_path)
651
+
652
+ # Persist metadata to database (non-blocking)
653
+ try:
654
+ self.db.create_project(
655
+ {
656
+ "title": project.name,
657
+ "description": project.content,
658
+ "status": project.status.value,
659
+ }
660
+ )
661
+ except Exception as e:
662
+ logger.debug(
663
+ "failed_to_persist_project_to_database",
664
+ error=str(e),
665
+ severity="operational",
666
+ )
667
+
668
+ def update(self, project_id: str, updates: dict) -> Project | None:
669
+ """Update specific fields of a project.
670
+
671
+ Args:
672
+ project_id: Project identifier
673
+ updates: Dictionary of field updates
674
+
675
+ Returns:
676
+ Updated Project object if found, None otherwise
677
+ """
678
+ project = self.get(project_id)
679
+ if not project:
680
+ return None
681
+
682
+ # Update fields
683
+ for key, value in updates.items():
684
+ if hasattr(project, key):
685
+ setattr(project, key, value)
686
+
687
+ # Save back
688
+ self.save(project)
689
+ return project
690
+
691
+ def delete(self, project_id: str) -> bool:
692
+ """Delete a project.
693
+
694
+ Args:
695
+ project_id: Project identifier
696
+
697
+ Returns:
698
+ True if deleted, False if not found
699
+ """
700
+ project = self.get(project_id)
701
+ if not project:
702
+ return False
703
+
704
+ project_path = self.projects_dir / project.filename
705
+ if project_path.exists():
706
+ try:
707
+ project_path.unlink()
708
+ except (OSError, PermissionError):
709
+ return False
710
+
711
+ return True