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,908 @@
1
+ """GitHub synchronization operations with parallel execution."""
2
+
3
+ import threading
4
+ import time
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from typing import Any
7
+
8
+ from structlog import get_logger
9
+
10
+ from roadmap.adapters.sync.backends.services.github_issue_dependency_service import (
11
+ GitHubIssueDependencyService,
12
+ )
13
+ from roadmap.adapters.sync.backends.services.github_label_sync_service import (
14
+ GitHubLabelSyncService,
15
+ )
16
+ from roadmap.adapters.sync.backends.services.github_local_issue_sync_service import (
17
+ GitHubLocalIssueSyncService,
18
+ )
19
+ from roadmap.common.logging import log_error_with_context
20
+ from roadmap.common.services.retry import API_RETRY
21
+ from roadmap.common.utils.timezone_utils import now_utc
22
+ from roadmap.core.interfaces import SyncReport
23
+
24
+ logger = get_logger()
25
+
26
+
27
+ class GitHubSyncOps:
28
+ """Perform synchronization operations with GitHub backend."""
29
+
30
+ def __init__(self, backend: Any):
31
+ """Initialize GitHubSyncOps.
32
+
33
+ Args:
34
+ backend: GitHub backend instance.
35
+ """
36
+ self.backend = backend
37
+ self._create_lock = threading.Lock()
38
+ self._last_create_time = 0.0
39
+ self._create_min_interval_seconds = self._get_create_min_interval_seconds()
40
+ self._label_service = GitHubLabelSyncService(backend)
41
+ self._dependency_service = GitHubIssueDependencyService()
42
+ self._local_issue_service = GitHubLocalIssueSyncService(backend)
43
+
44
+ @property
45
+ def _label_cache(self) -> set[str] | None:
46
+ return self._label_service.label_cache
47
+
48
+ @_label_cache.setter
49
+ def _label_cache(self, value: set[str] | None) -> None:
50
+ self._label_service.label_cache = value
51
+
52
+ @property
53
+ def _label_support(self) -> bool | None:
54
+ return self._label_service.label_support
55
+
56
+ @_label_support.setter
57
+ def _label_support(self, value: bool | None) -> None:
58
+ self._label_service.label_support = value
59
+
60
+ def _get_create_min_interval_seconds(self) -> float:
61
+ """Get minimum interval between issue creations to avoid secondary limits."""
62
+ config = getattr(self.backend, "config", {}) or {}
63
+ sync_settings = (
64
+ config.get("sync_settings", {}) if isinstance(config, dict) else {}
65
+ )
66
+ interval = sync_settings.get("create_min_interval_seconds")
67
+ try:
68
+ if interval is not None:
69
+ return max(0.0, float(interval))
70
+ except (TypeError, ValueError):
71
+ logger.debug(
72
+ "github_create_interval_invalid",
73
+ interval=interval,
74
+ severity="data_error",
75
+ )
76
+ return 1.0
77
+
78
+ def _sync_labels_enabled(self) -> bool:
79
+ return self._label_service.sync_labels_enabled()
80
+
81
+ def _get_label_color(self, name: str) -> str:
82
+ return self._label_service.get_label_color(name)
83
+
84
+ def _get_label_client(self):
85
+ """Return the label API client, or None if labels are not supported."""
86
+ return self._label_service.get_label_client()
87
+
88
+ def _init_label_cache(self, client) -> bool:
89
+ """Populate self._label_cache from GitHub. Returns False on failure."""
90
+ return self._label_service.init_label_cache(client)
91
+
92
+ def _ensure_labels_exist(self, labels: list[str]) -> None:
93
+ self._label_service.ensure_labels_exist(labels)
94
+
95
+ def _throttle_issue_creation(self) -> None:
96
+ """Throttle issue creation to reduce secondary rate limit errors."""
97
+ if self._create_min_interval_seconds <= 0:
98
+ return
99
+
100
+ with self._create_lock:
101
+ now = time.monotonic()
102
+ elapsed = now - self._last_create_time
103
+ delay = self._create_min_interval_seconds - elapsed
104
+ if delay > 0:
105
+ logger.debug(
106
+ "github_issue_create_throttle",
107
+ delay_seconds=round(delay, 3),
108
+ )
109
+ time.sleep(delay)
110
+ self._last_create_time = time.monotonic()
111
+
112
+ def _persist_issue_before_linking(self, issue: Any, issue_id: str) -> bool:
113
+ """Persist issue to database before linking to GitHub.
114
+
115
+ Args:
116
+ issue: The Issue domain object
117
+ issue_id: The ID to use for persistence
118
+
119
+ Returns:
120
+ True if persist succeeded or was skipped, False on hard error
121
+ """
122
+ return self._local_issue_service.persist_issue_before_linking(issue, issue_id)
123
+
124
+ def _link_issue_to_github(
125
+ self, issue_uuid: str, github_number: int
126
+ ) -> tuple[bool, str | None]:
127
+ """Link issue to GitHub in database.
128
+
129
+ Args:
130
+ issue_uuid: The local issue UUID
131
+ github_number: The GitHub issue number
132
+
133
+ Returns:
134
+ Tuple of (success: bool, error_message: str | None)
135
+ """
136
+ return self._local_issue_service.link_issue_to_github(issue_uuid, github_number)
137
+
138
+ def _handle_push_error(self, issue: Any, error_msg: str) -> tuple[bool, str]:
139
+ """Handle and categorize push errors.
140
+
141
+ Args:
142
+ issue: The Issue domain object
143
+ error_msg: The error message
144
+
145
+ Returns:
146
+ Tuple of (success: bool, categorized_error_message: str)
147
+ """
148
+ if "Access forbidden" in error_msg or "403" in error_msg:
149
+ logger.debug(
150
+ "github_push_issue_skipped_due_to_permissions",
151
+ issue_id=issue.id,
152
+ error=error_msg,
153
+ severity="config",
154
+ )
155
+ return False, f"Permission denied (check token scope): {error_msg}"
156
+ elif "Gone" in error_msg or "410" in error_msg:
157
+ logger.info(
158
+ "github_push_issue_skipped_resource_deleted",
159
+ issue_id=issue.id,
160
+ error=error_msg,
161
+ severity="operational",
162
+ )
163
+ return False, f"Remote issue deleted: {error_msg}"
164
+ elif "not found" in error_msg.lower() or "404" in error_msg:
165
+ logger.warning(
166
+ "github_push_issue_failed_not_found",
167
+ issue_id=issue.id,
168
+ error=error_msg,
169
+ severity="operational",
170
+ )
171
+ return False, error_msg
172
+ elif "Rate limit exceeded" in error_msg or "429" in error_msg:
173
+ logger.warning(
174
+ "github_push_issue_rate_limited",
175
+ issue_id=issue.id,
176
+ error=error_msg,
177
+ severity="operational",
178
+ )
179
+ return False, f"Rate limited: {error_msg}"
180
+ elif "Validation error" in error_msg:
181
+ logger.warning(
182
+ "github_push_issue_validation_error",
183
+ issue_id=issue.id,
184
+ error=error_msg,
185
+ severity="data_error",
186
+ )
187
+ return False, error_msg
188
+ else:
189
+ logger.warning(
190
+ "github_push_issue_failed",
191
+ issue_id=issue.id,
192
+ error=error_msg,
193
+ error_type=type(error_msg).__name__,
194
+ severity="operational",
195
+ )
196
+ return False, error_msg
197
+
198
+ def _push_single_issue(self, issue: Any) -> tuple[bool, str | None]:
199
+ """Push a single issue to GitHub API with retry logic.
200
+
201
+ Args:
202
+ issue: The Issue domain object to push
203
+
204
+ Returns:
205
+ Tuple of (success: bool, error_message: str | None)
206
+ """
207
+
208
+ @API_RETRY
209
+ def _push_with_retry():
210
+ """Inner function with retry decorator for API calls."""
211
+ from roadmap.adapters.sync.backends.converters import (
212
+ IssueToGitHubPayloadConverter,
213
+ )
214
+
215
+ client = self.backend.get_api_client()
216
+ github_number = IssueToGitHubPayloadConverter.get_github_number(issue)
217
+ payload = IssueToGitHubPayloadConverter.to_payload(issue, github_number)
218
+
219
+ labels = payload.get("labels") if isinstance(payload, dict) else None
220
+ if isinstance(labels, list) and labels:
221
+ self._ensure_labels_exist(labels)
222
+
223
+ if github_number:
224
+ # Update existing issue
225
+ client.update_issue(github_number, **payload)
226
+ logger.info(
227
+ "github_issue_updated",
228
+ issue_id=issue.id,
229
+ github_number=github_number,
230
+ title=issue.title,
231
+ )
232
+ return True, None
233
+
234
+ # Create new issue (throttled to avoid secondary rate limits)
235
+ self._throttle_issue_creation()
236
+ result = client.create_issue(**payload)
237
+ github_number = result.get("number")
238
+
239
+ if github_number:
240
+ self._persist_issue_before_linking(issue, issue.id)
241
+ self._link_issue_to_github(issue.id, github_number)
242
+ logger.info(
243
+ "github_issue_created",
244
+ issue_id=issue.id,
245
+ github_number=github_number,
246
+ title=issue.title,
247
+ )
248
+
249
+ return True, None
250
+
251
+ try:
252
+ return _push_with_retry()
253
+ except Exception as e:
254
+ error_msg = str(e)
255
+ return self._handle_push_error(issue, error_msg)
256
+
257
+ def push_issues(self, local_issues: list) -> SyncReport:
258
+ """Push local issues to GitHub backend with parallel execution.
259
+
260
+ Args:
261
+ local_issues: List of local issues to push.
262
+
263
+ Returns:
264
+ Sync report with push results and errors.
265
+ """
266
+ report = SyncReport()
267
+
268
+ if not local_issues:
269
+ logger.info("push_issues_empty")
270
+ return report
271
+
272
+ logger.info("push_issues_starting", issue_count=len(local_issues))
273
+
274
+ with ThreadPoolExecutor(max_workers=5) as executor:
275
+ futures = {
276
+ executor.submit(self._push_single_issue, issue): issue
277
+ for issue in local_issues
278
+ }
279
+
280
+ for future in as_completed(futures):
281
+ issue = futures[future]
282
+ try:
283
+ success, error_msg = future.result()
284
+ if success:
285
+ report.pushed.append(issue.id)
286
+ logger.debug("push_issue_succeeded", issue_id=issue.id)
287
+ else:
288
+ report.errors[issue.id] = error_msg or "Failed to push issue"
289
+ logger.debug(
290
+ "push_issue_failed", issue_id=issue.id, error=error_msg
291
+ )
292
+ except Exception as e:
293
+ error_msg = str(e)
294
+ report.errors[issue.id] = error_msg
295
+ log_error_with_context(
296
+ e,
297
+ operation="push_issue",
298
+ entity_type="Issue",
299
+ entity_id=issue.id,
300
+ include_traceback=False,
301
+ )
302
+
303
+ logger.info(
304
+ "push_issues_completed",
305
+ total=len(local_issues),
306
+ pushed=len(report.pushed),
307
+ failed=len(report.errors),
308
+ )
309
+ return report
310
+
311
+ def _get_project_id_for_synced_issue(self) -> str | None:
312
+ """Get default project ID for synced issues.
313
+
314
+ Returns:
315
+ Project ID or None
316
+ """
317
+ return self._local_issue_service.get_project_id_for_synced_issue()
318
+
319
+ def _resolve_local_issue_id(
320
+ self, github_id: str | int | None, local_issue: Any
321
+ ) -> str:
322
+ """Resolve the local issue ID for a pulled GitHub issue.
323
+
324
+ Prefers an existing remote link mapping; falls back to the local issue ID.
325
+ """
326
+ return self._local_issue_service.resolve_local_issue_id(github_id, local_issue)
327
+
328
+ def _create_or_update_issue_locally(
329
+ self, sync_issue: Any, local_issue: Any, github_id: str | int | None
330
+ ) -> str | None:
331
+ """Create or update issue in local database.
332
+
333
+ Args:
334
+ sync_issue: The SyncIssue from GitHub
335
+ local_issue: The converted local Issue domain object
336
+ github_id: The GitHub issue ID
337
+
338
+ Returns:
339
+ Local issue ID if successful
340
+ """
341
+ return self._local_issue_service.create_or_update_issue_locally(
342
+ sync_issue,
343
+ local_issue,
344
+ github_id,
345
+ )
346
+
347
+ def _link_pulled_issue_locally(
348
+ self, local_issue_id: str | None, github_id: str | int | None
349
+ ) -> bool:
350
+ """Link pulled issue to GitHub in local database.
351
+
352
+ Args:
353
+ sync_issue: The SyncIssue from GitHub
354
+
355
+ Returns:
356
+ True if linking succeeded or was skipped
357
+ """
358
+ return self._local_issue_service.link_pulled_issue_locally(
359
+ local_issue_id,
360
+ github_id,
361
+ )
362
+
363
+ def _handle_pull_error(self, sync_issue: Any, error_msg: str) -> tuple[bool, str]:
364
+ """Handle and categorize pull errors.
365
+
366
+ Args:
367
+ sync_issue: The SyncIssue from GitHub
368
+ error_msg: The error message
369
+
370
+ Returns:
371
+ Tuple of (success: bool, categorized_error_message: str)
372
+ """
373
+ remote_id = getattr(sync_issue, "backend_id", "unknown")
374
+
375
+ if "Access forbidden" in error_msg or "403" in error_msg:
376
+ logger.debug(
377
+ "github_pull_issue_skipped_due_to_permissions",
378
+ github_number=remote_id,
379
+ error=error_msg,
380
+ severity="config",
381
+ )
382
+ return False, f"Permission denied (check token scope): {error_msg}"
383
+ elif "Gone" in error_msg or "410" in error_msg:
384
+ logger.info(
385
+ "github_pull_issue_skipped_resource_deleted",
386
+ github_number=remote_id,
387
+ error=error_msg,
388
+ severity="operational",
389
+ )
390
+ return False, f"Remote issue deleted: {error_msg}"
391
+ elif "not found" in error_msg.lower() or "404" in error_msg:
392
+ logger.debug(
393
+ "github_pull_issue_failed_not_found",
394
+ github_number=remote_id,
395
+ error=error_msg,
396
+ severity="operational",
397
+ )
398
+ return False, error_msg
399
+ elif "Rate limit exceeded" in error_msg or "429" in error_msg:
400
+ logger.debug(
401
+ "github_pull_issue_rate_limited",
402
+ github_number=remote_id,
403
+ error=error_msg,
404
+ severity="operational",
405
+ )
406
+ return False, f"Rate limited: {error_msg}"
407
+ else:
408
+ logger.debug(
409
+ "github_pull_issue_failed",
410
+ github_number=remote_id,
411
+ error=error_msg,
412
+ error_type=type(error_msg).__name__,
413
+ severity="operational",
414
+ )
415
+ return False, error_msg
416
+
417
+ def _pull_single_issue(self, sync_issue: Any) -> tuple[bool, str | None]:
418
+ """Pull a single issue from GitHub and create/update locally.
419
+
420
+ Args:
421
+ sync_issue: The SyncIssue from GitHub fetch
422
+
423
+ Returns:
424
+ Tuple of (success: bool, error_message: str | None)
425
+ """
426
+ try:
427
+ from roadmap.adapters.sync.backends.converters import (
428
+ GitHubPayloadToIssueConverter,
429
+ )
430
+
431
+ local_issue = GitHubPayloadToIssueConverter.from_sync_issue(sync_issue)
432
+ github_id = sync_issue.remote_ids.get("github") or sync_issue.backend_id
433
+
434
+ matching_local_issue = self.backend._find_matching_local_issue(
435
+ local_issue.title, github_id
436
+ )
437
+ updates = {
438
+ "title": local_issue.title,
439
+ "description": local_issue.content or "",
440
+ "status": local_issue.status,
441
+ "priority": local_issue.priority,
442
+ "assignee": local_issue.assignee,
443
+ "milestone": local_issue.milestone,
444
+ "labels": local_issue.labels,
445
+ }
446
+ self.backend._apply_or_create_local_issue(
447
+ local_issue.id,
448
+ matching_local_issue,
449
+ updates,
450
+ github_id,
451
+ remote_issue=sync_issue,
452
+ )
453
+
454
+ return True, None
455
+
456
+ except Exception as e:
457
+ log_error_with_context(
458
+ e,
459
+ operation="pull_issue",
460
+ entity_type="Issue",
461
+ entity_id=str(getattr(sync_issue, "backend_id", "unknown")),
462
+ include_traceback=True,
463
+ )
464
+ error_msg = str(e)
465
+ return self._handle_pull_error(sync_issue, error_msg)
466
+
467
+ def pull_issues(self, issue_ids: list[str]) -> SyncReport:
468
+ """Pull issues from GitHub backend by IDs with dependency resolution.
469
+
470
+ This method now handles dependencies by:
471
+ 1. Fetching all milestones referenced by the requested issues
472
+ 2. Pulling those milestones first
473
+ 3. Then pulling the issues with proper milestone references
474
+
475
+ Args:
476
+ issue_ids: List of issue IDs to pull.
477
+
478
+ Returns:
479
+ Sync report with pull results and errors.
480
+ """
481
+ report = SyncReport()
482
+
483
+ if not issue_ids:
484
+ return report
485
+
486
+ logger.info(
487
+ "pull_issues_with_dependencies_starting", issue_count=len(issue_ids)
488
+ )
489
+
490
+ # Fetch all remote data
491
+ all_remote_issues, all_remote_milestones = self._fetch_remote_data()
492
+
493
+ # Analyze dependencies and build pull list
494
+ issues_to_pull, milestones_needed, report = self._analyze_issue_dependencies(
495
+ issue_ids, all_remote_issues, all_remote_milestones, report
496
+ )
497
+
498
+ # Phase 1: Pull milestones
499
+ milestone_pull_report = self._pull_milestone_dependencies(milestones_needed)
500
+
501
+ # Phase 2: Pull issues
502
+ return self._pull_issues_phase(
503
+ issues_to_pull,
504
+ all_remote_milestones,
505
+ milestone_pull_report,
506
+ report,
507
+ )
508
+
509
+ def _fetch_remote_data(self) -> tuple[dict, dict]:
510
+ """Fetch issues and milestones from GitHub."""
511
+ from roadmap.adapters.sync.backends.services.github_issue_fetch_service import (
512
+ GitHubIssueFetchService,
513
+ )
514
+ from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
515
+ GitHubMilestoneFetchService,
516
+ )
517
+
518
+ issue_fetch_service = GitHubIssueFetchService(
519
+ self.backend.github_client,
520
+ self.backend.config,
521
+ self.backend._helpers,
522
+ )
523
+ all_remote_issues = issue_fetch_service.get_issues()
524
+
525
+ milestone_fetch_service = GitHubMilestoneFetchService(
526
+ self.backend.github_client,
527
+ self.backend.config,
528
+ )
529
+ all_remote_milestones = milestone_fetch_service.get_milestones()
530
+
531
+ return all_remote_issues, all_remote_milestones
532
+
533
+ def _analyze_issue_dependencies(
534
+ self,
535
+ issue_ids: list[str],
536
+ all_remote_issues: dict,
537
+ all_remote_milestones: dict,
538
+ report: SyncReport,
539
+ ) -> tuple[list, set, SyncReport]:
540
+ """Build list of issues to pull and their milestone dependencies."""
541
+ return self._dependency_service.analyze_issue_dependencies(
542
+ issue_ids,
543
+ all_remote_issues,
544
+ all_remote_milestones,
545
+ report,
546
+ )
547
+
548
+ def _pull_milestone_dependencies(self, milestones_needed: set) -> SyncReport:
549
+ """Pull milestone dependencies first."""
550
+ milestone_pull_report = SyncReport()
551
+ if milestones_needed:
552
+ logger.info("pulling_dependencies_milestones", count=len(milestones_needed))
553
+ milestone_ids = list(milestones_needed)
554
+ milestone_pull_report = self.pull_milestones(milestone_ids)
555
+
556
+ if milestone_pull_report.errors:
557
+ logger.warning(
558
+ "milestone_pull_failures",
559
+ failed_count=len(milestone_pull_report.errors),
560
+ errors=list(milestone_pull_report.errors.keys())[:5],
561
+ )
562
+
563
+ return milestone_pull_report
564
+
565
+ def _pull_issues_phase(
566
+ self,
567
+ issues_to_pull: list,
568
+ all_remote_milestones: dict,
569
+ milestone_pull_report: SyncReport,
570
+ report: SyncReport,
571
+ ) -> SyncReport:
572
+ """Pull issues after their dependencies are satisfied."""
573
+ successful_pulls = []
574
+ failed_pulls = {}
575
+
576
+ logger.info("pulling_issues_phase", issue_count=len(issues_to_pull))
577
+
578
+ for issue_id, _lookup_id, sync_issue in issues_to_pull:
579
+ try:
580
+ # Check if issue's milestone was successfully pulled
581
+ if sync_issue.milestone:
582
+ milestone_num = self._find_milestone_number(
583
+ sync_issue.milestone, all_remote_milestones
584
+ )
585
+
586
+ if milestone_num and milestone_num in milestone_pull_report.errors:
587
+ failed_pulls[issue_id] = (
588
+ f"Milestone '{sync_issue.milestone}' pull failed: "
589
+ f"{milestone_pull_report.errors[milestone_num]}"
590
+ )
591
+ logger.debug(
592
+ "issue_skipped_milestone_failed",
593
+ issue_id=issue_id,
594
+ milestone=sync_issue.milestone,
595
+ )
596
+ continue
597
+
598
+ # Pull the issue and create/update locally
599
+ success, error = self._pull_single_issue(sync_issue)
600
+
601
+ if success:
602
+ successful_pulls.append(issue_id)
603
+ logger.debug("pull_issue_processed", issue_id=issue_id)
604
+ else:
605
+ failed_pulls[issue_id] = error or "Unknown error"
606
+
607
+ except Exception as e:
608
+ failed_pulls[issue_id] = str(e)
609
+ log_error_with_context(
610
+ e,
611
+ operation="pull_issue",
612
+ entity_type="Issue",
613
+ entity_id=issue_id,
614
+ include_traceback=False,
615
+ )
616
+
617
+ report.pulled = successful_pulls
618
+ report.errors = failed_pulls
619
+
620
+ logger.info(
621
+ "pull_issues_complete",
622
+ milestones_pulled=len(milestone_pull_report.pulled)
623
+ if hasattr(milestone_pull_report, "pulled")
624
+ else 0,
625
+ issues_successful=len(successful_pulls),
626
+ issues_failed=len(failed_pulls),
627
+ )
628
+
629
+ return report
630
+
631
+ def _find_milestone_number(
632
+ self, milestone_name: str, all_remote_milestones: dict
633
+ ) -> int | None:
634
+ """Find milestone number by name."""
635
+ return self._dependency_service.find_milestone_number(
636
+ milestone_name,
637
+ all_remote_milestones,
638
+ )
639
+
640
+ def pull_milestones(self, milestone_ids: list[str]) -> SyncReport:
641
+ """Pull milestones from GitHub backend by IDs.
642
+
643
+ Args:
644
+ milestone_ids: List of milestone IDs to pull (GitHub milestone numbers as strings).
645
+
646
+ Returns:
647
+ Sync report with pull results and errors.
648
+ """
649
+ report = SyncReport()
650
+
651
+ if not milestone_ids:
652
+ return report
653
+
654
+ logger.info("pull_milestones_starting", milestone_count=len(milestone_ids))
655
+
656
+ # Fetch all remote milestones upfront
657
+ from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
658
+ GitHubMilestoneFetchService,
659
+ )
660
+
661
+ try:
662
+ fetch_service = GitHubMilestoneFetchService(
663
+ self.backend.github_client,
664
+ self.backend.config,
665
+ )
666
+ all_remote_milestones = fetch_service.get_milestones()
667
+
668
+ successful_pulls = []
669
+ failed_pulls = {}
670
+
671
+ logger.info(
672
+ "pull_milestones_keys_analysis",
673
+ requested_sample=milestone_ids[:3] if milestone_ids else [],
674
+ remote_keys_sample=list(all_remote_milestones.keys())[:3]
675
+ if all_remote_milestones
676
+ else [],
677
+ requested_count=len(milestone_ids),
678
+ remote_count=len(all_remote_milestones),
679
+ )
680
+
681
+ # Process each requested milestone and save locally
682
+ for milestone_id in milestone_ids:
683
+ try:
684
+ # Strip any prefix if present
685
+ lookup_id = milestone_id
686
+ if milestone_id.startswith("_remote_milestone_"):
687
+ lookup_id = milestone_id[18:] # Remove prefix
688
+
689
+ if lookup_id not in all_remote_milestones:
690
+ error_msg = "Milestone not found on remote"
691
+ failed_pulls[milestone_id] = error_msg
692
+ logger.debug(
693
+ "pull_milestone_not_found",
694
+ milestone_id=milestone_id,
695
+ lookup_id=lookup_id,
696
+ )
697
+ continue
698
+
699
+ # Pull the milestone and create/update locally
700
+ sync_milestone = all_remote_milestones[lookup_id]
701
+ success, error = self._pull_single_milestone(sync_milestone)
702
+
703
+ if success:
704
+ successful_pulls.append(milestone_id)
705
+ logger.debug(
706
+ "pull_milestone_processed", milestone_id=milestone_id
707
+ )
708
+ else:
709
+ failed_pulls[milestone_id] = error or "Unknown error"
710
+
711
+ except Exception as e:
712
+ error_msg = str(e)
713
+ failed_pulls[milestone_id] = error_msg
714
+ log_error_with_context(
715
+ e,
716
+ operation="pull_milestone",
717
+ entity_type="Milestone",
718
+ entity_id=milestone_id,
719
+ include_traceback=False,
720
+ )
721
+
722
+ report.pulled = successful_pulls
723
+ report.errors = failed_pulls
724
+
725
+ logger.info(
726
+ "pull_milestones_complete",
727
+ successful=len(successful_pulls),
728
+ failed=len(failed_pulls),
729
+ )
730
+
731
+ return report
732
+
733
+ except Exception as e:
734
+ logger.error(
735
+ "pull_milestones_failed", error=str(e), error_type=type(e).__name__
736
+ )
737
+ report.error = f"Failed to pull milestones: {str(e)}"
738
+ return report
739
+
740
+ def _pull_single_milestone(self, sync_milestone) -> tuple[bool, str | None]:
741
+ """Pull a single milestone and persist it locally.
742
+
743
+ Args:
744
+ sync_milestone: SyncMilestone object from GitHub
745
+
746
+ Returns:
747
+ Tuple of (success: bool, error_message: str | None)
748
+ """
749
+ try:
750
+ # Create or update milestone locally
751
+ if not hasattr(self.backend, "core") or not self.backend.core:
752
+ return False, "Core not available"
753
+
754
+ core = self.backend.core
755
+
756
+ # Check if milestone already exists by GitHub milestone number
757
+ github_milestone_num = sync_milestone.backend_id
758
+ existing_milestone = None
759
+
760
+ # Try to find existing milestone by name (GitHub milestone title)
761
+ all_milestones = core.milestones.list()
762
+ for ms in all_milestones:
763
+ if ms.name == sync_milestone.name:
764
+ existing_milestone = ms
765
+ break
766
+
767
+ if existing_milestone:
768
+ # Update existing milestone
769
+ logger.debug(
770
+ "milestone_exists_updating",
771
+ milestone_name=sync_milestone.name,
772
+ github_number=github_milestone_num,
773
+ )
774
+
775
+ # Update fields
776
+ updates = {
777
+ "status": sync_milestone.status,
778
+ "due_date": sync_milestone.due_date,
779
+ }
780
+
781
+ if sync_milestone.headline:
782
+ updates["headline"] = sync_milestone.headline
783
+
784
+ success = core.milestones.update(existing_milestone.name, **updates)
785
+
786
+ if not success:
787
+ return False, "Failed to update milestone"
788
+
789
+ # Store GitHub milestone number in database metadata
790
+ self._link_milestone_to_github(
791
+ existing_milestone.name, github_milestone_num
792
+ )
793
+
794
+ else:
795
+ # Create new milestone
796
+ logger.debug(
797
+ "milestone_creating_new",
798
+ milestone_name=sync_milestone.name,
799
+ github_number=github_milestone_num,
800
+ )
801
+
802
+ milestone = core.milestones.create(
803
+ name=sync_milestone.name,
804
+ headline=sync_milestone.headline or "",
805
+ due_date=sync_milestone.due_date,
806
+ status=sync_milestone.status,
807
+ )
808
+
809
+ # Store GitHub milestone number in database metadata
810
+ self._link_milestone_to_github(milestone.name, github_milestone_num)
811
+
812
+ logger.info(
813
+ "milestone_pulled_successfully",
814
+ milestone_name=sync_milestone.name,
815
+ github_number=github_milestone_num,
816
+ )
817
+
818
+ return True, None
819
+
820
+ except Exception as e:
821
+ error_msg = str(e)
822
+ logger.error(
823
+ "pull_single_milestone_failed",
824
+ milestone_name=sync_milestone.name,
825
+ error=error_msg,
826
+ error_type=type(e).__name__,
827
+ )
828
+ return False, error_msg
829
+
830
+ def _link_milestone_to_github(
831
+ self, milestone_name: str, github_milestone_number: int
832
+ ) -> bool:
833
+ """Link milestone to GitHub milestone number by updating the milestone.
834
+
835
+ Args:
836
+ milestone_name: Local milestone name
837
+ github_milestone_number: GitHub milestone number
838
+
839
+ Returns:
840
+ True if successful, False otherwise
841
+ """
842
+ try:
843
+ if not hasattr(self.backend, "core") or not self.backend.core:
844
+ return False
845
+
846
+ # Get the milestone using the service layer
847
+ milestone = self.backend.core.milestone_service.get_milestone(
848
+ milestone_name
849
+ )
850
+ if not milestone:
851
+ logger.debug(
852
+ "milestone_not_found_for_linking",
853
+ milestone_name=milestone_name,
854
+ )
855
+ return False
856
+
857
+ # Update the milestone's github_milestone field directly
858
+ milestone.github_milestone = github_milestone_number
859
+ milestone.updated = now_utc()
860
+
861
+ # Save the updated milestone using the repository
862
+ self.backend.core.milestone_service.repository.save(milestone)
863
+
864
+ logger.debug(
865
+ "milestone_linked_to_github",
866
+ milestone_name=milestone_name,
867
+ github_number=github_milestone_number,
868
+ )
869
+ return True
870
+
871
+ except Exception as e:
872
+ logger.warning(
873
+ "failed_to_link_milestone",
874
+ milestone_name=milestone_name,
875
+ github_number=github_milestone_number,
876
+ error=str(e),
877
+ )
878
+ return False
879
+
880
+ def push_milestones(self, local_milestones: list) -> SyncReport:
881
+ """Push multiple local milestones to GitHub.
882
+
883
+ Args:
884
+ local_milestones: List of Milestone objects to push
885
+
886
+ Returns:
887
+ SyncReport with pushed, conflicts, and errors.
888
+ """
889
+ report = SyncReport()
890
+
891
+ if not local_milestones:
892
+ return report
893
+
894
+ logger.info("push_milestones_starting", milestone_count=len(local_milestones))
895
+
896
+ # For now, milestone pushing is not implemented
897
+ # GitHub milestones would need to be created via the API
898
+ # This is a placeholder for future implementation
899
+
900
+ for milestone in local_milestones:
901
+ report.errors[milestone.name] = "Milestone pushing not yet implemented"
902
+
903
+ logger.warning(
904
+ "push_milestones_not_implemented",
905
+ milestone_count=len(local_milestones),
906
+ )
907
+
908
+ return report