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,690 @@
1
+ """Sync state comparison service.
2
+
3
+ Provides logic to compare local and remote issue states, identifying
4
+ what needs to be pushed, pulled, or resolved. Backend-agnostic.
5
+ Supports both two-way (legacy) and three-way merge analysis.
6
+ """
7
+
8
+ from collections.abc import Mapping
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from structlog import get_logger
13
+
14
+ from roadmap.core.domain.issue import Issue
15
+ from roadmap.core.models.sync_models import SyncIssue
16
+ from roadmap.core.services.sync.sync_change_computer import (
17
+ compute_changes as _compute_changes_helper,
18
+ )
19
+ from roadmap.core.services.sync.sync_change_computer import (
20
+ compute_changes_remote as _compute_changes_remote_helper,
21
+ )
22
+ from roadmap.core.services.sync.sync_conflict_detector import (
23
+ detect_field_conflicts as _detect_field_conflicts_helper,
24
+ )
25
+ from roadmap.core.services.sync.sync_conflict_resolver import Conflict, ConflictField
26
+ from roadmap.core.services.sync.sync_key_normalizer import (
27
+ normalize_remote_keys as _normalize_remote_keys_helper,
28
+ )
29
+ from roadmap.core.services.sync.sync_report import IssueChange
30
+ from roadmap.core.services.sync.sync_state import IssueBaseState
31
+ from roadmap.core.services.sync.sync_state_normalizer import (
32
+ extract_timestamp as _extract_timestamp_helper,
33
+ )
34
+ from roadmap.core.services.sync.sync_state_normalizer import (
35
+ normalize_remote_state as _normalize_remote_state_helper,
36
+ )
37
+ from roadmap.core.services.sync.sync_three_way import (
38
+ build_issue_change as _build_issue_change_helper,
39
+ )
40
+
41
+ logger = get_logger()
42
+
43
+
44
+ class SyncStateComparator:
45
+ """Compares local and remote issue states.
46
+
47
+ Identifies conflicts, updates, and items requiring push/pull
48
+ without knowledge of the backend implementation.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ fields_to_sync: list[str] | None = None,
54
+ backend=None, # Optional: SyncBackendInterface for key normalization
55
+ ):
56
+ """Initialize the state comparator.
57
+
58
+ Args:
59
+ fields_to_sync: Fields to check for changes (default: common fields)
60
+ Note: 'title' is intentionally excluded as it's display metadata
61
+ that doesn't affect workflow state. Only sync fields that change
62
+ the work item's state (status, labels, priority, etc).
63
+ backend: Optional backend instance for normalizing remote issue keys.
64
+ When provided, remote issue keys are normalized to match local keys
65
+ using backend-specific ID mapping (e.g., gh-42 → 42).
66
+ """
67
+ self.logger = get_logger()
68
+ self.backend = backend
69
+ default_fields = [
70
+ "status",
71
+ "priority",
72
+ "content",
73
+ "assignee",
74
+ ]
75
+ self.fields_to_sync = fields_to_sync or default_fields
76
+ self.logger.debug(
77
+ "sync_state_comparator_initialized",
78
+ fields_to_sync=self.fields_to_sync,
79
+ backend_aware=backend is not None,
80
+ note="title excluded from sync as display metadata",
81
+ )
82
+
83
+ def _normalize_remote_keys(
84
+ self,
85
+ local: dict[str, Issue],
86
+ remote: Mapping[str, Any],
87
+ ) -> tuple[dict[str, Issue], Mapping[str, Any]]:
88
+ """Normalize remote issue keys to match local issue keys.
89
+
90
+ Local issues are keyed by UUID (e.g., "7e99d67b").
91
+ Remote issues may be keyed by backend-specific IDs (e.g., "gh-42" or "42").
92
+
93
+ This method creates a mapping by matching remote IDs to local remote_ids
94
+ using two sources (in order of preference):
95
+ 1. Database cache (fast O(1) lookups via RemoteLinkRepository)
96
+ 2. YAML files (fallback if database not available)
97
+
98
+ Args:
99
+ local: Dict of local issues keyed by UUID
100
+ remote: Dict of remote SyncIssue objects keyed by backend-specific ID
101
+
102
+ Returns:
103
+ Tuple of (local, normalized_remote) where normalized_remote is keyed
104
+ by local issue UUIDs for proper matching.
105
+ """
106
+ return _normalize_remote_keys_helper(
107
+ local, remote, self.backend, logger=self.logger
108
+ )
109
+
110
+ def identify_conflicts(
111
+ self,
112
+ local: dict[str, Issue],
113
+ remote: dict[str, dict[str, Any]],
114
+ ) -> list[Conflict]:
115
+ """Identify issues that differ in both local and remote.
116
+
117
+ A conflict exists when:
118
+ 1. Same issue exists in both local and remote
119
+ 2. They have different values for at least one field
120
+
121
+ Args:
122
+ local: Dict of local issues keyed by ID
123
+ remote: Dict of remote issues keyed by ID
124
+
125
+ Returns:
126
+ List of Conflict objects representing detected conflicts
127
+
128
+ Raises:
129
+ ValueError: If issue data is invalid
130
+ """
131
+ self.logger.info(
132
+ "identify_conflicts_start",
133
+ local_count=len(local),
134
+ remote_count=len(remote),
135
+ )
136
+
137
+ conflicts = []
138
+ common_ids = set(local.keys()) & set(remote.keys())
139
+
140
+ self.logger.debug(
141
+ "identifying_conflicts",
142
+ common_issue_count=len(common_ids),
143
+ )
144
+
145
+ for issue_id in common_ids:
146
+ try:
147
+ local_issue = local[issue_id]
148
+ remote_issue = remote[issue_id]
149
+
150
+ # Detect field-level conflicts
151
+ conflicting_fields = self._detect_field_conflicts(
152
+ local_issue, remote_issue
153
+ )
154
+
155
+ if conflicting_fields:
156
+ # Extract updated timestamps
157
+ local_updated = local_issue.updated
158
+ remote_updated = self._extract_timestamp(remote_issue, "updated_at")
159
+
160
+ conflict = Conflict(
161
+ issue_id=issue_id,
162
+ local_issue=local_issue,
163
+ remote_issue=remote_issue,
164
+ fields=conflicting_fields,
165
+ local_updated=local_updated,
166
+ remote_updated=remote_updated,
167
+ )
168
+
169
+ conflicts.append(conflict)
170
+ self.logger.debug(
171
+ "conflict_identified",
172
+ issue_id=issue_id,
173
+ field_count=len(conflicting_fields),
174
+ )
175
+
176
+ except Exception as e:
177
+ self.logger.error(
178
+ "conflict_detection_error",
179
+ issue_id=issue_id,
180
+ error=str(e),
181
+ )
182
+ raise ValueError(
183
+ f"Failed to detect conflicts for {issue_id}: {str(e)}"
184
+ ) from e
185
+
186
+ self.logger.info("identify_conflicts_complete", conflict_count=len(conflicts))
187
+ return conflicts
188
+
189
+ def identify_updates(
190
+ self,
191
+ local: dict[str, Issue],
192
+ remote: dict[str, dict[str, Any]],
193
+ ) -> list[Issue]:
194
+ """Identify local issues that need pushing to remote.
195
+
196
+ An update is needed when:
197
+ 1. Issue exists only in local (new issue)
198
+ 2. Issue exists in both but local is newer
199
+
200
+ Args:
201
+ local: Dict of local issues keyed by ID
202
+ remote: Dict of remote issues keyed by ID
203
+
204
+ Returns:
205
+ List of Issues that need to be pushed
206
+
207
+ Raises:
208
+ ValueError: If issue data is invalid
209
+ """
210
+ self.logger.info(
211
+ "identify_updates_start",
212
+ local_count=len(local),
213
+ remote_count=len(remote),
214
+ )
215
+
216
+ updates = []
217
+
218
+ for issue_id, local_issue in local.items():
219
+ try:
220
+ # New issue (doesn't exist in remote)
221
+ if issue_id not in remote:
222
+ self.logger.debug("new_local_issue_identified", issue_id=issue_id)
223
+ updates.append(local_issue)
224
+ continue
225
+
226
+ # Existing issue - check if local is newer
227
+ remote_issue = remote[issue_id]
228
+ remote_updated = self._extract_timestamp(remote_issue, "updated_at")
229
+
230
+ if remote_updated is None:
231
+ # Can't compare - assume local should be pushed
232
+ self.logger.debug(
233
+ "no_remote_timestamp_treating_as_update",
234
+ issue_id=issue_id,
235
+ )
236
+ updates.append(local_issue)
237
+ continue
238
+
239
+ if local_issue.updated > remote_updated:
240
+ time_diff = (local_issue.updated - remote_updated).total_seconds()
241
+ self.logger.debug(
242
+ "local_is_newer_needs_update",
243
+ issue_id=issue_id,
244
+ seconds_ahead=time_diff,
245
+ )
246
+ updates.append(local_issue)
247
+
248
+ except Exception as e:
249
+ self.logger.error(
250
+ "update_detection_error",
251
+ issue_id=issue_id,
252
+ error=str(e),
253
+ )
254
+ raise ValueError(
255
+ f"Failed to detect updates for {issue_id}: {str(e)}"
256
+ ) from e
257
+
258
+ self.logger.info("identify_updates_complete", update_count=len(updates))
259
+ return updates
260
+
261
+ def identify_pulls(
262
+ self,
263
+ local: dict[str, Issue],
264
+ remote: dict[str, dict[str, Any]],
265
+ ) -> list[dict[str, Any]]:
266
+ """Identify remote issues that need pulling to local.
267
+
268
+ A pull is needed when:
269
+ 1. Issue exists only in remote (new from remote)
270
+ 2. Issue exists in both but remote is newer
271
+
272
+ Args:
273
+ local: Dict of local issues keyed by ID
274
+ remote: Dict of remote issues keyed by ID
275
+
276
+ Returns:
277
+ List of remote issues that need to be pulled
278
+
279
+ Raises:
280
+ ValueError: If issue data is invalid
281
+ """
282
+ self.logger.info(
283
+ "identify_pulls_start",
284
+ local_count=len(local),
285
+ remote_count=len(remote),
286
+ )
287
+
288
+ pulls = []
289
+
290
+ for issue_id, remote_issue in remote.items():
291
+ try:
292
+ # New remote issue (doesn't exist locally)
293
+ if issue_id not in local:
294
+ self.logger.debug("new_remote_issue_identified", issue_id=issue_id)
295
+ pulls.append(issue_id)
296
+ continue
297
+
298
+ # Existing issue - check if remote is newer
299
+ local_issue = local[issue_id]
300
+ remote_updated = self._extract_timestamp(remote_issue, "updated_at")
301
+
302
+ if remote_updated is None:
303
+ # Can't compare - skip
304
+ self.logger.debug(
305
+ "no_remote_timestamp_skipping_pull",
306
+ issue_id=issue_id,
307
+ )
308
+ continue
309
+
310
+ if remote_updated > local_issue.updated:
311
+ time_diff = (remote_updated - local_issue.updated).total_seconds()
312
+ self.logger.debug(
313
+ "remote_is_newer_needs_pull",
314
+ issue_id=issue_id,
315
+ seconds_ahead=time_diff,
316
+ )
317
+ pulls.append(issue_id)
318
+
319
+ except Exception as e:
320
+ self.logger.error(
321
+ "pull_detection_error",
322
+ issue_id=issue_id,
323
+ error=str(e),
324
+ )
325
+ raise ValueError(
326
+ f"Failed to detect pulls for {issue_id}: {str(e)}"
327
+ ) from e
328
+
329
+ self.logger.info("identify_pulls_complete", pull_count=len(pulls))
330
+ return pulls
331
+
332
+ def identify_up_to_date(
333
+ self,
334
+ local: dict[str, Issue],
335
+ remote: dict[str, dict[str, Any]],
336
+ ) -> list[str]:
337
+ """Identify issues that are up-to-date in both local and remote.
338
+
339
+ An issue is up-to-date when:
340
+ 1. It exists in both local and remote
341
+ 2. They have identical values (or no conflict detected)
342
+ 3. Timestamps are equal
343
+
344
+ Args:
345
+ local: Dict of local issues keyed by ID
346
+ remote: Dict of remote issues keyed by ID
347
+
348
+ Returns:
349
+ List of issue IDs that are up-to-date
350
+
351
+ Raises:
352
+ ValueError: If issue data is invalid
353
+ """
354
+ self.logger.info(
355
+ "identify_up_to_date_start",
356
+ local_count=len(local),
357
+ remote_count=len(remote),
358
+ )
359
+
360
+ up_to_date = []
361
+ common_ids = set(local.keys()) & set(remote.keys())
362
+
363
+ for issue_id in common_ids:
364
+ try:
365
+ local_issue = local[issue_id]
366
+ remote_issue = remote[issue_id]
367
+
368
+ # Check if they have conflicts
369
+ conflicts = self._detect_field_conflicts(local_issue, remote_issue)
370
+
371
+ if conflicts:
372
+ # Has conflicts - not up to date
373
+ continue
374
+
375
+ # Check timestamps
376
+ remote_updated = self._extract_timestamp(remote_issue, "updated_at")
377
+
378
+ if remote_updated is not None and local_issue.updated != remote_updated:
379
+ # Different timestamps - not up to date
380
+ continue
381
+
382
+ # No conflicts and timestamps match (or can't compare)
383
+ up_to_date.append(issue_id)
384
+ self.logger.debug("issue_is_up_to_date", issue_id=issue_id)
385
+
386
+ except Exception as e:
387
+ self.logger.warning(
388
+ "up_to_date_check_error",
389
+ issue_id=issue_id,
390
+ error=str(e),
391
+ )
392
+ # If we can't determine, assume not up-to-date
393
+ continue
394
+
395
+ self.logger.info(
396
+ "identify_up_to_date_complete", up_to_date_count=len(up_to_date)
397
+ )
398
+ return up_to_date
399
+
400
+ def _detect_field_conflicts(
401
+ self,
402
+ local: Issue,
403
+ remote: dict[str, Any] | object,
404
+ ) -> list[ConflictField]:
405
+ """Detect field-level conflicts between local and remote issues.
406
+
407
+ Compares only fields that affect workflow state. The 'title' field is
408
+ intentionally excluded as it's metadata that doesn't change work item
409
+ state or require synchronization.
410
+
411
+ Args:
412
+ local: The local Issue
413
+ remote: The remote issue dict
414
+
415
+ Returns:
416
+ List of ConflictFields representing conflicts
417
+
418
+ Raises:
419
+ ValueError: If field data is invalid
420
+ """
421
+ return _detect_field_conflicts_helper(
422
+ local,
423
+ remote,
424
+ self.fields_to_sync,
425
+ extract_timestamp=self._extract_timestamp,
426
+ logger=self.logger,
427
+ )
428
+
429
+ def _extract_timestamp(
430
+ self,
431
+ data: dict[str, Any] | object,
432
+ timestamp_field: str = "updated_at",
433
+ ) -> datetime | None:
434
+ """Extract and parse a timestamp from remote issue data.
435
+
436
+ Args:
437
+ data: The remote issue dict
438
+ timestamp_field: The field name containing the timestamp
439
+
440
+ Returns:
441
+ Parsed datetime or None if not found/invalid
442
+
443
+ Raises:
444
+ ValueError: If timestamp format is invalid
445
+ """
446
+ return _extract_timestamp_helper(data, timestamp_field, logger=self.logger)
447
+
448
+ def _resolve_issue_title(
449
+ self,
450
+ issue_id: str,
451
+ local: Issue | None,
452
+ remote: Any | None,
453
+ baseline: IssueBaseState | None,
454
+ ) -> str:
455
+ """Resolve a sensible title for presentation and logging."""
456
+ title = None
457
+ if local is not None:
458
+ title = getattr(local, "title", None)
459
+ if not title and remote is not None:
460
+ title = (
461
+ remote.get("title")
462
+ if isinstance(remote, dict)
463
+ else getattr(remote, "title", None)
464
+ )
465
+ if not title and baseline is not None:
466
+ title = getattr(baseline, "title", issue_id)
467
+ return title or issue_id
468
+
469
+ def _normalize_remote_state(self, remote: Any) -> dict[str, Any] | None:
470
+ """Normalize remote representations into a plain dict for comparison."""
471
+ return _normalize_remote_state_helper(remote, logger=self.logger)
472
+
473
+ def _handle_first_sync_semantics(
474
+ self,
475
+ change: IssueChange,
476
+ local: Issue | None,
477
+ remote_state: dict | None,
478
+ baseline: IssueBaseState | None,
479
+ ) -> None:
480
+ """Handle first-sync special cases where baseline is missing."""
481
+ if local is not None and remote_state is None:
482
+ change.local_changes = (
483
+ self._compute_changes(baseline, local) if local is not None else {}
484
+ )
485
+ change.local_changes["_new"] = True
486
+ change.conflict_type = "local_only"
487
+ change.has_conflict = False
488
+ return
489
+
490
+ if remote_state is not None and local is None:
491
+ change.remote_changes = (
492
+ self._compute_changes_remote(baseline, remote_state)
493
+ if remote_state is not None
494
+ else {}
495
+ )
496
+ change.remote_changes["_new"] = True
497
+ change.conflict_type = "remote_only"
498
+ change.has_conflict = False
499
+ return
500
+
501
+ if local is not None and remote_state is not None:
502
+ # Both present: simplified rule - differing status => conflict
503
+ from roadmap.common.constants import Status
504
+
505
+ # Normalize remote status
506
+ remote_status = (
507
+ remote_state.get("status") if isinstance(remote_state, dict) else None
508
+ )
509
+ try:
510
+ if isinstance(remote_status, str):
511
+ try:
512
+ remote_status = Status(remote_status)
513
+ except Exception:
514
+ remote_status = Status(remote_status.lower())
515
+ except Exception as e:
516
+ logger.debug(
517
+ "status_normalization_failed",
518
+ operation="normalize_status",
519
+ field="remote_status",
520
+ error=str(e),
521
+ action="Skipping status normalization",
522
+ )
523
+
524
+ local_status = getattr(local, "status", None)
525
+
526
+ def status_value(s):
527
+ if s is None:
528
+ return None
529
+ return s.value if hasattr(s, "value") else str(s).lower()
530
+
531
+ change.local_changes.setdefault("_new", True)
532
+ change.remote_changes.setdefault("_new", True)
533
+
534
+ if status_value(local_status) == status_value(remote_status):
535
+ change.local_changes = {}
536
+ change.remote_changes = {}
537
+ change.conflict_type = "no_change"
538
+ change.has_conflict = False
539
+ else:
540
+ mutual_conflicts = self._detect_field_conflicts(local, remote_state)
541
+ flagged = {}
542
+ for c in mutual_conflicts:
543
+ flagged[c.field_name] = {
544
+ "local": c.local_value,
545
+ "remote": c.remote_value,
546
+ }
547
+ change.flagged_conflicts = flagged
548
+ change.conflict_type = "both_changed"
549
+ change.has_conflict = True
550
+
551
+ def _detect_and_flag_conflicts(
552
+ self, change: IssueChange, local: Issue | None, remote_state: dict | None
553
+ ) -> None:
554
+ """Detect field-level conflicts when a baseline exists."""
555
+ if local is not None and remote_state is not None:
556
+ conflicts = self._detect_field_conflicts(local, remote_state)
557
+ if conflicts:
558
+ flagged = {}
559
+ for c in conflicts:
560
+ flagged[c.field_name] = {
561
+ "local": c.local_value,
562
+ "remote": c.remote_value,
563
+ }
564
+ change.flagged_conflicts = flagged
565
+
566
+ def _build_issue_change(
567
+ self,
568
+ issue_id: str,
569
+ local: Issue | None,
570
+ remote: dict[str, Any] | None,
571
+ baseline: IssueBaseState | None = None,
572
+ ) -> IssueChange:
573
+ """Construct an IssueChange for a single issue using three-way context.
574
+
575
+ This centralizes the logic of computing local/remote changes, detecting
576
+ field-level conflicts, and setting conflict metadata for the analyzer.
577
+ """
578
+ return _build_issue_change_helper(
579
+ issue_id,
580
+ local,
581
+ remote,
582
+ baseline,
583
+ resolve_title=self._resolve_issue_title,
584
+ normalize_remote_state=self._normalize_remote_state,
585
+ compute_changes=self._compute_changes,
586
+ compute_changes_remote=self._compute_changes_remote,
587
+ handle_first_sync_semantics=self._handle_first_sync_semantics,
588
+ detect_and_flag_conflicts=self._detect_and_flag_conflicts,
589
+ extract_timestamp=self._extract_timestamp,
590
+ fields_to_sync=self.fields_to_sync,
591
+ logger=self.logger,
592
+ )
593
+
594
+ def analyze_three_way(
595
+ self,
596
+ local: dict[str, Issue],
597
+ remote: Mapping[str, Any],
598
+ baseline: dict[str, IssueBaseState] | None = None,
599
+ ) -> list[IssueChange]:
600
+ """Analyze changes using three-way merge context.
601
+
602
+ Compares baseline → local and baseline → remote to identify
603
+ what changed in each direction, providing complete conflict context.
604
+
605
+ Args:
606
+ local: Dict of local issues keyed by ID
607
+ remote: Dict of remote SyncIssue objects keyed by ID
608
+ baseline: Dict of baseline states keyed by ID (from last sync)
609
+
610
+ Returns:
611
+ List of IssueChange objects with three-way context
612
+
613
+ Raises:
614
+ ValueError: If analysis fails
615
+ """
616
+ self.logger.info(
617
+ "analyze_three_way_start",
618
+ local_count=len(local),
619
+ remote_count=len(remote),
620
+ baseline_count=len(baseline or {}),
621
+ backend_aware=self.backend is not None,
622
+ )
623
+
624
+ if baseline is None:
625
+ baseline = {}
626
+
627
+ # Normalize remote keys to match local UUIDs (if backend aware)
628
+ local, remote = self._normalize_remote_keys(local, remote)
629
+
630
+ changes = []
631
+ all_issue_ids = set(local.keys()) | set(remote.keys())
632
+
633
+ for issue_id in all_issue_ids:
634
+ try:
635
+ change = self._build_issue_change(
636
+ issue_id,
637
+ local.get(issue_id),
638
+ remote.get(issue_id),
639
+ baseline.get(issue_id) if baseline else None,
640
+ )
641
+ changes.append(change)
642
+ self.logger.debug(
643
+ "three_way_analysis_complete",
644
+ issue_id=issue_id,
645
+ conflict_type=change.conflict_type,
646
+ )
647
+ except Exception as e:
648
+ self.logger.error(
649
+ "three_way_analysis_error",
650
+ issue_id=issue_id,
651
+ error=str(e),
652
+ )
653
+ raise ValueError(
654
+ f"Failed to analyze {issue_id} in three-way merge: {str(e)}"
655
+ ) from e
656
+
657
+ self.logger.info("analyze_three_way_complete", change_count=len(changes))
658
+ return changes
659
+
660
+ def _compute_changes(
661
+ self,
662
+ baseline: IssueBaseState | None,
663
+ local: Issue,
664
+ ) -> dict[str, Any]:
665
+ """Compute what changed between baseline and local issue.
666
+
667
+ Args:
668
+ baseline: The baseline state
669
+ local: The current local issue
670
+
671
+ Returns:
672
+ Dict of field → value for changed fields
673
+ """
674
+ return _compute_changes_helper(baseline, local, logger=self.logger)
675
+
676
+ def _compute_changes_remote(
677
+ self,
678
+ baseline: IssueBaseState | None,
679
+ remote: SyncIssue | dict[str, Any],
680
+ ) -> dict[str, Any]:
681
+ """Compute what changed between baseline and remote issue.
682
+
683
+ Args:
684
+ baseline: The baseline state
685
+ remote: The current remote SyncIssue
686
+
687
+ Returns:
688
+ Dict of field → value for changed fields
689
+ """
690
+ return _compute_changes_remote_helper(baseline, remote, logger=self.logger)