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,1117 @@
1
+ """Top-level sync command - backend-agnostic interface to sync operations.
2
+
3
+ This command provides a unified interface for syncing with any backend
4
+ (GitHub, Git, etc.) without requiring the user to specify which backend.
5
+ The backend is automatically detected from configuration.
6
+ """
7
+
8
+ import sys
9
+
10
+ import click
11
+ from structlog import get_logger
12
+
13
+ from roadmap.adapters.cli.cli_command_helpers import require_initialized
14
+ from roadmap.adapters.cli.sync_metrics_command import sync_metrics
15
+ from roadmap.common.console import get_console
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ def _show_baseline(core, backend, verbose, console_inst) -> bool:
21
+ """Handle the `--base` flag: show or create baseline state."""
22
+ from roadmap.adapters.cli.sync_handlers import show_baseline
23
+
24
+ return show_baseline(core, backend, verbose, console_inst)
25
+
26
+
27
+ def _reset_baseline(core, backend, verbose, console_inst) -> bool:
28
+ """Handle the `--reset-baseline` flag: force recalculation of baseline."""
29
+ from roadmap.adapters.cli.services.sync_service import get_sync_backend
30
+ from roadmap.adapters.cli.sync_context import (
31
+ _clear_baseline_db,
32
+ _create_and_save_baseline,
33
+ _resolve_backend_and_init,
34
+ )
35
+
36
+ console_inst.print(
37
+ "⚠️ WARNING: Resetting baseline will:",
38
+ style="bold yellow",
39
+ )
40
+ console_inst.print(" • Clear all sync history")
41
+ console_inst.print(" • Treat all current issues as the new baseline")
42
+ console_inst.print(" • Next sync will see them as baseline (no changes)")
43
+ console_inst.print()
44
+
45
+ if not click.confirm("Continue with baseline reset?"):
46
+ console_inst.print("Cancelled.", style="dim")
47
+ return True
48
+
49
+ # Determine backend
50
+ backend_type, sync_backend = _resolve_backend_and_init(
51
+ core, backend, get_sync_backend
52
+ )
53
+ if not sync_backend:
54
+ console_inst.print("❌ Failed to initialize backend", style="bold red")
55
+ sys.exit(1)
56
+
57
+ # Clear existing baseline from database
58
+ _clear_baseline_db(core, console_inst)
59
+
60
+ # Create fresh baseline from current local state and save
61
+ success = _create_and_save_baseline(
62
+ core, sync_backend, backend_type, console_inst, verbose
63
+ )
64
+ if not success:
65
+ sys.exit(1)
66
+
67
+ return True
68
+
69
+
70
+ def _init_sync_context(
71
+ core,
72
+ backend,
73
+ baseline_option,
74
+ dry_run,
75
+ verbose,
76
+ console_inst,
77
+ enable_duplicate_detection=True,
78
+ title_threshold=0.90,
79
+ content_threshold=0.85,
80
+ auto_resolve_threshold=0.95,
81
+ ):
82
+ """Initialize backend, orchestrator, and required services for sync command.
83
+
84
+ Returns tuple: (backend_type, sync_backend, orchestrator, pre_sync_baseline, pre_sync_issue_count, state_comparator, conflict_resolver)
85
+ """
86
+ from roadmap.adapters.cli.sync_context import _init_sync_context as _ctx
87
+
88
+ return _ctx(
89
+ core,
90
+ backend,
91
+ baseline_option,
92
+ dry_run,
93
+ verbose,
94
+ console_inst,
95
+ enable_duplicate_detection,
96
+ title_threshold,
97
+ content_threshold,
98
+ auto_resolve_threshold,
99
+ )
100
+
101
+
102
+ def _resolve_backend_and_init(core, backend, get_sync_backend_callable):
103
+ """Resolve backend_type and initialize the sync backend instance.
104
+
105
+ Returns tuple `(backend_type, sync_backend)` where `sync_backend` may
106
+ be None if initialization failed.
107
+ """
108
+ from roadmap.adapters.cli.sync_context import _resolve_backend_and_init as _ctx
109
+
110
+ return _ctx(core, backend, get_sync_backend_callable)
111
+
112
+
113
+ def _clear_baseline_db(core, console_inst):
114
+ """Clear baseline entries from the SQLite DB if present.
115
+
116
+ Logs warnings but does not raise on failure.
117
+ """
118
+ from roadmap.adapters.cli.sync_context import _clear_baseline_db as _ctx
119
+
120
+ return _ctx(core, console_inst)
121
+
122
+
123
+ def _create_and_save_baseline(
124
+ core, sync_backend, backend_type, console_inst, verbose
125
+ ) -> bool:
126
+ """Create an initial baseline from local issues and save it to DB.
127
+
128
+ Returns True on success, False otherwise.
129
+ """
130
+ from roadmap.adapters.cli.sync_context import _create_and_save_baseline as _ctx
131
+
132
+ return _ctx(core, sync_backend, backend_type, console_inst, verbose)
133
+
134
+
135
+ def _capture_and_save_post_sync_baseline(
136
+ core, console_inst, pre_sync_issue_count, verbose
137
+ ):
138
+ """Capture local issues and save them as the post-sync baseline."""
139
+ from roadmap.adapters.cli.sync_handlers import capture_and_save_post_sync_baseline
140
+
141
+ return capture_and_save_post_sync_baseline(
142
+ core, console_inst, pre_sync_issue_count, verbose
143
+ )
144
+
145
+
146
+ def _perform_apply_phase(
147
+ core,
148
+ orchestrator,
149
+ console_inst,
150
+ analysis_report,
151
+ force_local,
152
+ force_remote,
153
+ push,
154
+ pull,
155
+ verbose,
156
+ ):
157
+ """Run the actual apply phase: perform sync and display summary."""
158
+ from roadmap.adapters.cli.sync_handlers import perform_apply_phase
159
+
160
+ return perform_apply_phase(
161
+ core,
162
+ orchestrator,
163
+ console_inst,
164
+ analysis_report,
165
+ force_local,
166
+ force_remote,
167
+ push,
168
+ pull,
169
+ verbose,
170
+ )
171
+
172
+
173
+ def _present_apply_intent(analysis_report, console_inst) -> bool:
174
+ """Present whether there are changes to apply and return True if apply is needed."""
175
+ from roadmap.adapters.cli.sync_handlers import present_apply_intent
176
+
177
+ return present_apply_intent(analysis_report, console_inst)
178
+
179
+
180
+ def _confirm_and_apply(
181
+ core,
182
+ orchestrator,
183
+ console_inst,
184
+ analysis_report,
185
+ force_local,
186
+ force_remote,
187
+ push,
188
+ pull,
189
+ verbose,
190
+ interactive,
191
+ ):
192
+ """Ask for confirmation and run the apply phase if confirmed."""
193
+ from roadmap.adapters.cli.sync_handlers import confirm_and_apply
194
+
195
+ return confirm_and_apply(
196
+ core,
197
+ orchestrator,
198
+ console_inst,
199
+ analysis_report,
200
+ force_local,
201
+ force_remote,
202
+ push,
203
+ pull,
204
+ verbose,
205
+ interactive,
206
+ )
207
+
208
+
209
+ def _finalize_sync(
210
+ core,
211
+ console_inst,
212
+ report,
213
+ pre_sync_issue_count,
214
+ verbose,
215
+ backend_type: str | None,
216
+ ):
217
+ """Finalize sync run: capture post-sync baseline and print completion messages."""
218
+ from roadmap.adapters.cli.sync_handlers import finalize_sync
219
+
220
+ finalize_sync(
221
+ core,
222
+ console_inst,
223
+ report,
224
+ pre_sync_issue_count,
225
+ verbose,
226
+ backend_type,
227
+ )
228
+
229
+
230
+ def _run_analysis_phase(
231
+ orchestrator,
232
+ push,
233
+ pull,
234
+ dry_run,
235
+ verbose,
236
+ console_inst,
237
+ interactive_duplicates=False,
238
+ ):
239
+ """Run analysis phase using orchestrator and present results.
240
+
241
+ Returns: tuple(plan, analysis_report)
242
+ """
243
+ from roadmap.adapters.cli.sync_handlers import run_analysis_phase
244
+
245
+ return run_analysis_phase(
246
+ orchestrator, push, pull, dry_run, verbose, console_inst, interactive_duplicates
247
+ )
248
+
249
+
250
+ def _build_local_status_breakdown(local_only_issues):
251
+ """Build status breakdown string for local-only issues."""
252
+ from collections import Counter
253
+
254
+ statuses = [c.local_state.status for c in local_only_issues if c.local_state]
255
+ status_counts = Counter(statuses)
256
+ return ", ".join(f"{s}: {count}" for s, count in sorted(status_counts.items()))
257
+
258
+
259
+ def _build_remote_status_breakdown(remote_only_issues):
260
+ """Build status breakdown string for remote-only issues."""
261
+ from collections import Counter
262
+
263
+ statuses = [
264
+ c.remote_state.get("status") for c in remote_only_issues if c.remote_state
265
+ ]
266
+ status_counts = Counter(statuses)
267
+ return ", ".join(f"{s}: {count}" for s, count in sorted(status_counts.items()))
268
+
269
+
270
+ def _display_local_only_issues(local_only_issues, console_inst):
271
+ """Display local-only issues in a formatted table."""
272
+ from rich.table import Table
273
+
274
+ console_inst.print(
275
+ "\n[bold cyan]📝 Local-Only Issues[/bold cyan] (exist locally but not remotely)"
276
+ )
277
+
278
+ table = Table(show_header=True, header_style="bold cyan")
279
+ table.add_column("Local ID", style="dim", width=10)
280
+ table.add_column("Title", width=50)
281
+ table.add_column("Status", style="yellow", width=12)
282
+
283
+ for change in sorted(local_only_issues, key=lambda c: c.title):
284
+ status = change.local_state.status if change.local_state else "unknown"
285
+ table.add_row(change.issue_id[:8], change.title, status)
286
+
287
+ console_inst.print(table)
288
+
289
+ # Show status breakdown
290
+ breakdown = _build_local_status_breakdown(local_only_issues)
291
+ console_inst.print(
292
+ f"[dim]Total: {len(local_only_issues)} issues ({breakdown})[/dim]"
293
+ )
294
+
295
+
296
+ def _display_remote_only_issues(remote_only_issues, console_inst):
297
+ """Display remote-only issues in a formatted table with link status."""
298
+ from rich.table import Table
299
+
300
+ console_inst.print(
301
+ "\n[bold cyan]🔄 Remote-Only Issues[/bold cyan] (exist remotely but not locally)"
302
+ )
303
+
304
+ table = Table(show_header=True, header_style="bold cyan")
305
+ table.add_column("Local ID", style="dim", width=10)
306
+ table.add_column("Remote ID", style="cyan", width=12)
307
+ table.add_column("Title", width=40)
308
+ table.add_column("Status", style="yellow", width=10)
309
+ table.add_column("Linked", width=8)
310
+
311
+ # Count linked vs orphaned
312
+ linked_count = 0
313
+ orphaned_count = 0
314
+
315
+ for change in sorted(remote_only_issues, key=lambda c: c.title):
316
+ status = change.remote_state.get("status") if change.remote_state else "unknown"
317
+ # Extract remote ID (backend_id) from remote_state
318
+ remote_id = (
319
+ change.remote_state.get("backend_id") if change.remote_state else "?"
320
+ )
321
+
322
+ # Determine if linked (has a local ID that's not "_remote_")
323
+ is_linked = change.issue_id and not change.issue_id.startswith("_remote_")
324
+ if is_linked:
325
+ linked_icon = "[green]✓[/green]"
326
+ linked_count += 1
327
+ else:
328
+ linked_icon = "[dim red]✗[/dim red]"
329
+ orphaned_count += 1
330
+
331
+ table.add_row(
332
+ change.issue_id[:8] if is_linked else "_remote_",
333
+ str(remote_id),
334
+ change.title,
335
+ status,
336
+ linked_icon,
337
+ )
338
+
339
+ console_inst.print(table)
340
+
341
+ # Show status breakdown and link status
342
+ breakdown = _build_remote_status_breakdown(remote_only_issues)
343
+ console_inst.print(
344
+ f"[dim]Total: {len(remote_only_issues)} issues ({breakdown})[/dim]"
345
+ )
346
+ console_inst.print(
347
+ f"[green]Linked to local: {linked_count}[/green] | [dim red]Orphaned (no local match): {orphaned_count}[/dim red]"
348
+ )
349
+
350
+
351
+ def _display_issue_lists(core, analysis_report, local_only, remote_only, console_inst):
352
+ """Display lists of local-only or remote-only issues."""
353
+ if not hasattr(analysis_report, "changes") or not analysis_report.changes:
354
+ console_inst.print("[dim]No issues found[/dim]")
355
+ return
356
+
357
+ # Get all issues by type
358
+ local_only_issues = [c for c in analysis_report.changes if c.is_local_only_change()]
359
+ remote_only_issues = [
360
+ c for c in analysis_report.changes if c.is_remote_only_change()
361
+ ]
362
+
363
+ if local_only and local_only_issues:
364
+ _display_local_only_issues(local_only_issues, console_inst)
365
+
366
+ if remote_only and remote_only_issues:
367
+ _display_remote_only_issues(remote_only_issues, console_inst)
368
+
369
+ if not (local_only or remote_only):
370
+ console_inst.print(
371
+ "[dim]Use --local-only or --remote-only to see issue lists[/dim]"
372
+ )
373
+
374
+
375
+ def _clear_baseline(core, backend, console_inst) -> bool:
376
+ """Handle the `--clear-baseline` flag to clear baseline without syncing."""
377
+ from roadmap.adapters.cli.sync_handlers import clear_baseline
378
+
379
+ return clear_baseline(core, backend, console_inst)
380
+
381
+
382
+ def _show_conflicts(core, backend, verbose, console_inst) -> bool:
383
+ """Handle the `--conflicts` flag to analyze and present conflicts."""
384
+ from roadmap.adapters.cli.sync_handlers import show_conflicts
385
+
386
+ return show_conflicts(core, backend, verbose, console_inst)
387
+
388
+
389
+ def _handle_link_unlink(core, backend, link, unlink, issue_id, console_inst) -> bool:
390
+ """Handle `--link`/`--unlink` operations for manual remote ID management."""
391
+ from roadmap.adapters.cli.sync_handlers import handle_link_unlink
392
+
393
+ return handle_link_unlink(core, backend, link, unlink, issue_id, console_inst)
394
+
395
+
396
+ def _handle_pre_sync_actions(
397
+ core,
398
+ backend,
399
+ base: bool,
400
+ reset_baseline: bool,
401
+ clear_baseline: bool,
402
+ conflicts: bool,
403
+ link: str | None,
404
+ unlink: bool,
405
+ issue_id: str | None,
406
+ verbose: bool,
407
+ console_inst,
408
+ ) -> bool:
409
+ """Handle pre-sync CLI actions that may short-circuit the main sync flow."""
410
+ from roadmap.adapters.cli.sync_handlers import handle_pre_sync_actions
411
+
412
+ return handle_pre_sync_actions(
413
+ core,
414
+ backend,
415
+ base,
416
+ reset_baseline,
417
+ clear_baseline,
418
+ conflicts,
419
+ link,
420
+ unlink,
421
+ issue_id,
422
+ verbose,
423
+ console_inst,
424
+ )
425
+
426
+
427
+ @click.group(invoke_without_command=True)
428
+ @click.option(
429
+ "--dry-run",
430
+ is_flag=True,
431
+ help="Preview changes without applying them",
432
+ )
433
+ @click.option(
434
+ "--verbose",
435
+ is_flag=True,
436
+ help="Show detailed sync information",
437
+ )
438
+ @click.option(
439
+ "--force-local",
440
+ is_flag=True,
441
+ help="Resolve all conflicts by keeping local changes",
442
+ )
443
+ @click.option(
444
+ "--force-remote",
445
+ is_flag=True,
446
+ help="Resolve all conflicts by keeping remote changes",
447
+ )
448
+ @click.option(
449
+ "--backend",
450
+ type=click.Choice(["github", "git"], case_sensitive=False),
451
+ default=None,
452
+ help="Sync backend to use (auto-detected from config if not specified)",
453
+ )
454
+ @click.option(
455
+ "--base",
456
+ is_flag=True,
457
+ help="Show the baseline state from the database (create if missing)",
458
+ )
459
+ @click.option(
460
+ "--reset-baseline",
461
+ is_flag=True,
462
+ help="Force recalculate baseline from current state (WARNING: resets sync history)",
463
+ )
464
+ @click.option(
465
+ "--clear-baseline",
466
+ is_flag=True,
467
+ help="Clear the baseline without syncing (clears sync_base_state table)",
468
+ )
469
+ @click.option(
470
+ "--conflicts",
471
+ is_flag=True,
472
+ help="Show detailed conflict information between local, remote, and baseline",
473
+ )
474
+ @click.option(
475
+ "--push",
476
+ is_flag=True,
477
+ help="Push local changes to remote only (no pull)",
478
+ )
479
+ @click.option(
480
+ "--pull",
481
+ is_flag=True,
482
+ help="Pull remote changes and merge locally only (no push)",
483
+ )
484
+ @click.option(
485
+ "--link",
486
+ type=str,
487
+ metavar="REMOTE_ID",
488
+ help="Link local issue to remote ID (requires --issue-id)",
489
+ )
490
+ @click.option(
491
+ "--unlink",
492
+ is_flag=True,
493
+ help="Unlink local issue from remote (requires --issue-id)",
494
+ )
495
+ @click.option(
496
+ "--issue-id",
497
+ type=str,
498
+ help="Local issue ID for link/unlink operations",
499
+ )
500
+ @click.option(
501
+ "--baseline",
502
+ type=click.Choice(["local", "remote", "interactive"], case_sensitive=False),
503
+ default=None,
504
+ help="Strategy for first sync baseline (local=local is source, remote=remote is source, interactive=choose per-issue)",
505
+ )
506
+ @click.option(
507
+ "--local-only",
508
+ is_flag=True,
509
+ help="Show issues that exist locally but not remotely",
510
+ )
511
+ @click.option(
512
+ "--remote-only",
513
+ is_flag=True,
514
+ help="Show issues that exist remotely but not locally",
515
+ )
516
+ @click.option(
517
+ "--milestone",
518
+ "-m",
519
+ multiple=True,
520
+ help="Filter sync to specific milestones (can specify multiple)",
521
+ )
522
+ @click.option(
523
+ "--milestone-state",
524
+ type=click.Choice(["open", "closed", "all"], case_sensitive=False),
525
+ default="all",
526
+ help="Filter milestones by state (default: all)",
527
+ )
528
+ @click.option(
529
+ "--since",
530
+ type=str,
531
+ help="Only sync milestones updated since date (ISO format: YYYY-MM-DD)",
532
+ )
533
+ @click.option(
534
+ "--until",
535
+ type=str,
536
+ help="Only sync milestones updated until date (ISO format: YYYY-MM-DD)",
537
+ )
538
+ @click.option(
539
+ "--interactive",
540
+ "-i",
541
+ is_flag=True,
542
+ help="Interactively resolve conflicts with visual diffs and prompts",
543
+ )
544
+ @click.option(
545
+ "--resume",
546
+ is_flag=True,
547
+ help="Resume from last checkpoint after a failed sync",
548
+ )
549
+ @click.option(
550
+ "--detect-duplicates/--no-detect-duplicates",
551
+ default=True,
552
+ help="Enable/disable duplicate detection during sync (default: enabled)",
553
+ )
554
+ @click.option(
555
+ "--interactive-duplicates",
556
+ is_flag=True,
557
+ help="Interactively resolve detected duplicate issues with prompts",
558
+ )
559
+ @click.option(
560
+ "--duplicate-title-threshold",
561
+ type=float,
562
+ default=None,
563
+ help="Minimum similarity for title duplicates (0.0-1.0, default from config)",
564
+ )
565
+ @click.option(
566
+ "--duplicate-content-threshold",
567
+ type=float,
568
+ default=None,
569
+ help="Minimum similarity for content duplicates (0.0-1.0, default from config)",
570
+ )
571
+ @click.option(
572
+ "--duplicate-auto-resolve-threshold",
573
+ type=float,
574
+ default=None,
575
+ help="Minimum confidence for auto-resolving duplicates (0.0-1.0, default from config)",
576
+ )
577
+ @click.option(
578
+ "--show-metrics",
579
+ is_flag=True,
580
+ help="Display sync metrics after completion",
581
+ )
582
+ @click.option(
583
+ "--dedup-only",
584
+ is_flag=True,
585
+ help="Run only the deduplication phase (detect and delete duplicates, skip sync)",
586
+ )
587
+ @click.option(
588
+ "--fuzzy",
589
+ is_flag=True,
590
+ help="Enable fuzzy matching for duplicate detection (slower but more thorough)",
591
+ )
592
+ @click.pass_context
593
+ @require_initialized
594
+ def sync(
595
+ ctx: click.Context,
596
+ dry_run: bool,
597
+ verbose: bool,
598
+ force_local: bool,
599
+ force_remote: bool,
600
+ backend: str | None,
601
+ base: bool,
602
+ reset_baseline: bool,
603
+ clear_baseline: bool,
604
+ conflicts: bool,
605
+ push: bool,
606
+ pull: bool,
607
+ link: str | None,
608
+ unlink: bool,
609
+ issue_id: str | None,
610
+ baseline: str | None,
611
+ local_only: bool,
612
+ remote_only: bool,
613
+ milestone: tuple[str, ...],
614
+ milestone_state: str,
615
+ since: str | None,
616
+ until: str | None,
617
+ interactive: bool,
618
+ resume: bool,
619
+ detect_duplicates: bool,
620
+ interactive_duplicates: bool,
621
+ duplicate_title_threshold: float | None,
622
+ duplicate_content_threshold: float | None,
623
+ duplicate_auto_resolve_threshold: float | None,
624
+ show_metrics: bool,
625
+ dedup_only: bool,
626
+ fuzzy: bool,
627
+ ) -> None:
628
+ """Sync roadmap with remote repository.
629
+
630
+ This command provides intelligent two-way sync with automatic conflict
631
+ resolution using three-way merge. It supports multiple backends:
632
+
633
+ - **github**: Sync with GitHub issues API (requires GitHub configuration)
634
+ - **git**: Sync with Git repository (works with any Git hosting)
635
+
636
+ If no backend is specified, the command auto-detects from your config
637
+ (set during `roadmap init`). GitHub owner/repo can be auto-detected from
638
+ your git remote origin URL.
639
+
640
+ **Sync Process:**
641
+ 1. Pull remote changes → merge with local using three-way merge
642
+ 2. Resolve conflicts automatically (when possible)
643
+ 3. Flag unresolvable conflicts for manual review
644
+ 4. Push local changes to remote
645
+ 5. User must manually run: git add, git commit, git push
646
+
647
+ **Conflict Resolution:**
648
+ - Critical fields (status, assignee, milestone) → flagged for review
649
+ - Non-critical fields (labels, description) → automatically merged
650
+ - Metadata (timestamps) → remote wins (remote is authoritative)
651
+
652
+ **Milestone Filtering:**
653
+ - Filter sync to specific milestones by name: --milestone "v1.0"
654
+ - Filter by state: --milestone-state open|closed|all
655
+ - Filter by date range: --since 2024-01-01 --until 2024-12-31
656
+ - Combine filters: --milestone "v1.0" --milestone "v2.0" --milestone-state open
657
+
658
+ **Examples:**
659
+ # Sync with GitHub API
660
+ roadmap sync
661
+
662
+ # Preview changes (dry-run, detailed preview)
663
+ roadmap sync --dry-run --verbose
664
+
665
+ # Sync specific milestones only
666
+ roadmap sync --milestone "v1.0" --milestone "v2.0"
667
+
668
+ # Sync only open milestones
669
+ roadmap sync --milestone-state open
670
+
671
+ # Sync milestones updated since a date
672
+ roadmap sync --since 2024-01-01
673
+
674
+ # Sync with verbose output (shows all pulls and pushes)
675
+ roadmap sync --verbose
676
+
677
+ # First sync with local as baseline (local is source of truth)
678
+ roadmap sync --baseline=local
679
+
680
+ # First sync with remote as baseline (remote is source of truth)
681
+ roadmap sync --baseline=remote
682
+
683
+ # First sync with interactive baseline (choose per-issue)
684
+ roadmap sync --baseline=interactive
685
+
686
+ # Resolve all conflicts locally (keep your changes)
687
+ roadmap sync --force-local
688
+
689
+ # Override backend selection
690
+ roadmap sync --backend=github
691
+ """
692
+ core = ctx.obj["core"]
693
+ console_inst = get_console()
694
+
695
+ # If a subcommand is being invoked, handle it
696
+ if ctx.invoked_subcommand is not None:
697
+ return
698
+
699
+ # Validate mutually exclusive flags
700
+ if push and pull:
701
+ console_inst.print(
702
+ "❌ Cannot use both --push and --pull together. Choose one direction.",
703
+ style="bold red",
704
+ )
705
+ sys.exit(1)
706
+ # Handle early pre-sync actions (baseline display/reset/clear, conflicts, link/unlink)
707
+ if _handle_pre_sync_actions(
708
+ core,
709
+ backend,
710
+ base,
711
+ reset_baseline,
712
+ clear_baseline,
713
+ conflicts,
714
+ link,
715
+ unlink,
716
+ issue_id,
717
+ verbose,
718
+ console_inst,
719
+ ):
720
+ return
721
+
722
+ # Handle dedup-only mode
723
+ if dedup_only:
724
+ _execute_dedup_only(
725
+ core,
726
+ console_inst,
727
+ verbose,
728
+ detect_duplicates,
729
+ duplicate_title_threshold,
730
+ duplicate_content_threshold,
731
+ duplicate_auto_resolve_threshold,
732
+ interactive_duplicates,
733
+ fuzzy,
734
+ dry_run,
735
+ )
736
+ return
737
+
738
+ # Handle checkpoint resume if requested
739
+ if resume:
740
+ if not _handle_resume(console_inst, core):
741
+ return
742
+
743
+ try:
744
+ _execute_sync_workflow(
745
+ core,
746
+ console_inst,
747
+ backend,
748
+ baseline,
749
+ dry_run,
750
+ verbose,
751
+ detect_duplicates,
752
+ duplicate_title_threshold,
753
+ duplicate_content_threshold,
754
+ duplicate_auto_resolve_threshold,
755
+ push,
756
+ pull,
757
+ force_local,
758
+ force_remote,
759
+ local_only,
760
+ remote_only,
761
+ milestone,
762
+ milestone_state,
763
+ since,
764
+ until,
765
+ interactive,
766
+ show_metrics,
767
+ )
768
+ except Exception as exc:
769
+ logger.error(
770
+ "sync_command_failed",
771
+ operation="sync",
772
+ error_type=type(exc).__name__,
773
+ error=str(exc),
774
+ severity="operational",
775
+ error_classification="cli_error",
776
+ suggested_action="check_logs_for_details",
777
+ )
778
+ console_inst.print(
779
+ f"❌ Unexpected error during sync: {exc}",
780
+ style="bold red",
781
+ )
782
+ if verbose:
783
+ raise
784
+ sys.exit(1)
785
+
786
+
787
+ def _handle_resume(console_inst, core) -> bool:
788
+ """Handle checkpoint resume logic.
789
+
790
+ Returns True to continue, False to exit.
791
+ """
792
+ from roadmap.core.services.sync.sync_checkpoint import SyncCheckpointManager
793
+
794
+ checkpoint_manager = SyncCheckpointManager(core)
795
+ can_resume, checkpoint = checkpoint_manager.can_resume()
796
+
797
+ if can_resume and checkpoint:
798
+ console_inst.print("\n[bold cyan]📍 Resuming from checkpoint[/bold cyan]")
799
+ console_inst.print(f"[dim]Checkpoint ID: {checkpoint.checkpoint_id}[/dim]")
800
+ console_inst.print(f"[dim]Phase: {checkpoint.phase}[/dim]")
801
+ console_inst.print(f"[dim]Timestamp: {checkpoint.timestamp}[/dim]")
802
+
803
+ if not click.confirm("\nContinue with resume?", default=True):
804
+ console_inst.print("[yellow]Resume cancelled[/yellow]")
805
+ return False
806
+ return True
807
+ else:
808
+ console_inst.print(
809
+ "[yellow]⚠️ No resumable checkpoint found or checkpoint too old[/yellow]"
810
+ )
811
+ if checkpoint:
812
+ console_inst.print(
813
+ f"[dim]Last checkpoint was in phase '{checkpoint.phase}' at {checkpoint.timestamp}[/dim]"
814
+ )
815
+ return False
816
+
817
+
818
+ def _fetch_remote_for_dedup(core, console_inst, verbose, get_sync_backend):
819
+ """Initialise sync backend and return (backend, remote_issues_dict).
820
+
821
+ Returns (None, {}) if the backend cannot be authenticated or issues cannot be fetched.
822
+ """
823
+ from roadmap.adapters.cli.sync_context import _resolve_backend_and_init
824
+
825
+ sync_backend = None
826
+ remote_issues: dict = {}
827
+ try:
828
+ _, sync_backend = _resolve_backend_and_init(core, None, get_sync_backend)
829
+ if sync_backend:
830
+ auth_result = sync_backend.authenticate()
831
+ if not auth_result.is_ok():
832
+ console_inst.print(
833
+ "[yellow]⚠️ Backend authentication failed, skipping remote deduplication[/yellow]"
834
+ )
835
+ return None, {}
836
+ issues_result = sync_backend.get_issues()
837
+ if issues_result.is_ok():
838
+ remote_issues = issues_result.unwrap() or {}
839
+ console_inst.print(f" 📋 Loaded {len(remote_issues)} remote issues")
840
+ if len(remote_issues) > 1000:
841
+ console_inst.print(
842
+ "[dim]ℹ️ Large dataset detected - remote deletion will be slow (respecting GitHub rate limits)[/dim]"
843
+ )
844
+ else:
845
+ console_inst.print(
846
+ "[yellow]⚠️ Failed to fetch remote issues, skipping remote deduplication[/yellow]"
847
+ )
848
+ except Exception as e:
849
+ console_inst.print(f"[yellow]⚠️ Could not initialize backend: {e}[/yellow]")
850
+ if verbose:
851
+ logger.exception("backend_init_failed")
852
+ return sync_backend, remote_issues
853
+
854
+
855
+ def _run_and_display_dedup(
856
+ dedup_service,
857
+ local_issues: list,
858
+ remote_issues: dict,
859
+ dry_run: bool,
860
+ console_inst,
861
+ verbose: bool,
862
+ ) -> None:
863
+ """Run the deduplication service and display results."""
864
+ import time
865
+
866
+ start_time = time.time()
867
+ try:
868
+ response = dedup_service.execute(
869
+ local_issues=local_issues,
870
+ remote_issues=remote_issues,
871
+ dry_run=dry_run,
872
+ )
873
+ elapsed = time.time() - start_time
874
+ console_inst.print()
875
+ console_inst.print("[bold cyan]✨ Deduplication Results[/bold cyan]")
876
+ console_inst.print(f" 🗑️ Deleted: {response.duplicates_removed} duplicates")
877
+ local_remaining = len(response.local_issues)
878
+ remote_remaining = len(response.remote_issues)
879
+ console_inst.print(
880
+ f" 📊 Remaining: {local_remaining} local + {remote_remaining} remote"
881
+ )
882
+ total_before = len(local_issues) + len(remote_issues)
883
+ if total_before > 0:
884
+ total_after = local_remaining + remote_remaining
885
+ reduction_pct = ((total_before - total_after) / total_before) * 100
886
+ console_inst.print(f" 📉 Reduction: {reduction_pct:.1f}%")
887
+ console_inst.print(f" ⏱️ Time: {elapsed:.2f}s")
888
+ console_inst.print()
889
+ console_inst.print("[green]✅ Deduplication complete[/green]")
890
+ except Exception as e:
891
+ console_inst.print(f"❌ Deduplication failed: {e}", style="bold red")
892
+ if verbose:
893
+ raise
894
+ sys.exit(1)
895
+
896
+
897
+ def _execute_dedup_only(
898
+ core,
899
+ console_inst,
900
+ verbose,
901
+ detect_duplicates,
902
+ duplicate_title_threshold,
903
+ duplicate_content_threshold,
904
+ duplicate_auto_resolve_threshold,
905
+ interactive_duplicates,
906
+ fuzzy: bool = False,
907
+ dry_run: bool = False,
908
+ ) -> None:
909
+ """Execute deduplication phase only (no sync).
910
+
911
+ Fetches local and remote issues, detects duplicates in both,
912
+ and deletes duplicates (both local and remote via GraphQL).
913
+
914
+ Args:
915
+ core: RoadmapCore instance
916
+ console_inst: Console instance for output
917
+ verbose: If True, show detailed output
918
+ detect_duplicates: If True, enable duplicate detection
919
+ duplicate_title_threshold: Title similarity threshold override
920
+ duplicate_content_threshold: Content similarity threshold override
921
+ duplicate_auto_resolve_threshold: Auto-resolve confidence threshold
922
+ interactive_duplicates: If True, prompt for duplicate resolution
923
+ fuzzy: If True, enable fuzzy matching (slower but catches more duplicates)
924
+ dry_run: If True, preview duplicates without actually deleting
925
+ """
926
+ from roadmap.adapters.cli.services.sync_service import get_sync_backend
927
+ from roadmap.application.services.deduplicate_service import DeduplicateService
928
+ from roadmap.core.services.sync.duplicate_detector import DuplicateDetector
929
+
930
+ if not detect_duplicates:
931
+ console_inst.print(
932
+ "[yellow]⚠️ Duplicate detection is disabled (--no-detect-duplicates)[/yellow]"
933
+ )
934
+ return
935
+
936
+ console_inst.print("[bold cyan]🔍 Deduplication Only Mode[/bold cyan]")
937
+ if fuzzy:
938
+ console_inst.print("[yellow]⚠️ Fuzzy matching ENABLED (slower)[/yellow]")
939
+ console_inst.print("[dim]Fetching issues and detecting duplicates...[/dim]\n")
940
+
941
+ try:
942
+ local_issues = core.issue_service.list_all_including_archived() or []
943
+ console_inst.print(f" 📋 Loaded {len(local_issues)} local issues")
944
+ except Exception as e:
945
+ console_inst.print(f"❌ Failed to load local issues: {e}", style="bold red")
946
+ return
947
+
948
+ sync_backend, remote_issues = _fetch_remote_for_dedup(
949
+ core, console_inst, verbose, get_sync_backend
950
+ )
951
+
952
+ detector = DuplicateDetector(
953
+ title_similarity_threshold=duplicate_title_threshold or 0.90,
954
+ content_similarity_threshold=duplicate_content_threshold or 0.85,
955
+ auto_resolve_threshold=duplicate_auto_resolve_threshold or 0.95,
956
+ enable_fuzzy_matching=fuzzy,
957
+ )
958
+ dedup_service = DeduplicateService(
959
+ issue_repo=core.issue_service.repository,
960
+ duplicate_detector=detector,
961
+ backend=sync_backend,
962
+ )
963
+ _run_and_display_dedup(
964
+ dedup_service, local_issues, remote_issues, dry_run, console_inst, verbose
965
+ )
966
+
967
+
968
+ def _execute_sync_workflow(
969
+ core,
970
+ console_inst,
971
+ backend,
972
+ baseline,
973
+ dry_run,
974
+ verbose,
975
+ detect_duplicates,
976
+ duplicate_title_threshold,
977
+ duplicate_content_threshold,
978
+ duplicate_auto_resolve_threshold,
979
+ push,
980
+ pull,
981
+ force_local,
982
+ force_remote,
983
+ local_only,
984
+ remote_only,
985
+ milestone,
986
+ milestone_state,
987
+ since,
988
+ until,
989
+ interactive,
990
+ show_metrics,
991
+ ) -> None:
992
+ """Execute main sync workflow."""
993
+ # Get thresholds from config
994
+ sync_config = core.config_service.get_sync_config()
995
+ title_threshold = (
996
+ duplicate_title_threshold
997
+ if duplicate_title_threshold is not None
998
+ else sync_config["duplicate_title_threshold"]
999
+ )
1000
+ content_threshold = (
1001
+ duplicate_content_threshold
1002
+ if duplicate_content_threshold is not None
1003
+ else sync_config["duplicate_content_threshold"]
1004
+ )
1005
+ auto_resolve_threshold = (
1006
+ duplicate_auto_resolve_threshold
1007
+ if duplicate_auto_resolve_threshold is not None
1008
+ else sync_config["duplicate_auto_resolve_threshold"]
1009
+ )
1010
+
1011
+ # Initialize sync context
1012
+ (
1013
+ backend_type,
1014
+ sync_backend,
1015
+ orchestrator,
1016
+ pre_sync_baseline,
1017
+ pre_sync_issue_count,
1018
+ state_comparator,
1019
+ conflict_resolver,
1020
+ ) = _init_sync_context(
1021
+ core,
1022
+ backend,
1023
+ baseline,
1024
+ dry_run,
1025
+ verbose,
1026
+ console_inst,
1027
+ detect_duplicates,
1028
+ title_threshold,
1029
+ content_threshold,
1030
+ auto_resolve_threshold,
1031
+ )
1032
+
1033
+ # Run analysis phase
1034
+ plan, analysis_report = _run_analysis_phase(
1035
+ orchestrator,
1036
+ push,
1037
+ pull,
1038
+ dry_run,
1039
+ verbose,
1040
+ console_inst,
1041
+ interactive_duplicates=False,
1042
+ )
1043
+
1044
+ # Display local-only or remote-only lists if requested
1045
+ if local_only or remote_only:
1046
+ _display_issue_lists(
1047
+ core, analysis_report, local_only, remote_only, console_inst
1048
+ )
1049
+
1050
+ # Handle dry-run
1051
+ if dry_run:
1052
+ from roadmap.adapters.cli.sync_handlers.dry_run_display import (
1053
+ display_detailed_dry_run_preview,
1054
+ )
1055
+
1056
+ display_detailed_dry_run_preview(
1057
+ analysis_report,
1058
+ milestone_filter=milestone,
1059
+ milestone_state=milestone_state,
1060
+ since=since,
1061
+ until=until,
1062
+ verbose=verbose,
1063
+ )
1064
+ return
1065
+
1066
+ # Check if there's anything to apply
1067
+ if not _present_apply_intent(analysis_report, console_inst):
1068
+ return
1069
+
1070
+ # Confirm and apply changes
1071
+ report = _confirm_and_apply(
1072
+ core,
1073
+ orchestrator,
1074
+ console_inst,
1075
+ analysis_report,
1076
+ force_local,
1077
+ force_remote,
1078
+ push,
1079
+ pull,
1080
+ verbose,
1081
+ interactive,
1082
+ )
1083
+
1084
+ # User cancelled
1085
+ if report is None:
1086
+ return
1087
+
1088
+ # Finalize real sync
1089
+ _finalize_sync(
1090
+ core,
1091
+ console_inst,
1092
+ report,
1093
+ pre_sync_issue_count,
1094
+ verbose,
1095
+ backend_type,
1096
+ )
1097
+ # Display metrics if requested
1098
+ if show_metrics and hasattr(report, "metrics") and report.metrics:
1099
+ from rich.panel import Panel
1100
+
1101
+ from roadmap.presentation.formatters.sync_metrics_formatter import (
1102
+ create_metrics_summary_table,
1103
+ )
1104
+
1105
+ console_inst.print()
1106
+ console_inst.print(
1107
+ Panel(
1108
+ create_metrics_summary_table(report.metrics, verbose),
1109
+ title="[bold cyan]Sync Metrics[/bold cyan]",
1110
+ border_style="cyan",
1111
+ )
1112
+ )
1113
+ console_inst.print()
1114
+
1115
+
1116
+ # Register metrics subcommand
1117
+ sync.add_command(sync_metrics, name="metrics")