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,383 @@
1
+ """Error types for sync operations.
2
+
3
+ This module defines error types used with the Result pattern for sync operations,
4
+ enabling explicit error handling without exceptions.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+ from typing import Any
10
+
11
+
12
+ class SyncErrorType(StrEnum):
13
+ """Types of errors that can occur during sync operations."""
14
+
15
+ # Authentication errors
16
+ AUTHENTICATION_FAILED = "authentication_failed"
17
+ TOKEN_EXPIRED = "token_expired"
18
+ PERMISSION_DENIED = "permission_denied"
19
+
20
+ # Network errors
21
+ NETWORK_ERROR = "network_error"
22
+ TIMEOUT = "timeout"
23
+ SERVICE_UNAVAILABLE = "service_unavailable"
24
+ API_RATE_LIMIT = "api_rate_limit"
25
+
26
+ # Data errors
27
+ INVALID_DATA = "invalid_data"
28
+ SCHEMA_MISMATCH = "schema_mismatch"
29
+ DUPLICATE_ENTITY = "duplicate_entity"
30
+ VALIDATION_ERROR = "validation_error"
31
+
32
+ # Resource errors
33
+ RESOURCE_NOT_FOUND = "resource_not_found"
34
+ RESOURCE_DELETED = "resource_deleted"
35
+ MILESTONE_NOT_FOUND = "milestone_not_found"
36
+ PROJECT_NOT_FOUND = "project_not_found"
37
+
38
+ # Conflict errors
39
+ CONFLICT = "conflict"
40
+ MERGE_CONFLICT = "merge_conflict"
41
+
42
+ # System errors
43
+ DATABASE_ERROR = "database_error"
44
+ FILE_SYSTEM_ERROR = "file_system_error"
45
+ CONFIGURATION_ERROR = "configuration_error"
46
+
47
+ # Circuit breaker errors
48
+ CIRCUIT_BREAKER_OPEN = "circuit_breaker_open"
49
+ RETRY_EXHAUSTED = "retry_exhausted"
50
+
51
+ # Unknown
52
+ UNKNOWN_ERROR = "unknown_error"
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class SyncError:
57
+ """Error information for sync operations.
58
+
59
+ Provides structured error information for use with Result pattern,
60
+ including error type, message, and recovery suggestions.
61
+
62
+ Attributes:
63
+ error_type: The category of error
64
+ message: Human-readable error message
65
+ entity_type: Type of entity affected (Issue, Milestone, Project, etc.)
66
+ entity_id: Identifier of affected entity (optional)
67
+ is_recoverable: Whether the error can be recovered from
68
+ suggested_fix: User-actionable recovery suggestion (optional)
69
+ metadata: Additional error context (optional)
70
+ cause: Original exception if wrapped (optional)
71
+ """
72
+
73
+ error_type: SyncErrorType
74
+ message: str
75
+ entity_type: str = "Unknown"
76
+ entity_id: str | None = None
77
+ is_recoverable: bool = True
78
+ suggested_fix: str | None = None
79
+ metadata: dict[str, Any] | None = None
80
+ cause: Exception | None = None
81
+
82
+ def __str__(self) -> str:
83
+ """Get string representation of error.
84
+
85
+ Returns:
86
+ Formatted error message
87
+ """
88
+ parts = [f"{self.error_type.value}: {self.message}"]
89
+ if self.entity_id:
90
+ parts.append(f"(entity: {self.entity_type} #{self.entity_id})")
91
+ if self.suggested_fix:
92
+ parts.append(f"Fix: {self.suggested_fix}")
93
+ return " ".join(parts)
94
+
95
+ def with_entity(
96
+ self, entity_type: str, entity_id: str | None = None
97
+ ) -> "SyncError":
98
+ """Create a new error with entity information.
99
+
100
+ Args:
101
+ entity_type: Type of entity (Issue, Milestone, etc.)
102
+ entity_id: Entity identifier
103
+
104
+ Returns:
105
+ New SyncError with entity information
106
+ """
107
+ return SyncError(
108
+ error_type=self.error_type,
109
+ message=self.message,
110
+ entity_type=entity_type,
111
+ entity_id=entity_id,
112
+ is_recoverable=self.is_recoverable,
113
+ suggested_fix=self.suggested_fix,
114
+ metadata=self.metadata,
115
+ cause=self.cause,
116
+ )
117
+
118
+ def with_suggestion(self, suggested_fix: str) -> "SyncError":
119
+ """Create a new error with a recovery suggestion.
120
+
121
+ Args:
122
+ suggested_fix: User-actionable suggestion
123
+
124
+ Returns:
125
+ New SyncError with suggestion
126
+ """
127
+ return SyncError(
128
+ error_type=self.error_type,
129
+ message=self.message,
130
+ entity_type=self.entity_type,
131
+ entity_id=self.entity_id,
132
+ is_recoverable=self.is_recoverable,
133
+ suggested_fix=suggested_fix,
134
+ metadata=self.metadata,
135
+ cause=self.cause,
136
+ )
137
+
138
+ @classmethod
139
+ def from_exception(
140
+ cls,
141
+ exception: Exception,
142
+ error_type: SyncErrorType = SyncErrorType.UNKNOWN_ERROR,
143
+ entity_type: str = "Unknown",
144
+ entity_id: str | None = None,
145
+ ) -> "SyncError":
146
+ """Create SyncError from an exception.
147
+
148
+ Args:
149
+ exception: The exception to wrap
150
+ error_type: Category of error (default: UNKNOWN_ERROR)
151
+ entity_type: Type of affected entity
152
+ entity_id: Identifier of affected entity
153
+
154
+ Returns:
155
+ SyncError wrapping the exception
156
+ """
157
+ # Infer error type from exception if possible
158
+ inferred_type = cls._infer_error_type(exception)
159
+ if inferred_type:
160
+ error_type = inferred_type
161
+
162
+ return cls(
163
+ error_type=error_type,
164
+ message=str(exception),
165
+ entity_type=entity_type,
166
+ entity_id=entity_id,
167
+ is_recoverable=cls._is_recoverable(error_type),
168
+ suggested_fix=cls._get_suggested_fix(error_type),
169
+ cause=exception,
170
+ )
171
+
172
+ @staticmethod
173
+ def _infer_error_type(exception: Exception) -> SyncErrorType | None:
174
+ """Infer error type from exception class and message.
175
+
176
+ Args:
177
+ exception: The exception to analyze
178
+
179
+ Returns:
180
+ Inferred error type or None
181
+ """
182
+ exc_name = type(exception).__name__
183
+ exc_msg = str(exception).lower()
184
+
185
+ # Check exception class names first
186
+ class_mappings = {
187
+ ("TimeoutError", "ConnectTimeout"): SyncErrorType.TIMEOUT,
188
+ ("ConnectionError", "ConnectError"): SyncErrorType.NETWORK_ERROR,
189
+ ("ValidationError",): SyncErrorType.VALIDATION_ERROR,
190
+ (
191
+ "IntegrityError",
192
+ "DatabaseError",
193
+ "OperationalError",
194
+ ): SyncErrorType.DATABASE_ERROR,
195
+ }
196
+
197
+ for classes, error_type in class_mappings.items():
198
+ if exc_name in classes:
199
+ return error_type
200
+
201
+ # Check message patterns - order matters for specificity
202
+ message_patterns = [
203
+ # More specific patterns first
204
+ (["token", "expired"], SyncErrorType.TOKEN_EXPIRED),
205
+ (["milestone", "not found"], SyncErrorType.MILESTONE_NOT_FOUND),
206
+ (["project", "not found"], SyncErrorType.PROJECT_NOT_FOUND),
207
+ # Less specific patterns
208
+ (["timeout"], SyncErrorType.TIMEOUT),
209
+ (["connection"], SyncErrorType.NETWORK_ERROR),
210
+ (["rate limit", "429"], SyncErrorType.API_RATE_LIMIT),
211
+ (["auth", "401"], SyncErrorType.AUTHENTICATION_FAILED),
212
+ (["permission", "403"], SyncErrorType.PERMISSION_DENIED),
213
+ (["not found", "404"], SyncErrorType.RESOURCE_NOT_FOUND),
214
+ (["validation"], SyncErrorType.VALIDATION_ERROR),
215
+ (["duplicate", "unique"], SyncErrorType.DUPLICATE_ENTITY),
216
+ (["schema"], SyncErrorType.SCHEMA_MISMATCH),
217
+ (["conflict"], SyncErrorType.CONFLICT),
218
+ ]
219
+
220
+ for patterns, error_type in message_patterns:
221
+ if all(pattern in exc_msg for pattern in patterns):
222
+ return error_type
223
+
224
+ return None
225
+
226
+ @staticmethod
227
+ def _is_recoverable(error_type: SyncErrorType) -> bool:
228
+ """Determine if an error type is recoverable.
229
+
230
+ Args:
231
+ error_type: The error type
232
+
233
+ Returns:
234
+ True if the error can be recovered from
235
+ """
236
+ non_recoverable = {
237
+ SyncErrorType.PERMISSION_DENIED,
238
+ SyncErrorType.SCHEMA_MISMATCH,
239
+ SyncErrorType.CONFIGURATION_ERROR,
240
+ }
241
+ return error_type not in non_recoverable
242
+
243
+ @staticmethod
244
+ def _get_suggested_fix(error_type: SyncErrorType) -> str | None:
245
+ """Get suggested fix for an error type.
246
+
247
+ Args:
248
+ error_type: The error type
249
+
250
+ Returns:
251
+ Suggested fix or None
252
+ """
253
+ fixes = {
254
+ SyncErrorType.AUTHENTICATION_FAILED: "Check your credentials and token",
255
+ SyncErrorType.TOKEN_EXPIRED: "Refresh or regenerate your access token",
256
+ SyncErrorType.PERMISSION_DENIED: "Verify you have required permissions",
257
+ SyncErrorType.API_RATE_LIMIT: "Wait for rate limit to reset or reduce request frequency",
258
+ SyncErrorType.TIMEOUT: "Check network connection and try again",
259
+ SyncErrorType.NETWORK_ERROR: "Check network connection and try again",
260
+ SyncErrorType.RESOURCE_NOT_FOUND: "Verify the resource exists and ID is correct",
261
+ SyncErrorType.DUPLICATE_ENTITY: "Use duplicate detection to resolve conflicts",
262
+ SyncErrorType.CIRCUIT_BREAKER_OPEN: "Wait for circuit breaker to reset, then retry",
263
+ SyncErrorType.RETRY_EXHAUSTED: "Check error logs and resolve underlying issue",
264
+ SyncErrorType.CONFIGURATION_ERROR: "Review and fix configuration settings",
265
+ }
266
+ return fixes.get(error_type)
267
+
268
+
269
+ # Common error constructors for convenience
270
+
271
+
272
+ def authentication_error(message: str = "Authentication failed") -> SyncError:
273
+ """Create an authentication error.
274
+
275
+ Args:
276
+ message: Error message
277
+
278
+ Returns:
279
+ SyncError with AUTHENTICATION_FAILED type
280
+ """
281
+ return SyncError(
282
+ error_type=SyncErrorType.AUTHENTICATION_FAILED,
283
+ message=message,
284
+ is_recoverable=True,
285
+ suggested_fix="Check your credentials and token",
286
+ )
287
+
288
+
289
+ def network_error(message: str = "Network error occurred") -> SyncError:
290
+ """Create a network error.
291
+
292
+ Args:
293
+ message: Error message
294
+
295
+ Returns:
296
+ SyncError with NETWORK_ERROR type
297
+ """
298
+ return SyncError(
299
+ error_type=SyncErrorType.NETWORK_ERROR,
300
+ message=message,
301
+ is_recoverable=True,
302
+ suggested_fix="Check network connection and try again",
303
+ )
304
+
305
+
306
+ def rate_limit_error(retry_after: int | None = None) -> SyncError:
307
+ """Create a rate limit error.
308
+
309
+ Args:
310
+ retry_after: Seconds until rate limit resets
311
+
312
+ Returns:
313
+ SyncError with API_RATE_LIMIT type
314
+ """
315
+ message = "API rate limit exceeded"
316
+ if retry_after:
317
+ message += f" (retry after {retry_after}s)"
318
+
319
+ metadata = {"retry_after": retry_after} if retry_after else None
320
+
321
+ return SyncError(
322
+ error_type=SyncErrorType.API_RATE_LIMIT,
323
+ message=message,
324
+ is_recoverable=True,
325
+ suggested_fix="Wait for rate limit to reset or reduce request frequency",
326
+ metadata=metadata,
327
+ )
328
+
329
+
330
+ def resource_not_found_error(resource_type: str, resource_id: str) -> SyncError:
331
+ """Create a resource not found error.
332
+
333
+ Args:
334
+ resource_type: Type of resource (Issue, Milestone, etc.)
335
+ resource_id: Resource identifier
336
+
337
+ Returns:
338
+ SyncError with RESOURCE_NOT_FOUND type
339
+ """
340
+ return SyncError(
341
+ error_type=SyncErrorType.RESOURCE_NOT_FOUND,
342
+ message=f"{resource_type} not found",
343
+ entity_type=resource_type,
344
+ entity_id=resource_id,
345
+ is_recoverable=False,
346
+ suggested_fix="Verify the resource exists and ID is correct",
347
+ )
348
+
349
+
350
+ def conflict_error(
351
+ entity_type: str,
352
+ entity_id: str,
353
+ message: str = "Conflict detected",
354
+ ) -> SyncError:
355
+ """Create a conflict error.
356
+
357
+ Args:
358
+ entity_type: Type of entity
359
+ entity_id: Entity identifier
360
+ message: Conflict description
361
+
362
+ Returns:
363
+ SyncError with CONFLICT type
364
+ """
365
+ return SyncError(
366
+ error_type=SyncErrorType.CONFLICT,
367
+ message=message,
368
+ entity_type=entity_type,
369
+ entity_id=entity_id,
370
+ is_recoverable=True,
371
+ suggested_fix="Resolve the conflict manually or use --interactive mode",
372
+ )
373
+
374
+
375
+ __all__ = [
376
+ "SyncError",
377
+ "SyncErrorType",
378
+ "authentication_error",
379
+ "network_error",
380
+ "rate_limit_error",
381
+ "resource_not_found_error",
382
+ "conflict_error",
383
+ ]
@@ -0,0 +1,222 @@
1
+ """Helpers for normalizing remote keys to local UUIDs.
2
+
3
+ This module encapsulates the mapping logic previously embedded in
4
+ `SyncStateComparator._normalize_remote_keys`, allowing focused testing and
5
+ re-use by other components.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from typing import Any
12
+
13
+ import structlog
14
+
15
+ logger = structlog.get_logger()
16
+
17
+
18
+ def _build_remote_id_mapping(
19
+ local: dict[str, Any],
20
+ backend: Any,
21
+ remote_link_repo: Any,
22
+ logger: Any | None = None,
23
+ ) -> tuple[dict[str, str], bool]:
24
+ """Build reverse mapping from remote ID to local UUID.
25
+
26
+ Returns:
27
+ (remote_id_to_local_uuid dict, db_lookup_used bool)
28
+ """
29
+ remote_id_to_local_uuid: dict[str, str] = {}
30
+ db_lookup_available = False
31
+ backend_name = backend.get_backend_name()
32
+
33
+ # Try database lookup first
34
+ if remote_link_repo:
35
+ db_lookup_available = True
36
+ db_links = remote_link_repo.get_all_links_for_backend(backend_name)
37
+ for issue_uuid, remote_id in db_links.items():
38
+ remote_id_key = str(remote_id)
39
+ remote_id_to_local_uuid[remote_id_key] = issue_uuid
40
+ if logger is not None:
41
+ try:
42
+ logger.debug(
43
+ "loaded_remote_links_from_database",
44
+ backend=backend_name,
45
+ link_count=len(remote_id_to_local_uuid),
46
+ )
47
+ except Exception as logging_error:
48
+ logger.error(
49
+ "logger_failed",
50
+ operation="log_remote_links_loaded",
51
+ error=str(logging_error),
52
+ )
53
+
54
+ # Supplement from YAML if DB is missing entries
55
+ if not db_lookup_available or len(remote_id_to_local_uuid) < len(local):
56
+ for local_uuid, local_issue in local.items():
57
+ remote_ids = getattr(local_issue, "remote_ids", None)
58
+ if remote_ids and backend_name in remote_ids:
59
+ remote_id = remote_ids[backend_name]
60
+ remote_id_key = str(remote_id)
61
+ if remote_id_key not in remote_id_to_local_uuid:
62
+ remote_id_to_local_uuid[remote_id_key] = local_uuid
63
+ if logger is not None:
64
+ try:
65
+ logger.debug(
66
+ "loaded_remote_id_from_yaml",
67
+ remote_id=remote_id_key,
68
+ local_uuid=local_uuid,
69
+ backend=backend_name,
70
+ )
71
+ except Exception as logging_error:
72
+ logger.error(
73
+ "logger_failed",
74
+ operation="log_loaded_remote_id_from_yaml",
75
+ error=str(logging_error),
76
+ )
77
+
78
+ return remote_id_to_local_uuid, db_lookup_available
79
+
80
+
81
+ def _apply_remote_normalization(
82
+ remote: Mapping[str, Any],
83
+ remote_id_to_local_uuid: dict[str, str],
84
+ backend_name: str,
85
+ logger: Any | None = None,
86
+ ) -> tuple[dict[str, Any], int]:
87
+ """Apply ID mapping to normalize remote issues.
88
+
89
+ Returns:
90
+ (normalized_remote dict, count of unmatched)
91
+ """
92
+ normalized_remote: dict[str, Any] = {}
93
+ unmatched_count = 0
94
+
95
+ # Partition remote issues into matched and unmatched
96
+ for remote_key, remote_issue in remote.items():
97
+ remote_key_str = str(remote_key)
98
+ if remote_key_str in remote_id_to_local_uuid:
99
+ local_uuid = remote_id_to_local_uuid[remote_key_str]
100
+ normalized_remote[local_uuid] = remote_issue
101
+ if logger is not None:
102
+ try:
103
+ logger.debug(
104
+ "normalized_remote_key",
105
+ original_key=remote_key_str,
106
+ normalized_to=local_uuid,
107
+ )
108
+ except Exception as logging_error:
109
+ logger.error(
110
+ "logger_failed",
111
+ operation="log_normalized_remote_key",
112
+ error=str(logging_error),
113
+ )
114
+ else:
115
+ prefixed_key = f"_remote_{remote_key}"
116
+ normalized_remote[prefixed_key] = remote_issue
117
+ unmatched_count += 1
118
+ if logger is not None:
119
+ try:
120
+ logger.debug(
121
+ "new_remote_issue",
122
+ remote_key=str(remote_key),
123
+ prefixed_key=prefixed_key,
124
+ )
125
+ except Exception as logging_error:
126
+ logger.error(
127
+ "logger_failed",
128
+ operation="log_new_remote_issue",
129
+ error=str(logging_error),
130
+ )
131
+
132
+ return normalized_remote, unmatched_count
133
+
134
+
135
+ def normalize_remote_keys(
136
+ local: dict[str, Any],
137
+ remote: Mapping[str, Any],
138
+ backend: Any | None,
139
+ logger: Any | None = None,
140
+ ) -> tuple[dict[str, Any], Mapping[str, Any]]:
141
+ """Normalize remote issue keys to match local issue UUIDs.
142
+
143
+ Returns (local, normalized_remote) where `normalized_remote` is keyed by
144
+ local UUIDs where a mapping exists, and unmatched remote keys are
145
+ preserved with a `_remote_{key}` prefix.
146
+ """
147
+ if backend is None:
148
+ return local, remote
149
+
150
+ backend_name = backend.get_backend_name()
151
+ remote_link_repo = getattr(backend, "remote_link_repo", None)
152
+
153
+ # Build ID mapping from DB + YAML
154
+ remote_id_to_local_uuid, db_lookup_available = _build_remote_id_mapping(
155
+ local, backend, remote_link_repo, logger
156
+ )
157
+ if logger is not None:
158
+ try:
159
+ logger.info(
160
+ "remote_link_mapping_built",
161
+ backend=backend_name,
162
+ db_lookup_used=db_lookup_available,
163
+ local_issue_count=len(local),
164
+ remote_issue_count=len(remote),
165
+ mapped_links=len(remote_id_to_local_uuid),
166
+ )
167
+ except Exception as logging_error:
168
+ logger.error(
169
+ "logger_failed",
170
+ operation="log_remote_link_mapping_built",
171
+ error=str(logging_error),
172
+ )
173
+
174
+ # Apply mapping to normalize remote
175
+ normalized_remote, unmatched_count = _apply_remote_normalization(
176
+ remote, remote_id_to_local_uuid, backend_name, logger
177
+ )
178
+ if logger is not None:
179
+ try:
180
+ sample_unmatched = [
181
+ str(key)
182
+ for key in list(remote.keys())[:5]
183
+ if str(key) not in remote_id_to_local_uuid
184
+ ]
185
+ logger.debug(
186
+ "remote_link_mapping_sample",
187
+ backend=backend_name,
188
+ sample_unmatched=sample_unmatched,
189
+ )
190
+ except Exception as logging_error:
191
+ logger.error(
192
+ "logger_failed",
193
+ operation="log_remote_link_mapping_sample",
194
+ error=str(logging_error),
195
+ )
196
+
197
+ if logger is not None:
198
+ try:
199
+ logger.info(
200
+ "remote_keys_normalized",
201
+ original_remote_count=len(remote),
202
+ normalized_count=len(normalized_remote),
203
+ matched=len(remote) - unmatched_count,
204
+ unmatched=unmatched_count,
205
+ backend=backend_name,
206
+ db_lookup_used=db_lookup_available,
207
+ )
208
+ if unmatched_count > 0 and not db_lookup_available:
209
+ logger.warning(
210
+ "remote_keys_unmatched_no_db_links",
211
+ unmatched=unmatched_count,
212
+ backend=backend_name,
213
+ hint="remote issues appear unlinked to local IDs",
214
+ )
215
+ except Exception as logging_error:
216
+ logger.error(
217
+ "logger_failed",
218
+ operation="log_remote_keys_normalized",
219
+ error=str(logging_error),
220
+ )
221
+
222
+ return local, normalized_remote