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,346 @@
1
+ """GitHub issue client service for roadmap application.
2
+
3
+ This module handles fetching and syncing GitHub issues with internal roadmap issues.
4
+ It provides a simple interface for GitHub issue operations without requiring a
5
+ GitHub repository to be configured.
6
+ """
7
+
8
+ import os
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from roadmap.adapters.github.handlers.base import GitHubAPIError
12
+ from roadmap.common.logging import get_logger
13
+ from roadmap.common.logging.error_logging import (
14
+ log_external_service_error,
15
+ log_validation_error,
16
+ )
17
+ from roadmap.common.observability.instrumentation import traced
18
+ from roadmap.infrastructure.github_gateway import GitHubGateway
19
+
20
+ if TYPE_CHECKING:
21
+ from roadmap.core.interfaces import GitHubBackendInterface # noqa: F401
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class GitHubIssueClient:
27
+ """Client for fetching and managing GitHub issues.
28
+
29
+ This client allows operations on GitHub issues without requiring
30
+ owner/repo configuration upfront. Configuration can be provided
31
+ per-request or via environment variables.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ token: str | None = None,
37
+ github_backend: "GitHubBackendInterface | None" = None,
38
+ ):
39
+ """Initialize GitHub issue client.
40
+
41
+ Args:
42
+ token: Optional GitHub personal access token.
43
+ If not provided, will look for GITHUB_TOKEN environment variable.
44
+ github_backend: Optional GitHub backend interface. If not provided,
45
+ will create GitHubBackendAdapter instances per-request.
46
+
47
+ Raises:
48
+ GitHubAPIError: If no token is available
49
+ """
50
+ self.token = token or os.getenv("GITHUB_TOKEN")
51
+ if not self.token:
52
+ raise GitHubAPIError(
53
+ "GitHub token is required. Set GITHUB_TOKEN environment variable "
54
+ "or provide token to GitHubIssueClient."
55
+ )
56
+ self._github_backend = github_backend
57
+ logger.debug("github_issue_client_initialized")
58
+
59
+ @traced("fetch_issue")
60
+ def fetch_issue(self, owner: str, repo: str, issue_number: int) -> dict[str, Any]:
61
+ """Fetch a GitHub issue by number.
62
+
63
+ Args:
64
+ owner: Repository owner (username or organization)
65
+ repo: Repository name
66
+ issue_number: GitHub issue number (must be positive integer)
67
+
68
+ Returns:
69
+ Dictionary with issue details:
70
+ - number: Issue number
71
+ - title: Issue title
72
+ - body: Issue description
73
+ - state: 'open' or 'closed'
74
+ - labels: List of label names
75
+ - assignees: List of assignee usernames
76
+ - milestone: Milestone title or None
77
+ - url: GitHub issue URL
78
+ - created_at: ISO format timestamp
79
+ - updated_at: ISO format timestamp
80
+
81
+ Raises:
82
+ GitHubAPIError: If issue not found or API error occurs
83
+ ValueError: If issue_number is invalid
84
+ """
85
+ logger.info(
86
+ "fetching_github_issue", owner=owner, repo=repo, number=issue_number
87
+ )
88
+
89
+ try:
90
+ # Use injected backend or create adapter on-demand
91
+ if self._github_backend is None:
92
+ client = GitHubGateway.get_github_client(
93
+ {"token": self.token, "owner": owner, "repo": repo}
94
+ )
95
+ issue_data = client.fetch_issue(issue_number)
96
+ else:
97
+ issue_data = self._github_backend.get_issue(str(issue_number))
98
+
99
+ logger.debug("github_issue_fetched_successfully", number=issue_number)
100
+ return issue_data if isinstance(issue_data, dict) else {}
101
+ except ValueError as e:
102
+ log_validation_error(
103
+ e,
104
+ entity_type="GitHubIssue",
105
+ field_name="issue_number",
106
+ proposed_value=issue_number,
107
+ )
108
+ logger.warning(
109
+ "invalid_github_issue_number", error=str(e), severity="data_error"
110
+ )
111
+ raise
112
+ except GitHubAPIError as e:
113
+ log_external_service_error(
114
+ e,
115
+ service_name="GitHub",
116
+ operation="fetch_issue",
117
+ )
118
+ logger.warning(
119
+ "github_issue_fetch_failed", error=str(e), severity="infrastructure"
120
+ )
121
+ raise
122
+
123
+ @traced("validate_token")
124
+ def validate_token(self) -> tuple[bool, str]:
125
+ """Validate GitHub token by making a test API call.
126
+
127
+ Returns:
128
+ Tuple of (is_valid, message)
129
+ - (True, "Token is valid (authenticated as USERNAME)") if successful
130
+ - (False, error_message) if invalid or error
131
+ """
132
+ logger.info("validating_github_token")
133
+ try:
134
+ # Create a backend adapter with minimal configuration (needs owner/repo for initialization)
135
+ # We'll use a dummy repo for validation since we only care about authentication
136
+ client = GitHubGateway.get_github_client(
137
+ {"token": self.token, "owner": "dummy", "repo": "dummy"}
138
+ )
139
+ is_valid, message = client.validate_github_token()
140
+
141
+ if is_valid:
142
+ logger.debug("github_token_validated", authenticated_user=message)
143
+ else:
144
+ logger.warning(
145
+ "github_token_validation_failed",
146
+ error_message=message,
147
+ severity="config_error",
148
+ )
149
+ return is_valid, message
150
+ except GitHubAPIError as e:
151
+ log_external_service_error(
152
+ e,
153
+ service_name="GitHub",
154
+ operation="validate_token",
155
+ )
156
+ return False, str(e)
157
+
158
+ @traced("issue_exists")
159
+ def issue_exists(self, owner: str, repo: str, issue_number: int) -> bool:
160
+ """Check if a GitHub issue exists.
161
+
162
+ Args:
163
+ owner: Repository owner (username or organization)
164
+ repo: Repository name
165
+ issue_number: GitHub issue number
166
+
167
+ Returns:
168
+ True if issue exists, False otherwise
169
+ """
170
+ logger.info(
171
+ "checking_github_issue_exists", owner=owner, repo=repo, number=issue_number
172
+ )
173
+ try:
174
+ self.fetch_issue(owner, repo, issue_number)
175
+ logger.debug("github_issue_exists", number=issue_number)
176
+ return True
177
+ except GitHubAPIError:
178
+ logger.debug("github_issue_does_not_exist", number=issue_number)
179
+ return False
180
+
181
+ @traced("get_issue_diff")
182
+ def get_issue_diff(
183
+ self,
184
+ owner: str,
185
+ repo: str,
186
+ issue_number: int,
187
+ local_data: dict[str, Any],
188
+ ) -> dict[str, Any]:
189
+ """Get differences between local issue data and GitHub issue.
190
+
191
+ Args:
192
+ owner: Repository owner
193
+ repo: Repository name
194
+ issue_number: GitHub issue number
195
+ local_data: Local issue data to compare (keys: title, body, state, labels, assignees)
196
+
197
+ Returns:
198
+ Dictionary with changes:
199
+ - has_changes: bool
200
+ - title_changed: bool (if different)
201
+ - body_changed: bool (if different)
202
+ - state_changed: bool (if different)
203
+ - labels_changed: bool (if different)
204
+ - assignees_changed: bool (if different)
205
+ - github_data: dict with full GitHub issue data
206
+ - changes: dict with field-level details
207
+
208
+ Raises:
209
+ GitHubAPIError: If issue fetch fails
210
+ """
211
+ logger.info(
212
+ "comparing_issue_with_github", owner=owner, repo=repo, number=issue_number
213
+ )
214
+
215
+ github_data = self.fetch_issue(owner, repo, issue_number)
216
+
217
+ changes = {}
218
+ has_changes = False
219
+
220
+ # Compare fields
221
+ fields_to_compare = ["title", "body", "state"]
222
+ for field in fields_to_compare:
223
+ local_value = local_data.get(field, "")
224
+ github_value = github_data.get(field, "")
225
+ if local_value != github_value:
226
+ changes[field] = {
227
+ "local": local_value,
228
+ "github": github_value,
229
+ "changed": True,
230
+ }
231
+ has_changes = True
232
+
233
+ # Compare lists (labels, assignees)
234
+ for field in ["labels", "assignees"]:
235
+ local_value = sorted(set(local_data.get(field, [])))
236
+ github_value = sorted(set(github_data.get(field, [])))
237
+ if local_value != github_value:
238
+ changes[field] = {
239
+ "local": local_value,
240
+ "github": github_value,
241
+ "changed": True,
242
+ }
243
+ has_changes = True
244
+
245
+ result = {
246
+ "has_changes": has_changes,
247
+ "github_data": github_data,
248
+ "changes": changes,
249
+ }
250
+
251
+ logger.debug(
252
+ "issue_diff_computed", has_changes=has_changes, fields_changed=len(changes)
253
+ )
254
+ return result
255
+
256
+ @traced("get_issues")
257
+ def get_issues(
258
+ self,
259
+ owner: str,
260
+ repo: str,
261
+ state: str = "all",
262
+ labels: list[str] | None = None,
263
+ milestone: str | None = None,
264
+ assignee: str | None = None,
265
+ per_page: int = 100,
266
+ ) -> list[dict[str, Any]]:
267
+ """Get issues from a GitHub repository.
268
+
269
+ Args:
270
+ owner: Repository owner (username or organization)
271
+ repo: Repository name
272
+ state: Issue state filter ('open', 'closed', 'all')
273
+ labels: Optional list of label names to filter by
274
+ milestone: Optional milestone title to filter by
275
+ assignee: Optional assignee username to filter by
276
+ per_page: Number of issues per page (max 100)
277
+
278
+ Returns:
279
+ List of issue dictionaries from GitHub API
280
+
281
+ Raises:
282
+ GitHubAPIError: If API call fails
283
+ """
284
+ logger.info(
285
+ "fetching_github_issues",
286
+ owner=owner,
287
+ repo=repo,
288
+ state=state,
289
+ limit=per_page,
290
+ )
291
+
292
+ try:
293
+ # Use injected backend or create adapter on-demand
294
+ if self._github_backend is None:
295
+ client = GitHubGateway.get_github_client(
296
+ {"token": self.token, "owner": owner, "repo": repo}
297
+ )
298
+ issues_data = client.get_issues(
299
+ state, labels, milestone, assignee, per_page
300
+ )
301
+ else:
302
+ # If backend provided, use it - backend get_issues() takes no parameters
303
+ if hasattr(self._github_backend, "get_issues"):
304
+ all_issues = self._github_backend.get_issues()
305
+ # Filter results if needed (backend returns all issues)
306
+ issues_data = (
307
+ all_issues
308
+ if isinstance(all_issues, list)
309
+ else list(all_issues.values())
310
+ if isinstance(all_issues, dict)
311
+ else []
312
+ )
313
+ else:
314
+ issues_data = []
315
+
316
+ logger.debug(
317
+ "github_issues_fetched",
318
+ owner=owner,
319
+ repo=repo,
320
+ count=len(issues_data or []),
321
+ )
322
+ return issues_data if isinstance(issues_data, list) else []
323
+
324
+ except GitHubAPIError as e:
325
+ log_external_service_error(
326
+ e,
327
+ service_name="GitHub",
328
+ operation="get_issues",
329
+ )
330
+ logger.warning(
331
+ "github_issues_fetch_failed", error=str(e), severity="infrastructure"
332
+ )
333
+ raise
334
+ except Exception as e:
335
+ log_external_service_error(
336
+ e,
337
+ service_name="GitHub",
338
+ operation="get_issues",
339
+ )
340
+ logger.error(
341
+ "github_issues_fetch_error",
342
+ error=str(e),
343
+ error_type=type(e).__name__,
344
+ severity="infrastructure",
345
+ )
346
+ raise GitHubAPIError(f"Failed to fetch issues: {str(e)}") from e
@@ -0,0 +1 @@
1
+ """Package initialization file."""
@@ -0,0 +1,202 @@
1
+ """Service for cleaning up old backup files."""
2
+
3
+ from datetime import UTC, datetime, timedelta
4
+ from pathlib import Path
5
+ from typing import TypedDict
6
+
7
+ from roadmap.common.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class BackupInfo(TypedDict):
13
+ """Information about a backup file."""
14
+
15
+ path: Path
16
+ mtime: datetime
17
+ size: int
18
+
19
+
20
+ class BackupCleanupResult:
21
+ """Result of backup cleanup operation."""
22
+
23
+ def __init__(self):
24
+ """Initialize BackupCleanupResult."""
25
+ self.deleted_count: int = 0
26
+ self.failed_count: int = 0
27
+ self.total_freed_bytes: int = 0
28
+ self.files_deleted: list[Path] = []
29
+ self.files_failed: list[tuple[Path, str]] = []
30
+
31
+ def add_deleted(self, path: Path, size: int) -> None:
32
+ """Record a deleted file."""
33
+ self.deleted_count += 1
34
+ self.total_freed_bytes += size
35
+ self.files_deleted.append(path)
36
+
37
+ def add_failed(self, path: Path, error: str) -> None:
38
+ """Record a file that failed to delete."""
39
+ self.failed_count += 1
40
+ self.files_failed.append((path, error))
41
+
42
+ @property
43
+ def freed_mb(self) -> float:
44
+ """Get freed space in MB."""
45
+ return self.total_freed_bytes / (1024 * 1024)
46
+
47
+
48
+ class BackupCleanupService:
49
+ """Cleans up old backup files based on age and count."""
50
+
51
+ def cleanup_backups(
52
+ self,
53
+ roadmap_dir: Path,
54
+ keep: int = 10,
55
+ days: int | None = None,
56
+ dry_run: bool = False,
57
+ ) -> BackupCleanupResult:
58
+ """Clean up old backup files.
59
+
60
+ Args:
61
+ roadmap_dir: Roadmap directory
62
+ keep: Number of most recent backups to keep per issue
63
+ days: Delete backups older than this many days
64
+ dry_run: If True, don't delete files
65
+
66
+ Returns:
67
+ BackupCleanupResult with deletion statistics
68
+ """
69
+ backups_dir = roadmap_dir / "backups"
70
+ result = BackupCleanupResult()
71
+
72
+ if not backups_dir.exists():
73
+ logger.info("No backups directory found")
74
+ return result
75
+
76
+ # Get files to delete
77
+ files_to_delete = self._select_backups_for_deletion(backups_dir, keep, days)
78
+
79
+ if not files_to_delete:
80
+ logger.info("No backups to clean up")
81
+ return result
82
+
83
+ # Dry run: just record what would be deleted
84
+ if dry_run:
85
+ for backup in files_to_delete:
86
+ result.add_deleted(backup["path"], backup["size"])
87
+ logger.info(
88
+ "Dry run: would delete backups",
89
+ count=len(files_to_delete),
90
+ freed_mb=result.freed_mb,
91
+ )
92
+ return result
93
+
94
+ # Actually delete files
95
+ for backup in files_to_delete:
96
+ try:
97
+ backup["path"].unlink()
98
+ result.add_deleted(backup["path"], backup["size"])
99
+ logger.info("Deleted backup file", file=backup["path"].name)
100
+ except Exception as e:
101
+ result.add_failed(backup["path"], str(e))
102
+ logger.warning(
103
+ "Failed to delete backup",
104
+ file=backup["path"].name,
105
+ error=str(e),
106
+ severity="operational",
107
+ )
108
+
109
+ return result
110
+
111
+ @staticmethod
112
+ @staticmethod
113
+ def _group_backups_by_issue(
114
+ backup_files: list[Path],
115
+ ) -> dict[str, list[BackupInfo]]:
116
+ """Group backup files by issue ID.
117
+
118
+ Args:
119
+ backup_files: List of backup file paths
120
+
121
+ Returns:
122
+ Dict of issue_key -> list of BackupInfo
123
+ """
124
+ backups_by_issue: dict[str, list[BackupInfo]] = {}
125
+ for backup_file in backup_files:
126
+ parts = backup_file.stem.split("_")
127
+ issue_key = "_".join(parts[:-1])
128
+
129
+ if issue_key not in backups_by_issue:
130
+ backups_by_issue[issue_key] = []
131
+
132
+ stat = backup_file.stat()
133
+ backups_by_issue[issue_key].append(
134
+ {
135
+ "path": backup_file,
136
+ "mtime": datetime.fromtimestamp(stat.st_mtime, tz=UTC),
137
+ "size": stat.st_size,
138
+ }
139
+ )
140
+
141
+ return backups_by_issue
142
+
143
+ @staticmethod
144
+ def _should_delete_backup(
145
+ idx: int, backup: BackupInfo, keep: int, cutoff_date: datetime | None
146
+ ) -> bool:
147
+ """Determine if backup should be deleted.
148
+
149
+ Args:
150
+ idx: Index in sorted backup list
151
+ backup: Backup info dict
152
+ keep: Keep this many most recent backups
153
+ cutoff_date: Delete backups older than this date
154
+
155
+ Returns:
156
+ True if backup should be deleted
157
+ """
158
+ # Delete if beyond keep count
159
+ if idx >= keep:
160
+ return True
161
+
162
+ # Delete if older than cutoff
163
+ if cutoff_date and backup["mtime"] < cutoff_date:
164
+ return True
165
+
166
+ return False
167
+
168
+ @staticmethod
169
+ def _select_backups_for_deletion(
170
+ backups_dir: Path, keep: int, days: int | None
171
+ ) -> list[BackupInfo]:
172
+ """Select backup files to delete based on age and count.
173
+
174
+ Args:
175
+ backups_dir: Directory containing backup files
176
+ keep: Keep this many most recent backups per issue
177
+ days: Delete backups older than this many days
178
+
179
+ Returns:
180
+ List of BackupInfo dicts for files to delete
181
+ """
182
+ backup_files = list(backups_dir.glob("*.backup.md"))
183
+ if not backup_files:
184
+ return []
185
+
186
+ # Group backups by issue ID
187
+ backups_by_issue = BackupCleanupService._group_backups_by_issue(backup_files)
188
+
189
+ # Determine files to delete
190
+ files_to_delete: list[BackupInfo] = []
191
+ cutoff_date = datetime.now(UTC) - timedelta(days=days) if days else None
192
+
193
+ for _issue_key, backups in backups_by_issue.items():
194
+ backups.sort(key=lambda x: x["mtime"], reverse=True)
195
+
196
+ for idx, backup in enumerate(backups):
197
+ if BackupCleanupService._should_delete_backup(
198
+ idx, backup, keep, cutoff_date
199
+ ):
200
+ files_to_delete.append(backup)
201
+
202
+ return files_to_delete
@@ -0,0 +1,90 @@
1
+ """Data integrity validator service orchestrator.
2
+
3
+ Coordinates all data integrity validators and provides a unified interface
4
+ for running health checks across the system.
5
+ """
6
+
7
+ from roadmap.common.logging import get_logger
8
+ from roadmap.core.services.validator_base import HealthStatus
9
+ from roadmap.core.services.validators import (
10
+ ArchivableIssuesValidator,
11
+ ArchivableMilestonesValidator,
12
+ BackupValidator,
13
+ DataIntegrityValidator,
14
+ DuplicateIssuesValidator,
15
+ DuplicateMilestonesValidator,
16
+ FolderStructureValidator,
17
+ OrphanedIssuesValidator,
18
+ OrphanedMilestonesValidator,
19
+ )
20
+ from roadmap.core.services.validators import (
21
+ __all__ as _validators_all,
22
+ )
23
+ from roadmap.core.services.validators.health_status_utils import (
24
+ get_overall_status,
25
+ )
26
+
27
+ logger = get_logger(__name__)
28
+
29
+ # Re-export validators for backward compatibility - use same list as validators module
30
+ _exported_validators: list[str] = (
31
+ list(_validators_all) if isinstance(_validators_all, list | tuple) else []
32
+ )
33
+ __all__ = ["DataIntegrityValidatorService"] + _exported_validators # type: ignore[operator]
34
+
35
+
36
+ class DataIntegrityValidatorService:
37
+ """Orchestrator for data integrity validation checks."""
38
+
39
+ def __init__(self):
40
+ """Initialize data integrity validator service."""
41
+ # Note: This service uses static methods from validators.
42
+ # Instance creation is kept for potential future stateful functionality.
43
+ pass
44
+
45
+ def run_all_data_integrity_checks(self, core) -> dict[str, tuple[str, str]]:
46
+ """Run all data integrity checks.
47
+
48
+ Returns:
49
+ Dictionary mapping check names to (status, message) tuples
50
+ """
51
+ checks = {}
52
+
53
+ try:
54
+ checks["duplicate_issues"] = DuplicateIssuesValidator.perform_check()
55
+ checks["duplicate_milestones"] = (
56
+ DuplicateMilestonesValidator.perform_check()
57
+ )
58
+ checks["folder_structure"] = FolderStructureValidator.perform_check()
59
+ checks["old_backups"] = BackupValidator.check_old_backups()
60
+ checks["archivable_issues"] = (
61
+ ArchivableIssuesValidator.check_archivable_issues(core)
62
+ )
63
+ checks["archivable_milestones"] = (
64
+ ArchivableMilestonesValidator.check_archivable_milestones(core)
65
+ )
66
+ checks["data_integrity"] = DataIntegrityValidator.check_data_integrity()
67
+ checks["orphaned_issues"] = OrphanedIssuesValidator.check_orphaned_issues(
68
+ core
69
+ )
70
+ checks["orphaned_milestones"] = OrphanedMilestonesValidator.perform_check()
71
+
72
+ return checks
73
+ except Exception as e:
74
+ logger.error(
75
+ "data_integrity_validation_failed", error=str(e), severity="data_error"
76
+ )
77
+ return {
78
+ "error": (
79
+ HealthStatus.UNHEALTHY,
80
+ f"Data integrity validation failed: {e}",
81
+ )
82
+ }
83
+
84
+ def get_overall_status(self, checks: dict[str, tuple[str, str]]) -> str:
85
+ """Get overall status from all data integrity checks.
86
+
87
+ Returns:
88
+ Overall status: 'healthy', 'degraded', or 'unhealthy'
89
+ """
90
+ return get_overall_status(checks)