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,792 @@
1
+ """GitHub-specific implementation of SyncBackendInterface.
2
+
3
+ This module provides the GitHub API backend for syncing roadmap issues
4
+ with GitHub repositories. It implements the SyncBackendInterface protocol.
5
+
6
+ Updated to use Result<T, SyncError> pattern for explicit error handling.
7
+ """
8
+
9
+ import time
10
+ from collections.abc import Callable
11
+ from datetime import datetime # noqa: F401 # Used in type hints
12
+ from typing import Any, TypeVar
13
+
14
+ from structlog import get_logger
15
+
16
+ from roadmap.adapters.sync.backends.github_backend_helpers import GitHubBackendHelpers
17
+ from roadmap.adapters.sync.backends.github_client import GitHubClientWrapper
18
+ from roadmap.adapters.sync.backends.services.github_authentication_service import (
19
+ GitHubAuthenticationService,
20
+ )
21
+ from roadmap.adapters.sync.backends.services.github_issue_delete_service import (
22
+ GitHubIssueDeleteService,
23
+ )
24
+ from roadmap.adapters.sync.backends.services.github_issue_fetch_service import (
25
+ GitHubIssueFetchService,
26
+ )
27
+ from roadmap.common.result import Err, Ok, Result
28
+ from roadmap.core.domain.issue import Issue
29
+ from roadmap.core.interfaces import (
30
+ SyncConflict,
31
+ SyncReport,
32
+ )
33
+ from roadmap.core.models.sync_models import (
34
+ SyncIssue,
35
+ SyncMilestone,
36
+ SyncProject,
37
+ )
38
+ from roadmap.core.services.sync.sync_errors import (
39
+ SyncError,
40
+ SyncErrorType,
41
+ authentication_error,
42
+ )
43
+ from roadmap.core.services.sync.sync_metadata_service import SyncMetadataService
44
+ from roadmap.infrastructure.coordination.core import RoadmapCore
45
+
46
+ logger = get_logger()
47
+
48
+ T = TypeVar("T")
49
+
50
+
51
+ class GitHubSyncBackend:
52
+ """GitHub API implementation of the SyncBackendInterface.
53
+
54
+ Syncs roadmap issues with GitHub issues using the GitHub REST API.
55
+ Requires a GitHub Personal Access Token with 'repo' scope.
56
+
57
+ Attributes:
58
+ core: RoadmapCore instance with access to local issues
59
+ config: GitHub configuration with 'owner', 'repo', 'token'
60
+ github_client: GitHubIssueClient for API communication
61
+ """
62
+
63
+ def __init__(self, core: RoadmapCore, config: dict[str, Any]):
64
+ """Initialize GitHub sync backend.
65
+
66
+ Args:
67
+ core: RoadmapCore instance
68
+ config: Dict with required keys:
69
+ - owner: GitHub repository owner
70
+ - repo: GitHub repository name
71
+ - token: GitHub Personal Access Token (optional, checked at auth time)
72
+
73
+ Raises:
74
+ ValueError: If required config keys are missing
75
+ """
76
+ if not config.get("owner") or not config.get("repo"):
77
+ raise ValueError("GitHub config must include 'owner' and 'repo'")
78
+
79
+ self.core = core
80
+ self.config = config
81
+
82
+ # Initialize GitHub client if token is provided, otherwise defer to authenticate()
83
+ token = config.get("token")
84
+ owner = config.get("owner")
85
+ repo = config.get("repo")
86
+ self.github_client = self._safe_init(
87
+ lambda: GitHubClientWrapper(token, owner, repo) if token else None,
88
+ "GitHubClientWrapper",
89
+ )
90
+
91
+ # Initialize metadata service if available
92
+ self.metadata_service = self._safe_init(
93
+ lambda: SyncMetadataService(core),
94
+ "SyncMetadataService",
95
+ )
96
+
97
+ # Initialize remote link repository if available
98
+ # This enables fast database lookups during sync operations
99
+ self.remote_link_repo = None
100
+ if (
101
+ hasattr(core, "db")
102
+ and core.db is not None
103
+ and hasattr(core.db, "remote_links")
104
+ ):
105
+ self.remote_link_repo = core.db.remote_links
106
+
107
+ # Helper utilities extracted for mapping and local persistence
108
+ self._helpers = GitHubBackendHelpers(
109
+ core=self.core, remote_link_repo=self.remote_link_repo
110
+ )
111
+
112
+ # Initialize delegated services
113
+ self._auth_service = GitHubAuthenticationService(config)
114
+ self._fetch_service = None # Initialized lazily after auth
115
+ self._issue_delete_service = GitHubIssueDeleteService(config)
116
+
117
+ def _safe_init(self, factory: Callable[[], T], name: str) -> T | None:
118
+ """Safely initialize a component, returning None on failure.
119
+
120
+ Args:
121
+ factory: Callable that returns the initialized component
122
+ name: Component name for logging
123
+
124
+ Returns:
125
+ Initialized component or None if initialization fails
126
+ """
127
+ try:
128
+ return factory()
129
+ except (ImportError, AttributeError) as e:
130
+ logger.warning(
131
+ "initialization_failed",
132
+ component=name,
133
+ error=str(e),
134
+ error_type=type(e).__name__,
135
+ suggested_action="check_dependencies",
136
+ )
137
+ return None
138
+ except Exception as e:
139
+ logger.warning(
140
+ "initialization_failed",
141
+ component=name,
142
+ error=str(e),
143
+ error_type=type(e).__name__,
144
+ error_classification="unknown_error",
145
+ )
146
+ return None
147
+
148
+ def get_backend_name(self) -> str:
149
+ """Get the canonical name of this backend.
150
+
151
+ Returns:
152
+ 'github' - used as key in Issue.remote_ids dict
153
+ """
154
+ return "github"
155
+
156
+ def authenticate(self) -> Result[bool, SyncError]:
157
+ """Verify credentials and remote connectivity.
158
+
159
+ Returns:
160
+ Ok(True) if authentication succeeds (token valid and repo accessible)
161
+ Err(SyncError) with authentication error details
162
+ """
163
+ try:
164
+ result = self._auth_service.authenticate()
165
+ if result and self.github_client is None:
166
+ # Update our client reference after successful auth
167
+ self.github_client = self._auth_service.github_client
168
+
169
+ if result:
170
+ return Ok(True)
171
+ else:
172
+ return Err(authentication_error("GitHub authentication failed"))
173
+ except Exception as e:
174
+ logger.error(
175
+ "github_authenticate_exception",
176
+ error=str(e),
177
+ error_type=type(e).__name__,
178
+ )
179
+ return Err(
180
+ SyncError.from_exception(
181
+ e,
182
+ error_type=SyncErrorType.AUTHENTICATION_FAILED,
183
+ )
184
+ )
185
+
186
+ def get_api_client(self) -> Any:
187
+ """Get the GitHub API client with handler methods for push/pull operations.
188
+
189
+ Returns:
190
+ IssueHandler instance with create_issue(), update_issue(), etc. methods.
191
+
192
+ This method is used internally by GitHubSyncOps for actual API calls.
193
+ It ensures operations use the proper handler-based client for low-level
194
+ API operations, not the high-level GitHubClient wrapper.
195
+ """
196
+ import requests
197
+
198
+ from roadmap.adapters.github.handlers.issues import IssueHandler
199
+
200
+ # Create a session with the token
201
+ session = requests.Session()
202
+ token = self.config.get("token")
203
+ if token:
204
+ session.headers.update(
205
+ {
206
+ "Authorization": f"token {token}",
207
+ "Accept": "application/vnd.github.v3+json",
208
+ "User-Agent": "roadmap-cli/1.0",
209
+ }
210
+ )
211
+
212
+ # Return a properly configured issue handler for API calls
213
+ return IssueHandler(
214
+ session=session,
215
+ owner=self.config.get("owner"),
216
+ repo=self.config.get("repo"),
217
+ )
218
+
219
+ def get_label_client(self) -> Any | None:
220
+ """Get a GitHub client that supports label operations."""
221
+ token = self.config.get("token")
222
+ if not token:
223
+ return None
224
+
225
+ from roadmap.adapters.github.github import GitHubClient
226
+
227
+ return GitHubClient(
228
+ token=token,
229
+ owner=self.config.get("owner"),
230
+ repo=self.config.get("repo"),
231
+ )
232
+
233
+ def get_issues(self) -> Result[dict[str, SyncIssue], SyncError]:
234
+ """Fetch all issues from GitHub remote.
235
+
236
+ Returns:
237
+ Ok(dict) mapping issue_id -> SyncIssue objects on success
238
+ Err(SyncError) with error details on failure
239
+ """
240
+ try:
241
+ # Lazily initialize fetch service after first auth
242
+ if self._fetch_service is None and self.github_client:
243
+ self._fetch_service = GitHubIssueFetchService(
244
+ self.github_client, self.config, self._helpers
245
+ )
246
+
247
+ if self._fetch_service is None:
248
+ logger.warning("github_fetch_service_not_initialized")
249
+ return Err(
250
+ SyncError(
251
+ error_type=SyncErrorType.AUTHENTICATION_FAILED,
252
+ message="GitHub fetch service not initialized - authenticate first",
253
+ suggested_fix="Call authenticate() before get_issues()",
254
+ )
255
+ )
256
+
257
+ issues = self._fetch_service.get_issues()
258
+ return Ok(issues)
259
+ except Exception as e:
260
+ logger.error(
261
+ "github_get_issues_exception",
262
+ error=str(e),
263
+ error_type=type(e).__name__,
264
+ )
265
+ return Err(
266
+ SyncError.from_exception(
267
+ e,
268
+ error_type=SyncErrorType.NETWORK_ERROR,
269
+ )
270
+ )
271
+
272
+ def push_issues(self, local_issues: list[Issue]) -> Result[SyncReport, SyncError]:
273
+ """Push multiple local issues to GitHub.
274
+
275
+ Args:
276
+ local_issues: List of Issue objects to push
277
+
278
+ Returns:
279
+ Ok(SyncReport) with pushed, conflicts, and errors on success
280
+ Err(SyncError) with fatal error details if operation cannot proceed
281
+ """
282
+ try:
283
+ # Delegate orchestration to helper class
284
+ from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
285
+
286
+ ops = GitHubSyncOps(self)
287
+ report = ops.push_issues(local_issues)
288
+ return Ok(report)
289
+ except Exception as e:
290
+ logger.error(
291
+ "github_push_issues_exception",
292
+ error=str(e),
293
+ error_type=type(e).__name__,
294
+ issue_count=len(local_issues),
295
+ )
296
+ return Err(
297
+ SyncError.from_exception(
298
+ e,
299
+ error_type=SyncErrorType.UNKNOWN_ERROR,
300
+ entity_type="Issue",
301
+ )
302
+ )
303
+
304
+ def push_issue(self, local_issue: Issue) -> Result[bool, SyncError]:
305
+ """Push a single local issue to GitHub.
306
+
307
+ Args:
308
+ local_issue: The Issue object to push
309
+
310
+ Returns:
311
+ Ok(True) if push succeeds
312
+ Err(SyncError) with error details if push fails
313
+
314
+ Deprecated:
315
+ Use push_issues([issue]) instead. This method exists for backward
316
+ compatibility but is not used by the sync orchestrator.
317
+ """
318
+ report_result = self.push_issues([local_issue])
319
+ if report_result.is_err():
320
+ return report_result # type: ignore[return-value]
321
+
322
+ report = report_result.unwrap()
323
+ if len(report.pushed) > 0 and len(report.errors) == 0:
324
+ return Ok(True)
325
+ else:
326
+ error_msg = report.errors.get(str(local_issue.id), "Push failed")
327
+ return Err(
328
+ SyncError(
329
+ error_type=SyncErrorType.UNKNOWN_ERROR,
330
+ message=error_msg,
331
+ entity_type="Issue",
332
+ entity_id=str(local_issue.id),
333
+ )
334
+ )
335
+
336
+ def pull_issues(self, issue_ids: list[str]) -> Result[SyncReport, SyncError]:
337
+ """Pull specified remote GitHub issues to local.
338
+
339
+ Args:
340
+ issue_ids: List of remote issue IDs to pull
341
+
342
+ Returns:
343
+ Ok(SyncReport) with pulled, conflicts, and errors on success
344
+ Err(SyncError) with fatal error details if operation cannot proceed
345
+
346
+ Notes:
347
+ - Each ID should correspond to a remote issue
348
+ - Updates or creates local files as needed
349
+ - Uses parallel execution with thread pool for performance
350
+ """
351
+ try:
352
+ from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
353
+
354
+ ops = GitHubSyncOps(self)
355
+ report = ops.pull_issues(issue_ids)
356
+ return Ok(report)
357
+ except Exception as e:
358
+ logger.error(
359
+ "github_pull_issues_exception",
360
+ error=str(e),
361
+ error_type=type(e).__name__,
362
+ issue_count=len(issue_ids),
363
+ )
364
+ return Err(
365
+ SyncError.from_exception(
366
+ e,
367
+ error_type=SyncErrorType.UNKNOWN_ERROR,
368
+ entity_type="Issue",
369
+ )
370
+ )
371
+
372
+ def pull_milestones(self, milestone_ids: list[str]) -> SyncReport:
373
+ """Pull specified remote GitHub milestones to local.
374
+
375
+ Args:
376
+ milestone_ids: List of remote milestone IDs to pull (GitHub milestone numbers)
377
+
378
+ Returns:
379
+ SyncReport with pulled, conflicts, and errors.
380
+ """
381
+ from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
382
+
383
+ ops = GitHubSyncOps(self)
384
+ return ops.pull_milestones(milestone_ids)
385
+
386
+ def push_milestones(self, local_milestones: list) -> SyncReport:
387
+ """Push multiple local milestones to GitHub.
388
+
389
+ Args:
390
+ local_milestones: List of Milestone objects to push
391
+
392
+ Returns:
393
+ SyncReport with pushed, conflicts, and errors.
394
+
395
+ Note:
396
+ Currently not implemented - milestones must be created on GitHub first.
397
+ """
398
+ from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
399
+
400
+ ops = GitHubSyncOps(self)
401
+ return ops.push_milestones(local_milestones)
402
+
403
+ def pull_issue(self, issue_id: str) -> Result[bool, SyncError]:
404
+ """Pull a single remote GitHub issue to local.
405
+
406
+ Args:
407
+ issue_id: The remote issue ID (GitHub issue number like "_remote_123" or "123")
408
+
409
+ Returns:
410
+ Ok(True) if issue is valid and can be pulled
411
+ Err(SyncError) if error during validation
412
+
413
+ Notes:
414
+ - Validates the issue_id can be parsed
415
+ - Actual fetching/saving is handled by pull_issues caller
416
+ - Returns Ok(True) to indicate success to GitHubSyncOps
417
+ """
418
+ try:
419
+ # Parse issue_id - extract numeric part from "_remote_123" format
420
+ if issue_id.startswith("_remote_"):
421
+ issue_num_str = issue_id[8:] # Remove "_remote_" prefix
422
+ else:
423
+ issue_num_str = issue_id
424
+
425
+ # Validate it's a valid integer
426
+ try:
427
+ int(issue_num_str)
428
+ except (ValueError, TypeError):
429
+ logger.debug(
430
+ "pull_issue_invalid_id",
431
+ issue_id=issue_id,
432
+ reason="Cannot parse as integer",
433
+ )
434
+ return Err(
435
+ SyncError(
436
+ error_type=SyncErrorType.VALIDATION_ERROR,
437
+ message=f"Invalid issue ID format: {issue_id}",
438
+ suggested_fix="Ensure issue_id is a valid number",
439
+ )
440
+ )
441
+
442
+ # If we got here, the issue_id is valid
443
+ # The actual pull work happens elsewhere in the sync pipeline
444
+ return Ok(True)
445
+
446
+ except Exception as e:
447
+ logger.debug(
448
+ "pull_issue_validation_failed",
449
+ issue_id=issue_id,
450
+ error=str(e),
451
+ error_type=type(e).__name__,
452
+ )
453
+ return Err(
454
+ SyncError.from_exception(
455
+ e,
456
+ error_type=SyncErrorType.VALIDATION_ERROR,
457
+ )
458
+ )
459
+
460
+ def _convert_sync_to_issue(self, issue_id: str, sync_issue: SyncIssue) -> "Issue":
461
+ """Convert SyncIssue to local Issue object.
462
+
463
+ Args:
464
+ issue_id: Local issue ID
465
+ sync_issue: SyncIssue with remote data
466
+
467
+ Returns:
468
+ Issue instance with converted data
469
+
470
+ Raises:
471
+ ValueError: If conversion fails
472
+ """
473
+ return self._helpers._convert_sync_to_issue(issue_id, sync_issue)
474
+
475
+ def _convert_github_to_issue(
476
+ self, issue_id: str, remote_data: dict[str, Any]
477
+ ) -> "Issue":
478
+ """Convert GitHub issue dict to local Issue object.
479
+
480
+ Args:
481
+ issue_id: Local issue ID
482
+ remote_data: GitHub issue data from API
483
+
484
+ Returns:
485
+ Issue instance with converted data
486
+
487
+ Raises:
488
+ ValueError: If conversion fails
489
+ """
490
+ return self._helpers._convert_github_to_issue(issue_id, remote_data)
491
+
492
+ def _parse_timestamp(self, timestamp_str: str | None) -> "datetime | None":
493
+ """Parse ISO format timestamp string.
494
+
495
+ Args:
496
+ timestamp_str: ISO format timestamp (may have 'Z' suffix)
497
+
498
+ Returns:
499
+ datetime object or None if parsing fails
500
+ """
501
+ return self._helpers._parse_timestamp(timestamp_str)
502
+
503
+ def get_conflict_resolution_options(self, conflict: SyncConflict) -> list[str]:
504
+ """Get available resolution strategies for a conflict.
505
+
506
+ Args:
507
+ conflict: The SyncConflict to resolve
508
+
509
+ Returns:
510
+ List of resolution option codes (e.g., ['use_local', 'use_remote', 'merge'])
511
+ """
512
+ # GitHub backend supports the three basic strategies
513
+ return ["use_local", "use_remote", "merge"]
514
+
515
+ def resolve_conflict(self, conflict: SyncConflict, resolution: str) -> bool:
516
+ """Resolve a sync conflict using specified strategy.
517
+
518
+ Args:
519
+ conflict: The SyncConflict to resolve
520
+ resolution: The resolution strategy code ('use_local', 'use_remote', or 'merge')
521
+
522
+ Returns:
523
+ True if resolution succeeds, False otherwise.
524
+ """
525
+ from structlog import get_logger
526
+
527
+ logger = get_logger()
528
+
529
+ try:
530
+ logger.debug(
531
+ "github_resolve_conflict_started",
532
+ issue_id=conflict.issue_id,
533
+ resolution=resolution,
534
+ )
535
+
536
+ # For now, return True to indicate success
537
+ # Full implementation will handle actual conflict resolution
538
+ # when GitHub backend API is fully implemented
539
+
540
+ logger.info(
541
+ "github_resolve_conflict_completed",
542
+ issue_id=conflict.issue_id,
543
+ resolution=resolution,
544
+ )
545
+ return True
546
+
547
+ except Exception as e:
548
+ logger.warning(
549
+ "github_resolve_conflict_failed",
550
+ issue_id=conflict.issue_id,
551
+ resolution=resolution,
552
+ error=str(e),
553
+ )
554
+ return False
555
+
556
+ def get_milestones(self) -> Result[dict[str, SyncMilestone], SyncError]:
557
+ """Fetch all milestones from GitHub.
558
+
559
+ Returns:
560
+ Ok(dict) mapping milestone_number -> SyncMilestone objects on success
561
+ Err(SyncError) with error details on failure
562
+ """
563
+ from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
564
+ GitHubMilestoneFetchService,
565
+ )
566
+
567
+ if not self.github_client:
568
+ logger.warning("github_milestone_fetch_no_client")
569
+ return Err(
570
+ SyncError(
571
+ error_type=SyncErrorType.AUTHENTICATION_FAILED,
572
+ message="GitHub client not initialized - authenticate first",
573
+ entity_type="Milestone",
574
+ suggested_fix="Call authenticate() before get_milestones()",
575
+ )
576
+ )
577
+
578
+ try:
579
+ fetch_service = GitHubMilestoneFetchService(self.github_client, self.config)
580
+ milestones = fetch_service.get_milestones(state="all")
581
+
582
+ logger.info(
583
+ "github_milestones_retrieved",
584
+ count=len(milestones),
585
+ owner=self.config.get("owner"),
586
+ repo=self.config.get("repo"),
587
+ )
588
+
589
+ return Ok(milestones)
590
+
591
+ except Exception as e:
592
+ logger.error(
593
+ "github_get_milestones_failed",
594
+ error=str(e),
595
+ error_type=type(e).__name__,
596
+ owner=self.config.get("owner"),
597
+ repo=self.config.get("repo"),
598
+ )
599
+ return Err(
600
+ SyncError.from_exception(
601
+ e,
602
+ error_type=SyncErrorType.NETWORK_ERROR,
603
+ entity_type="Milestone",
604
+ )
605
+ )
606
+
607
+ # --- Helper methods for pull/create matching and persistence ---
608
+ def _find_matching_local_issue(
609
+ self, title: str, github_issue_number: str | int | None
610
+ ):
611
+ """Find a matching local issue by GitHub number or by title.
612
+
613
+ Accepts either numeric or string GitHub IDs; comparison is done
614
+ by normalizing both sides to string.
615
+ """
616
+ return self._helpers._find_matching_local_issue(title, github_issue_number)
617
+
618
+ def _apply_or_create_local_issue(
619
+ self,
620
+ issue_id: str,
621
+ matching_local_issue,
622
+ updates: dict,
623
+ github_issue_number: str | int | None,
624
+ remote_issue: SyncIssue | None = None,
625
+ ) -> None:
626
+ """Apply updates to an existing local issue or create a new one, and ensure linking/persistence.
627
+
628
+ Args:
629
+ issue_id: remote issue id (e.g. 'gh-123') used as fallback local id
630
+ matching_local_issue: existing local Issue object or None
631
+ updates: dict of fields to update on local issue
632
+ github_issue_number: numeric GitHub issue number if available
633
+ remote_issue: original SyncIssue object from GitHub (used when creating)
634
+ """
635
+ return self._helpers._apply_or_create_local_issue(
636
+ issue_id, matching_local_issue, updates, github_issue_number, remote_issue
637
+ )
638
+
639
+ def get_projects(self) -> Result[dict[str, SyncProject], SyncError]:
640
+ """Fetch all projects from GitHub.
641
+
642
+ Currently returns empty dict as project fetching is not yet implemented.
643
+ GitHub uses classic Project Boards and newer Projects (beta).
644
+ """
645
+ # TODO: Implement GitHub Projects API integration
646
+ return Ok({})
647
+
648
+ def delete_issues(self, issue_numbers: list[int]) -> int:
649
+ """Delete GitHub issues via GraphQL batch mutation.
650
+
651
+ Args:
652
+ issue_numbers: GitHub issue numbers to delete
653
+
654
+ Returns:
655
+ Count of successfully deleted issues
656
+ """
657
+ return self._issue_delete_service.delete_issues(issue_numbers)
658
+
659
+ def _get_delete_issue_config(self) -> tuple[str, str, str] | None:
660
+ """Resolve required delete configuration or log why delete is skipped."""
661
+ return self._issue_delete_service.get_delete_issue_config()
662
+
663
+ def _process_issue_lookup_batch(
664
+ self,
665
+ *,
666
+ batch: list[int],
667
+ owner: str,
668
+ repo: str,
669
+ token: str,
670
+ delete_batch_size: int,
671
+ inter_batch_delay_seconds: float,
672
+ rate_limit_delay_seconds: float,
673
+ ) -> tuple[int, list[int]]:
674
+ """Resolve node IDs for a lookup batch and process delete chunks."""
675
+ return self._issue_delete_service.process_issue_lookup_batch(
676
+ batch=batch,
677
+ owner=owner,
678
+ repo=repo,
679
+ token=token,
680
+ delete_batch_size=delete_batch_size,
681
+ inter_batch_delay_seconds=inter_batch_delay_seconds,
682
+ rate_limit_delay_seconds=rate_limit_delay_seconds,
683
+ )
684
+
685
+ def _delete_issue_node_chunks(
686
+ self,
687
+ *,
688
+ node_ids: dict[int, str],
689
+ token: str,
690
+ delete_batch_size: int,
691
+ inter_batch_delay_seconds: float,
692
+ rate_limit_delay_seconds: float,
693
+ ) -> int:
694
+ """Delete issues in chunked GraphQL mutations with optional retry."""
695
+ return self._issue_delete_service.delete_issue_node_chunks(
696
+ node_ids=node_ids,
697
+ token=token,
698
+ delete_batch_size=delete_batch_size,
699
+ inter_batch_delay_seconds=inter_batch_delay_seconds,
700
+ rate_limit_delay_seconds=rate_limit_delay_seconds,
701
+ )
702
+
703
+ def _retry_failed_rate_limited_deletes(
704
+ self,
705
+ *,
706
+ failed_numbers: list[int],
707
+ rate_limited: bool,
708
+ node_ids: dict[int, str],
709
+ token: str,
710
+ rate_limit_delay_seconds: float,
711
+ ) -> int:
712
+ """Retry failed delete mutations when they failed under rate limiting."""
713
+ return self._issue_delete_service.retry_failed_rate_limited_deletes(
714
+ failed_numbers=failed_numbers,
715
+ rate_limited=rate_limited,
716
+ node_ids=node_ids,
717
+ token=token,
718
+ rate_limit_delay_seconds=rate_limit_delay_seconds,
719
+ )
720
+
721
+ def _resolve_issue_node_ids(
722
+ self,
723
+ issue_numbers: list[int],
724
+ owner: str,
725
+ repo: str,
726
+ token: str,
727
+ ) -> tuple[dict[int, str], list[int]]:
728
+ """Resolve GitHub issue numbers to GraphQL node IDs.
729
+
730
+ Returns tuple of (node_ids, skipped_pr_numbers).
731
+ """
732
+ return self._issue_delete_service.resolve_issue_node_ids(
733
+ issue_numbers,
734
+ owner,
735
+ repo,
736
+ token,
737
+ )
738
+
739
+ def _delete_issues_batch(
740
+ self, node_ids: dict[int, str], token: str
741
+ ) -> tuple[int, bool, list[int]]:
742
+ """Delete issues by node ID via GraphQL mutation."""
743
+ return self._issue_delete_service.delete_issues_batch(node_ids, token)
744
+
745
+ def _extract_graphql_error_types(self, payload: dict[str, Any]) -> set[str]:
746
+ """Extract error types from a GraphQL payload."""
747
+ return self._issue_delete_service.extract_graphql_error_types(payload)
748
+
749
+ def _extract_graphql_error_details(
750
+ self, payload: dict[str, Any]
751
+ ) -> list[dict[str, Any]]:
752
+ """Extract error details from a GraphQL payload."""
753
+ return self._issue_delete_service.extract_graphql_error_details(payload)
754
+
755
+ def _post_graphql_with_backoff(
756
+ self,
757
+ query: str,
758
+ token: str,
759
+ operation: str,
760
+ max_attempts: int = 3,
761
+ delay: float = 2.0,
762
+ backoff: float = 2.0,
763
+ ) -> dict[str, Any] | None:
764
+ """Post GraphQL with backoff when resource limits are hit."""
765
+ return self._issue_delete_service.post_graphql_with_backoff(
766
+ query=query,
767
+ token=token,
768
+ operation=operation,
769
+ max_attempts=max_attempts,
770
+ delay=delay,
771
+ backoff=backoff,
772
+ post_graphql_fn=self._post_graphql,
773
+ sleep_fn=time.sleep,
774
+ )
775
+
776
+ def _post_graphql(self, query: str, token: str) -> dict[str, Any] | None:
777
+ """Post a GraphQL query to GitHub and return JSON data."""
778
+ return self._issue_delete_service.post_graphql(query, token)
779
+
780
+ @staticmethod
781
+ def _dict_to_sync_issue(issue_dict: dict[str, Any]) -> SyncIssue:
782
+ """Convert a raw GitHub API issue dict to SyncIssue.
783
+
784
+ Args:
785
+ issue_dict: Dict with issue data from GitHub API
786
+
787
+ Returns:
788
+ SyncIssue instance
789
+ """
790
+ # Delegate to helpers for consistent conversion
791
+ helpers = GitHubBackendHelpers(core=None)
792
+ return helpers._dict_to_sync_issue(issue_dict)