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,360 @@
1
+ """Dependency analysis service for detecting issues in issue relationships.
2
+
3
+ Analyzes depends_on and blocks relationships to detect:
4
+ - Circular dependencies (A → B → A)
5
+ - Broken references (pointing to non-existent issues)
6
+ - Self-dependencies (issue depends on itself)
7
+ - Orphaned blockers (issue blocks non-existent issue)
8
+ - Deep dependency chains (chains > 5 levels)
9
+ """
10
+
11
+ from dataclasses import dataclass, field
12
+ from enum import StrEnum
13
+
14
+ from roadmap.common.logging import get_logger
15
+ from roadmap.common.logging.error_logging import (
16
+ log_error_with_context,
17
+ )
18
+ from roadmap.common.observability.instrumentation import traced
19
+ from roadmap.core.domain.issue import Issue
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class DependencyIssueType(StrEnum):
25
+ """Types of dependency issues that can be found."""
26
+
27
+ CIRCULAR = "circular_dependency"
28
+ BROKEN = "broken_dependency"
29
+ SELF = "self_dependency"
30
+ ORPHANED_BLOCKER = "orphaned_blocker"
31
+ DEEP_CHAIN = "deep_dependency_chain"
32
+ MISSING_BIDIRECTIONAL = "missing_bidirectional"
33
+
34
+
35
+ @dataclass
36
+ class DependencyIssue:
37
+ """A problem found in issue dependencies."""
38
+
39
+ issue_id: str
40
+ issue_type: DependencyIssueType
41
+ message: str
42
+ affected_issues: list[str] = field(default_factory=list)
43
+ chain_length: int | None = None # For deep chain issues
44
+ severity: str = "warning" # error, warning, info
45
+
46
+ def __post_init__(self):
47
+ """Set severity based on issue type."""
48
+ if self.issue_type == DependencyIssueType.CIRCULAR:
49
+ self.severity = "error"
50
+ elif self.issue_type == DependencyIssueType.BROKEN:
51
+ self.severity = "error"
52
+ elif self.issue_type == DependencyIssueType.SELF:
53
+ self.severity = "error"
54
+ elif self.issue_type == DependencyIssueType.ORPHANED_BLOCKER:
55
+ self.severity = "warning"
56
+ elif self.issue_type == DependencyIssueType.DEEP_CHAIN:
57
+ self.severity = "info"
58
+ elif self.issue_type == DependencyIssueType.MISSING_BIDIRECTIONAL:
59
+ self.severity = "warning"
60
+
61
+
62
+ @dataclass
63
+ class DependencyAnalysisResult:
64
+ """Result of analyzing all dependencies."""
65
+
66
+ total_issues: int
67
+ issues_with_dependencies: int
68
+ issues_with_problems: int
69
+ problems: list[DependencyIssue] = field(default_factory=list)
70
+ circular_chains: list[list[str]] = field(
71
+ default_factory=list
72
+ ) # Lists of issue IDs forming cycles
73
+
74
+ @property
75
+ def is_healthy(self) -> bool:
76
+ """Check if all dependencies are healthy."""
77
+ return (
78
+ not any(p.severity == "error" for p in self.problems)
79
+ and not self.circular_chains
80
+ )
81
+
82
+ @property
83
+ def error_count(self) -> int:
84
+ """Count of critical errors."""
85
+ return len([p for p in self.problems if p.severity == "error"]) + len(
86
+ self.circular_chains
87
+ )
88
+
89
+ @property
90
+ def warning_count(self) -> int:
91
+ """Count of warnings."""
92
+ return len([p for p in self.problems if p.severity == "warning"])
93
+
94
+ @property
95
+ def info_count(self) -> int:
96
+ """Count of informational issues."""
97
+ return len([p for p in self.problems if p.severity == "info"])
98
+
99
+
100
+ class DependencyAnalyzer:
101
+ """Analyzes issue dependency relationships for problems."""
102
+
103
+ def __init__(self):
104
+ """Initialize the analyzer."""
105
+ self._issue_map: dict[str, Issue] = {}
106
+ self._analyzed = False
107
+
108
+ @traced("analyze_dependencies")
109
+ def analyze(self, issues: list[Issue]) -> DependencyAnalysisResult:
110
+ """Analyze all dependencies in the issue set.
111
+
112
+ Args:
113
+ issues: List of all issues in the system
114
+
115
+ Returns:
116
+ DependencyAnalysisResult with all problems found
117
+ """
118
+ try:
119
+ self._issue_map = {issue.id: issue for issue in issues}
120
+ self._analyzed = True
121
+
122
+ result = DependencyAnalysisResult(
123
+ total_issues=len(issues),
124
+ issues_with_dependencies=sum(
125
+ 1 for i in issues if i.depends_on or i.blocks
126
+ ),
127
+ issues_with_problems=0,
128
+ )
129
+
130
+ # Check all issues
131
+ for issue in issues:
132
+ self._check_issue_dependencies(issue, result)
133
+
134
+ result.issues_with_problems = len({p.issue_id for p in result.problems})
135
+
136
+ return result
137
+ except Exception as e:
138
+ log_error_with_context(
139
+ e,
140
+ operation="analyze_dependencies",
141
+ entity_type="Issue",
142
+ additional_context={"total_issues": len(issues)},
143
+ )
144
+ raise
145
+
146
+ def _check_depends_on_links(
147
+ self, issue: Issue, result: DependencyAnalysisResult
148
+ ) -> None:
149
+ """Check the depends_on list for self-deps, broken refs, and missing bidirectional links."""
150
+ for dep_id in issue.depends_on or []:
151
+ if dep_id == issue.id:
152
+ result.problems.append(
153
+ DependencyIssue(
154
+ issue_id=issue.id,
155
+ issue_type=DependencyIssueType.SELF,
156
+ message="Issue depends on itself",
157
+ affected_issues=[dep_id],
158
+ )
159
+ )
160
+ continue
161
+ if dep_id not in self._issue_map:
162
+ result.problems.append(
163
+ DependencyIssue(
164
+ issue_id=issue.id,
165
+ issue_type=DependencyIssueType.BROKEN,
166
+ message=f"Depends on non-existent issue {dep_id}",
167
+ affected_issues=[dep_id],
168
+ )
169
+ )
170
+ continue
171
+ dep_issue = self._issue_map[dep_id]
172
+ if issue.id not in (dep_issue.blocks or []):
173
+ result.problems.append(
174
+ DependencyIssue(
175
+ issue_id=issue.id,
176
+ issue_type=DependencyIssueType.MISSING_BIDIRECTIONAL,
177
+ message=f"Depends on {dep_id} but {dep_id} does not list this as a blocker",
178
+ affected_issues=[dep_id],
179
+ )
180
+ )
181
+
182
+ def _check_blocks_links(
183
+ self, issue: Issue, result: DependencyAnalysisResult
184
+ ) -> None:
185
+ """Check the blocks list for self-blocks, orphaned refs, and missing bidirectional links."""
186
+ for blocked_id in issue.blocks or []:
187
+ if blocked_id == issue.id:
188
+ result.problems.append(
189
+ DependencyIssue(
190
+ issue_id=issue.id,
191
+ issue_type=DependencyIssueType.SELF,
192
+ message="Issue blocks itself",
193
+ affected_issues=[blocked_id],
194
+ )
195
+ )
196
+ continue
197
+ if blocked_id not in self._issue_map:
198
+ result.problems.append(
199
+ DependencyIssue(
200
+ issue_id=issue.id,
201
+ issue_type=DependencyIssueType.ORPHANED_BLOCKER,
202
+ message=f"Blocks non-existent issue {blocked_id}",
203
+ affected_issues=[blocked_id],
204
+ )
205
+ )
206
+ continue
207
+ blocked_issue = self._issue_map[blocked_id]
208
+ if issue.id not in (blocked_issue.depends_on or []):
209
+ result.problems.append(
210
+ DependencyIssue(
211
+ issue_id=issue.id,
212
+ issue_type=DependencyIssueType.MISSING_BIDIRECTIONAL,
213
+ message=f"Blocks {blocked_id} but {blocked_id} does not list this as a dependency",
214
+ affected_issues=[blocked_id],
215
+ )
216
+ )
217
+
218
+ def _check_chain_and_cycles(
219
+ self, issue: Issue, result: DependencyAnalysisResult
220
+ ) -> None:
221
+ """Check for deep dependency chains and circular dependencies."""
222
+ if not issue.depends_on:
223
+ return
224
+ max_depth = self._find_max_chain_depth(issue.id, set(), 0)
225
+ if max_depth > 5:
226
+ result.problems.append(
227
+ DependencyIssue(
228
+ issue_id=issue.id,
229
+ issue_type=DependencyIssueType.DEEP_CHAIN,
230
+ message=f"Deep dependency chain ({max_depth} levels)",
231
+ affected_issues=issue.depends_on,
232
+ chain_length=max_depth,
233
+ )
234
+ )
235
+ for dep_id in issue.depends_on:
236
+ cycle = self._find_cycle_from(dep_id, issue.id, set())
237
+ if cycle:
238
+ cycle_list = sorted(list(cycle) + [issue.id])
239
+ if cycle_list not in result.circular_chains:
240
+ result.circular_chains.append(cycle_list)
241
+ result.problems.append(
242
+ DependencyIssue(
243
+ issue_id=issue.id,
244
+ issue_type=DependencyIssueType.CIRCULAR,
245
+ message=f"Circular dependency detected: {' → '.join(cycle_list)} → {issue.id}",
246
+ affected_issues=cycle_list,
247
+ )
248
+ )
249
+
250
+ def _check_issue_dependencies(
251
+ self, issue: Issue, result: DependencyAnalysisResult
252
+ ) -> None:
253
+ """Check all dependency relationships for a single issue."""
254
+ self._check_depends_on_links(issue, result)
255
+ self._check_blocks_links(issue, result)
256
+ self._check_chain_and_cycles(issue, result)
257
+
258
+ def _find_cycle_from(
259
+ self, current_id: str, target_id: str, visited: set[str]
260
+ ) -> set[str] | None:
261
+ """Find if there's a path from current_id back to target_id.
262
+
263
+ Args:
264
+ current_id: ID to start searching from
265
+ target_id: ID we're trying to reach (forming a cycle)
266
+ visited: IDs already visited in this search
267
+
268
+ Returns:
269
+ Set of IDs in the cycle if found, None otherwise
270
+ """
271
+ if current_id not in self._issue_map:
272
+ return None
273
+
274
+ if current_id in visited:
275
+ return None # Already visited this node, not our cycle
276
+
277
+ if current_id == target_id:
278
+ return {current_id} # Found the cycle!
279
+
280
+ visited.add(current_id)
281
+ issue = self._issue_map[current_id]
282
+
283
+ # Check all issues this one depends on
284
+ for dep_id in issue.depends_on or []:
285
+ result = self._find_cycle_from(dep_id, target_id, visited.copy())
286
+ if result:
287
+ result.add(current_id)
288
+ return result
289
+
290
+ return None
291
+
292
+ def _find_max_chain_depth(
293
+ self, current_id: str, visited: set[str], depth: int
294
+ ) -> int:
295
+ """Find the maximum dependency chain depth from current issue.
296
+
297
+ Args:
298
+ current_id: Issue ID to start from
299
+ visited: Set of visited IDs (to avoid infinite loops)
300
+ depth: Current depth in the chain
301
+
302
+ Returns:
303
+ Maximum chain depth found
304
+ """
305
+ if current_id not in self._issue_map:
306
+ return depth
307
+
308
+ if current_id in visited:
309
+ return depth # Cycle detected, return current depth
310
+
311
+ visited.add(current_id)
312
+ issue = self._issue_map[current_id]
313
+
314
+ if not issue.depends_on:
315
+ return depth # No further dependencies
316
+
317
+ # Find the maximum depth through any dependency
318
+ max_depth = depth
319
+ for dep_id in issue.depends_on:
320
+ depth_through_dep = self._find_max_chain_depth(
321
+ dep_id, visited.copy(), depth + 1
322
+ )
323
+ max_depth = max(max_depth, depth_through_dep)
324
+
325
+ return max_depth
326
+
327
+ @traced("get_issues_affecting")
328
+ def get_issues_affecting(self, issue_id: str) -> list[str]:
329
+ """Get all issues that this one affects through dependency chain.
330
+
331
+ Args:
332
+ issue_id: The issue to start from
333
+
334
+ Returns:
335
+ List of issue IDs that would be affected if this issue changes
336
+ """
337
+ if issue_id not in self._issue_map:
338
+ return []
339
+
340
+ affected = set()
341
+ self._collect_affected(issue_id, affected)
342
+ return sorted(affected)
343
+
344
+ def _collect_affected(self, issue_id: str, affected: set[str]):
345
+ """Recursively collect all issues affected by this one.
346
+
347
+ An issue is affected if this one blocks it (directly or indirectly).
348
+ """
349
+ if not self._analyzed:
350
+ return
351
+
352
+ # Find all issues that depend on this one (it affects them)
353
+ if issue_id in self._issue_map:
354
+ issue = self._issue_map[issue_id]
355
+ for blocked_id in issue.blocks or []:
356
+ if blocked_id in self._issue_map:
357
+ if blocked_id not in affected:
358
+ affected.add(blocked_id)
359
+ # Recursively get issues affected by the blocked issue
360
+ self._collect_affected(blocked_id, affected)
@@ -0,0 +1,126 @@
1
+ """Detects field-level conflicts between local and remote issues.
2
+
3
+ Extracted from SyncStateComparator to separate conflict detection logic.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from structlog import get_logger
9
+
10
+ from roadmap.core.domain.issue import Issue
11
+ from roadmap.core.services.sync.sync_conflict_resolver import ConflictField
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class FieldConflictDetector:
17
+ """Detects field-level conflicts between local and remote issues."""
18
+
19
+ def __init__(self, fields_to_sync: list[str] | None = None):
20
+ """Initialize with fields to check for conflicts.
21
+
22
+ Args:
23
+ fields_to_sync: List of field names to check for conflicts
24
+ """
25
+ self.fields_to_sync = fields_to_sync or [
26
+ "status",
27
+ "priority",
28
+ "content",
29
+ "labels",
30
+ "assignee",
31
+ ]
32
+
33
+ def detect_field_conflicts(
34
+ self, local_issue: Issue, remote_issue: Any, extractors: dict | None = None
35
+ ) -> list[ConflictField]:
36
+ """Detect which fields differ between local and remote issues.
37
+
38
+ Args:
39
+ local_issue: Local Issue object
40
+ remote_issue: Remote issue object or dict
41
+ extractors: Optional dict of field→extractor function mappings
42
+
43
+ Returns:
44
+ List of ConflictField objects for conflicting fields
45
+ """
46
+ conflicting_fields = []
47
+
48
+ if extractors is None:
49
+ extractors = {}
50
+
51
+ for field in self.fields_to_sync:
52
+ try:
53
+ # Get local value
54
+ local_value = self._get_field_value(local_issue, field)
55
+
56
+ # Get remote value
57
+ remote_value = self._get_field_value(remote_issue, field)
58
+
59
+ # Check for conflict
60
+ if local_value != remote_value:
61
+ conflict_field = ConflictField(
62
+ field_name=field,
63
+ local_value=local_value,
64
+ remote_value=remote_value,
65
+ local_updated=getattr(local_issue, "updated", None),
66
+ remote_updated=getattr(remote_issue, "updated_at", None),
67
+ )
68
+ conflicting_fields.append(conflict_field)
69
+ logger.debug(
70
+ "conflict_detected_in_field",
71
+ field=field,
72
+ local_value=str(local_value)[:100],
73
+ remote_value=str(remote_value)[:100],
74
+ )
75
+
76
+ except Exception as e:
77
+ logger.warning(
78
+ "conflict_detection_error",
79
+ field=field,
80
+ error=str(e),
81
+ )
82
+ continue
83
+
84
+ return conflicting_fields
85
+
86
+ @staticmethod
87
+ def _get_field_value(obj: Any, field: str) -> Any:
88
+ """Get field value from object (handles dict and objects).
89
+
90
+ Args:
91
+ obj: Object or dict to extract field from
92
+ field: Field name
93
+
94
+ Returns:
95
+ Field value, or None if not found
96
+ """
97
+ if obj is None:
98
+ return None
99
+
100
+ try:
101
+ if isinstance(obj, dict):
102
+ return obj.get(field)
103
+
104
+ # Handle enums (e.g., Status enum)
105
+ value = getattr(obj, field, None)
106
+ if value is None:
107
+ return None
108
+
109
+ # If it's an enum with .value, use that
110
+ if hasattr(value, "value"):
111
+ return value.value
112
+
113
+ # Handle special cases
114
+ if field == "labels" and isinstance(value, list):
115
+ return sorted(value) if value else []
116
+
117
+ return value
118
+
119
+ except Exception as e:
120
+ logger.debug(
121
+ "normalize_field_value_failed",
122
+ operation="normalize_field_value",
123
+ error=str(e),
124
+ action="Returning None",
125
+ )
126
+ return None
@@ -0,0 +1,202 @@
1
+ """Remote data fetching with retry logic and request management."""
2
+
3
+ import time
4
+ from collections.abc import Iterable
5
+ from typing import Any
6
+
7
+ from structlog import get_logger
8
+
9
+ from roadmap.core.services.utils.retry_policy import RetryPolicy
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class RemoteFetcher:
15
+ """Utility for fetching remote issue data via an adapter.
16
+
17
+ Adds retry/backoff logic and basic rate-limit handling to adapter calls.
18
+ Prefer batch APIs (`pull_issues`) when available; fall back to
19
+ per-item APIs (`pull_issue`) otherwise.
20
+ """
21
+
22
+ DEFAULT_RETRY_POLICY = RetryPolicy(max_retries=3, base_delay=0.5, factor=2.0)
23
+
24
+ @staticmethod
25
+ def _handle_rate_limit_from_response(resp: Any) -> float:
26
+ """Inspect a response-like object for rate-limit headers and return sleep seconds.
27
+
28
+ Returns 0.0 if no rate-limit sleep is required. If the response exposes
29
+ `headers` or `raw_response` with `X-RateLimit-Remaining` and
30
+ `X-RateLimit-Reset`, this will compute the seconds until reset.
31
+ """
32
+ try:
33
+ from collections.abc import Mapping
34
+
35
+ headers = None
36
+ if isinstance(resp, Mapping):
37
+ headers = resp.get("headers")
38
+ else:
39
+ headers = getattr(resp, "headers", None)
40
+ if headers is None:
41
+ raw = getattr(resp, "raw_response", None)
42
+ if isinstance(raw, Mapping):
43
+ headers = raw.get("headers")
44
+ else:
45
+ headers = getattr(raw, "headers", None)
46
+
47
+ if not headers or not isinstance(headers, Mapping):
48
+ return 0.0
49
+
50
+ remaining = headers.get("X-RateLimit-Remaining") or headers.get(
51
+ "x-ratelimit-remaining"
52
+ )
53
+ reset = headers.get("X-RateLimit-Reset") or headers.get("x-ratelimit-reset")
54
+ if remaining is not None and str(remaining) == "0" and reset:
55
+ try:
56
+ reset_ts = int(reset)
57
+ now_ts = int(time.time())
58
+ wait = max(0, reset_ts - now_ts)
59
+ return float(wait)
60
+ except Exception as e:
61
+ logger.debug(
62
+ "reset_time_parsing_failed",
63
+ operation="parse_reset_time",
64
+ reset_value=str(reset),
65
+ error=str(e),
66
+ action="Using default wait time 0",
67
+ )
68
+ return 0.0
69
+ except Exception as e:
70
+ logger.debug(
71
+ "rate_limit_check_failed",
72
+ operation="check_rate_limit",
73
+ error=str(e),
74
+ action="Assuming no rate limit",
75
+ )
76
+ return 0.0
77
+ return 0.0
78
+
79
+ @staticmethod
80
+ def fetch_issue(
81
+ adapter: Any, issue_id: str, retry_policy: RetryPolicy | None = None
82
+ ) -> Any:
83
+ """Fetch a single issue from remote adapter with retry logic.
84
+
85
+ Args:
86
+ adapter: Remote adapter instance with pull_issue method.
87
+ issue_id: The issue ID to fetch.
88
+ retry_policy: Optional custom retry policy.
89
+
90
+ Returns:
91
+ The fetched issue or None if fetch fails.
92
+ """
93
+ if adapter is None:
94
+ return None
95
+
96
+ rp = retry_policy or RemoteFetcher.DEFAULT_RETRY_POLICY
97
+
98
+ for attempt in range(1, rp.max_retries + 1):
99
+ try:
100
+ if hasattr(adapter, "pull_issue"):
101
+ result = adapter.pull_issue(issue_id)
102
+ # Check for rate-limit hints
103
+ sleep_for = RemoteFetcher._handle_rate_limit_from_response(result)
104
+ if sleep_for and sleep_for > 0:
105
+ logger.info(
106
+ "remote_rate_limited",
107
+ issue_id=issue_id,
108
+ wait_seconds=sleep_for,
109
+ )
110
+ time.sleep(sleep_for)
111
+ return result
112
+ return None
113
+ except Exception as e:
114
+ logger.warning(
115
+ "remote_fetch_issue_error",
116
+ issue_id=issue_id,
117
+ attempt=attempt,
118
+ error=str(e),
119
+ severity="infrastructure",
120
+ )
121
+ if attempt == rp.max_retries:
122
+ logger.error(
123
+ "remote_fetch_issue_failed",
124
+ issue_id=issue_id,
125
+ attempts=attempt,
126
+ severity="infrastructure",
127
+ )
128
+ return None
129
+ # Backoff
130
+ delay = rp.get_backoff_delay(attempt)
131
+ logger.info(
132
+ "remote_fetch_retry",
133
+ issue_id=issue_id,
134
+ attempt=attempt + 1,
135
+ delay=delay,
136
+ )
137
+ time.sleep(delay)
138
+
139
+ return None
140
+
141
+ @staticmethod
142
+ def fetch_issues(
143
+ adapter: Any, issue_ids: Iterable[str], retry_policy: RetryPolicy | None = None
144
+ ) -> list[Any]:
145
+ """Fetch multiple issues from remote adapter with retry logic.
146
+
147
+ Args:
148
+ adapter: Remote adapter instance with pull_issue method.
149
+ issue_ids: Iterable of issue IDs to fetch.
150
+ retry_policy: Optional custom retry policy.
151
+
152
+ Returns:
153
+ List of fetched issues.
154
+ """
155
+ if adapter is None:
156
+ return []
157
+
158
+ rp = retry_policy or RemoteFetcher.DEFAULT_RETRY_POLICY
159
+
160
+ ids = list(issue_ids)
161
+ if not ids:
162
+ return []
163
+
164
+ # Try batch API first with retries
165
+ if hasattr(adapter, "pull_issues"):
166
+ for attempt in range(1, rp.max_retries + 1):
167
+ try:
168
+ result = adapter.pull_issues(ids)
169
+ # If adapter returns report-like object, inspect for rate-limit
170
+ sleep_for = RemoteFetcher._handle_rate_limit_from_response(result)
171
+ if sleep_for and sleep_for > 0:
172
+ logger.info(
173
+ "remote_rate_limited_batch",
174
+ count=len(ids),
175
+ wait_seconds=sleep_for,
176
+ )
177
+ time.sleep(sleep_for)
178
+ return result or []
179
+ except Exception as e:
180
+ logger.warning(
181
+ "remote_fetch_issues_error",
182
+ attempt=attempt,
183
+ error=str(e),
184
+ severity="infrastructure",
185
+ )
186
+ if attempt == rp.max_retries:
187
+ logger.error(
188
+ "remote_fetch_issues_failed",
189
+ attempts=attempt,
190
+ severity="infrastructure",
191
+ )
192
+ break
193
+ delay = rp.get_backoff_delay(attempt)
194
+ logger.info("remote_fetch_retry", attempt=attempt + 1, delay=delay)
195
+ time.sleep(delay)
196
+
197
+ # Fallback to individual calls with retries per-item
198
+ results: list[Any] = []
199
+ for i in ids:
200
+ res = RemoteFetcher.fetch_issue(adapter, i, retry_policy=rp)
201
+ results.append(res)
202
+ return results