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,237 @@
1
+ """Enhanced dry-run display for sync operations."""
2
+
3
+ from rich.table import Table
4
+
5
+ from roadmap.common.console import get_console
6
+
7
+
8
+ def _display_active_filters(
9
+ console,
10
+ milestone_filter: tuple[str, ...] | None,
11
+ milestone_state: str | None,
12
+ since: str | None,
13
+ until: str | None,
14
+ ) -> None:
15
+ """Display active filters for the sync operation."""
16
+ if not (milestone_filter or milestone_state != "all" or since or until):
17
+ return
18
+
19
+ console.print("[bold yellow]Active Filters:[/bold yellow]")
20
+ if milestone_filter:
21
+ console.print(f" • Milestones: {', '.join(milestone_filter)}", style="yellow")
22
+ if milestone_state and milestone_state != "all":
23
+ console.print(f" • State: {milestone_state}", style="yellow")
24
+ if since:
25
+ console.print(f" • Since: {since}", style="yellow")
26
+ if until:
27
+ console.print(f" • Until: {until}", style="yellow")
28
+ console.print()
29
+
30
+
31
+ def _display_summary_statistics(console, push_changes, pull_changes, conflicts) -> None:
32
+ """Display summary statistics of changes."""
33
+ console.print("[bold]Summary:[/bold]")
34
+ console.print(f" • Issues to push: [cyan]{len(push_changes)}[/cyan]")
35
+ console.print(f" • Issues to pull: [green]{len(pull_changes)}[/green]")
36
+ console.print(f" • Conflicts detected: [red]{len(conflicts)}[/red]")
37
+ console.print()
38
+
39
+
40
+ def display_detailed_dry_run_preview(
41
+ analysis_report,
42
+ milestone_filter: tuple[str, ...] | None = None,
43
+ milestone_state: str | None = None,
44
+ since: str | None = None,
45
+ until: str | None = None,
46
+ verbose: bool = False,
47
+ ) -> None:
48
+ """Display detailed preview of sync operations in dry-run mode.
49
+
50
+ Args:
51
+ analysis_report: Sync analysis report with detected changes
52
+ milestone_filter: Tuple of milestone names to filter by
53
+ milestone_state: Filter milestones by state (open/closed/all)
54
+ since: Filter milestones updated since this date
55
+ until: Filter milestones updated until this date
56
+ verbose: Show detailed information
57
+ """
58
+ console = get_console()
59
+
60
+ # Header
61
+ console.print("\n[bold cyan]═══ DRY-RUN PREVIEW ═══[/bold cyan]")
62
+ console.print(
63
+ "[dim]The following changes would be applied if you run sync without --dry-run:[/dim]\n"
64
+ )
65
+
66
+ # Show active filters
67
+ _display_active_filters(console, milestone_filter, milestone_state, since, until)
68
+
69
+ # Get changes by type
70
+ if not hasattr(analysis_report, "changes") or not analysis_report.changes:
71
+ console.print("[dim]No changes detected[/dim]")
72
+ return
73
+
74
+ changes = analysis_report.changes
75
+ push_changes = [c for c in changes if c.is_local_only_change()]
76
+ pull_changes = [c for c in changes if c.is_remote_only_change()]
77
+ conflicts = [c for c in changes if c.has_conflict]
78
+
79
+ # Summary statistics
80
+ _display_summary_statistics(console, push_changes, pull_changes, conflicts)
81
+
82
+ # Issues to push
83
+ if push_changes:
84
+ _display_push_operations(push_changes, console, verbose)
85
+
86
+ # Issues to pull
87
+ if pull_changes:
88
+ _display_pull_operations(pull_changes, console, verbose)
89
+
90
+ # Conflicts
91
+ if conflicts:
92
+ _display_conflicts(conflicts, console, verbose)
93
+
94
+ # Footer
95
+ console.print(
96
+ "\n[bold yellow]⚠️ No changes will be applied in dry-run mode[/bold yellow]"
97
+ )
98
+ console.print(
99
+ "[dim]To apply these changes, run: roadmap sync (without --dry-run)[/dim]"
100
+ )
101
+
102
+
103
+ def _display_push_operations(push_changes, console, verbose: bool) -> None:
104
+ """Display issues that would be pushed to remote."""
105
+ console.print("[bold cyan]📤 PUSH Operations (Local → Remote):[/bold cyan]")
106
+
107
+ table = Table(show_header=True, header_style="bold cyan")
108
+ table.add_column("Issue ID", style="dim", width=12)
109
+ table.add_column("Title", width=40)
110
+ table.add_column("Action", width=15)
111
+
112
+ if verbose:
113
+ table.add_column("Changes", width=30)
114
+
115
+ for change in push_changes[:20]: # Limit to first 20
116
+ action = _determine_push_action(change)
117
+ table.add_row(
118
+ change.issue_id[:10],
119
+ change.title[:37] + "..." if len(change.title) > 40 else change.title,
120
+ action,
121
+ _format_changes(change) if verbose else "",
122
+ )
123
+
124
+ console.print(table)
125
+
126
+ if len(push_changes) > 20:
127
+ console.print(
128
+ f"[dim] ... and {len(push_changes) - 20} more[/dim]",
129
+ style="cyan",
130
+ )
131
+ console.print()
132
+
133
+
134
+ def _display_pull_operations(pull_changes, console, verbose: bool) -> None:
135
+ """Display issues that would be pulled from remote."""
136
+ console.print("[bold green]📥 PULL Operations (Remote → Local):[/bold green]")
137
+
138
+ table = Table(show_header=True, header_style="bold green")
139
+ table.add_column("Issue ID", style="dim", width=12)
140
+ table.add_column("Title", width=40)
141
+ table.add_column("Action", width=15)
142
+
143
+ if verbose:
144
+ table.add_column("Changes", width=30)
145
+
146
+ for change in pull_changes[:20]: # Limit to first 20
147
+ action = _determine_pull_action(change)
148
+ table.add_row(
149
+ change.issue_id[:10],
150
+ change.title[:37] + "..." if len(change.title) > 40 else change.title,
151
+ action,
152
+ _format_changes(change) if verbose else "",
153
+ )
154
+
155
+ console.print(table)
156
+
157
+ if len(pull_changes) > 20:
158
+ console.print(
159
+ f"[dim] ... and {len(pull_changes) - 20} more[/dim]",
160
+ style="green",
161
+ )
162
+ console.print()
163
+
164
+
165
+ def _display_conflicts(conflicts, console, verbose: bool) -> None:
166
+ """Display detected conflicts."""
167
+ console.print("[bold red]⚠️ CONFLICTS Detected:[/bold red]")
168
+ console.print(
169
+ "[dim]These issues have conflicting changes between local and remote:[/dim]\n"
170
+ )
171
+
172
+ table = Table(show_header=True, header_style="bold red")
173
+ table.add_column("Issue ID", style="dim", width=12)
174
+ table.add_column("Title", width=40)
175
+ table.add_column("Conflict Fields", width=25, style="yellow")
176
+
177
+ for conflict in conflicts[:10]: # Limit to first 10
178
+ conflict_fields = _get_conflict_fields(conflict)
179
+ table.add_row(
180
+ conflict.issue_id[:10],
181
+ conflict.title[:37] + "..." if len(conflict.title) > 40 else conflict.title,
182
+ conflict_fields,
183
+ )
184
+
185
+ console.print(table)
186
+
187
+ if len(conflicts) > 10:
188
+ console.print(f"[dim] ... and {len(conflicts) - 10} more[/dim]", style="red")
189
+
190
+ console.print(
191
+ "\n[yellow]💡 Tip: Use --force-local or --force-remote to auto-resolve conflicts[/yellow]"
192
+ )
193
+ console.print()
194
+
195
+
196
+ def _determine_push_action(change) -> str:
197
+ """Determine the push action type from a change object."""
198
+ if change.is_local_only_change():
199
+ return "[cyan]CREATE[/cyan]"
200
+ elif hasattr(change, "field_changes") and change.field_changes:
201
+ return "[yellow]UPDATE[/yellow]"
202
+ return "[green]SYNC[/green]"
203
+
204
+
205
+ def _determine_pull_action(change) -> str:
206
+ """Determine the pull action type from a change object."""
207
+ if change.is_remote_only_change():
208
+ return "[green]CREATE[/green]"
209
+ elif hasattr(change, "field_changes") and change.field_changes:
210
+ return "[yellow]UPDATE[/yellow]"
211
+ return "[cyan]SYNC[/cyan]"
212
+
213
+
214
+ def _format_changes(change) -> str:
215
+ """Format field changes for display."""
216
+ if not hasattr(change, "field_changes") or not change.field_changes:
217
+ return "[dim]new[/dim]"
218
+
219
+ fields = list(change.field_changes.keys())[:3] # First 3 fields
220
+ if len(fields) == 0:
221
+ return "[dim]metadata[/dim]"
222
+
223
+ result = ", ".join(fields)
224
+ if len(change.field_changes) > 3:
225
+ result += f", +{len(change.field_changes) - 3} more"
226
+ return result
227
+
228
+
229
+ def _get_conflict_fields(conflict) -> str:
230
+ """Get the conflicting fields from a conflict object."""
231
+ if hasattr(conflict, "conflicting_fields") and conflict.conflicting_fields:
232
+ fields = list(conflict.conflicting_fields)[:3]
233
+ result = ", ".join(fields)
234
+ if len(conflict.conflicting_fields) > 3:
235
+ result += "..."
236
+ return result
237
+ return "[dim]unknown[/dim]"
@@ -0,0 +1,396 @@
1
+ """Interactive conflict resolution with rich UI for user choices."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.prompt import Confirm, Prompt
10
+ from rich.syntax import Syntax
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+ from structlog import get_logger
14
+
15
+ if TYPE_CHECKING:
16
+ from roadmap.core.domain.issue import Issue
17
+ from roadmap.core.services.sync.sync_conflict_resolver import Conflict
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class InteractiveConflictResolver:
23
+ """Provides interactive UI for resolving sync conflicts."""
24
+
25
+ def __init__(self, console: Console | None = None):
26
+ """Initialize interactive conflict resolver.
27
+
28
+ Args:
29
+ console: Rich console for output (creates new one if None)
30
+ """
31
+ self.console = console or Console()
32
+
33
+ def resolve_interactively(
34
+ self,
35
+ conflicts: list[Conflict],
36
+ issues_by_id: dict[str, Issue],
37
+ ) -> dict[str, dict[str, str]]:
38
+ """Resolve conflicts interactively with user input.
39
+
40
+ Args:
41
+ conflicts: List of conflicts to resolve
42
+ issues_by_id: Dictionary mapping issue IDs to Issue objects
43
+
44
+ Returns:
45
+ Dictionary of resolutions: {issue_id: {field: resolution_strategy}}
46
+ """
47
+ if not conflicts:
48
+ return {}
49
+
50
+ self.console.print(
51
+ "\n[bold cyan]═══ Interactive Conflict Resolution ═══[/bold cyan]\n"
52
+ )
53
+ self.console.print(
54
+ f"Found [yellow]{len(conflicts)}[/yellow] conflicts that need your attention.\n"
55
+ )
56
+
57
+ resolutions = {}
58
+
59
+ for i, conflict in enumerate(conflicts, 1):
60
+ self.console.print(
61
+ f"[bold]Conflict {i}/{len(conflicts)}[/bold]",
62
+ style="cyan",
63
+ )
64
+ self.console.print()
65
+
66
+ issue = issues_by_id.get(conflict.issue_id)
67
+ if not issue:
68
+ logger.warning(
69
+ "conflict_issue_not_found",
70
+ issue_id=conflict.issue_id,
71
+ action="resolve_conflict",
72
+ )
73
+ continue
74
+
75
+ # Display conflict context
76
+ self._display_conflict_context(conflict, issue)
77
+
78
+ # Resolve each conflicting field
79
+ issue_resolutions = {}
80
+ for field in conflict.field_names:
81
+ resolution = self._resolve_field_conflict(conflict, field)
82
+ issue_resolutions[field] = resolution
83
+
84
+ resolutions[conflict.issue_id] = issue_resolutions
85
+
86
+ # Show confirmation
87
+ if i < len(conflicts):
88
+ if not Confirm.ask(
89
+ "\n[dim]Continue to next conflict?[/dim]", default=True
90
+ ):
91
+ self.console.print(
92
+ "[yellow]Remaining conflicts will use default strategy[/yellow]"
93
+ )
94
+ break
95
+ self.console.print("\n" + "─" * 60 + "\n")
96
+
97
+ return resolutions
98
+
99
+ def _display_conflict_context(self, conflict: Conflict, issue: Issue) -> None:
100
+ """Display context information about the conflict."""
101
+ # Issue header
102
+ header_text = Text()
103
+ header_text.append(f"#{issue.id}", style="bold cyan")
104
+ header_text.append(" • ", style="dim")
105
+ header_text.append(issue.title, style="bold white")
106
+
107
+ self.console.print(Panel(header_text, border_style="cyan", padding=(0, 2)))
108
+ self.console.print()
109
+
110
+ # Conflict summary
111
+ summary_table = Table(show_header=False, box=None, padding=(0, 2))
112
+ summary_table.add_column(style="dim", width=20)
113
+ summary_table.add_column()
114
+
115
+ summary_table.add_row("Conflicting Fields:", ", ".join(conflict.field_names))
116
+
117
+ if conflict.local_updated and conflict.remote_updated:
118
+ summary_table.add_row(
119
+ "Last Updated:",
120
+ f"Local: {conflict.local_updated.isoformat()[:10]} | Remote: {conflict.remote_updated.isoformat()[:10]}",
121
+ )
122
+
123
+ self.console.print(summary_table)
124
+ self.console.print()
125
+
126
+ def _resolve_field_conflict(self, conflict: Conflict, field: str) -> str:
127
+ """Resolve a single field conflict interactively.
128
+
129
+ Args:
130
+ conflict: Conflict object
131
+ field: Field name to resolve
132
+
133
+ Returns:
134
+ Resolution strategy chosen by user
135
+ """
136
+ # Get values from the conflict's local and remote issues
137
+ local_value = getattr(conflict.local_issue, field, "[dim]<none>[/dim]")
138
+ remote_value = conflict.remote_issue.get(field, "[dim]<none>[/dim]")
139
+ base_value = None # Base value not currently stored in Conflict
140
+
141
+ self.console.print(f"[bold yellow]Field:[/bold yellow] [cyan]{field}[/cyan]")
142
+ self.console.print()
143
+
144
+ # Show values side-by-side
145
+ comparison_table = Table(show_header=True, box=None, padding=(0, 2))
146
+ comparison_table.add_column("Version", style="bold", width=12)
147
+ comparison_table.add_column("Value", width=50)
148
+
149
+ if base_value is not None:
150
+ comparison_table.add_row(
151
+ "[dim]Baseline[/dim]",
152
+ self._format_value_for_display(base_value, field),
153
+ )
154
+
155
+ comparison_table.add_row(
156
+ "[blue]Local[/blue]",
157
+ self._format_value_for_display(local_value, field),
158
+ )
159
+
160
+ comparison_table.add_row(
161
+ "[magenta]Remote[/magenta]",
162
+ self._format_value_for_display(remote_value, field),
163
+ )
164
+
165
+ self.console.print(comparison_table)
166
+ self.console.print()
167
+
168
+ # Present resolution options
169
+ options = {
170
+ "1": ("LOCAL", "Keep local version (blue)"),
171
+ "2": ("REMOTE", "Keep remote version (magenta)"),
172
+ "3": ("VIEW_DIFF", "View detailed diff"),
173
+ }
174
+
175
+ # Add merge option for mergeable fields
176
+ if self._is_mergeable_field(field, local_value, remote_value):
177
+ options["4"] = ("MERGE", "Attempt automatic merge")
178
+ options["5"] = ("MANUAL", "Edit manually")
179
+ else:
180
+ options["4"] = ("MANUAL", "Edit manually")
181
+
182
+ options["s"] = ("SKIP", "Skip (keep flagged for review)")
183
+
184
+ # Display options
185
+ self.console.print("[bold]Choose resolution:[/bold]")
186
+ for key, (_strategy, description) in options.items():
187
+ self.console.print(f" {key}. {description}")
188
+ self.console.print()
189
+
190
+ # Get user choice
191
+ while True:
192
+ choice = Prompt.ask(
193
+ "Your choice",
194
+ choices=list(options.keys()),
195
+ default="1",
196
+ )
197
+
198
+ strategy = options[choice][0]
199
+
200
+ if strategy == "VIEW_DIFF":
201
+ self._show_detailed_diff(field, local_value, remote_value, base_value)
202
+ continue # Ask again after showing diff
203
+
204
+ if strategy == "MANUAL":
205
+ manual_value = self._get_manual_edit(field, local_value, remote_value)
206
+ if manual_value is not None:
207
+ # Store manual value for later application
208
+ return f"MANUAL:{manual_value}"
209
+ continue # Ask again if manual edit cancelled
210
+
211
+ return strategy
212
+
213
+ def _format_value_for_display(self, value: Any, field: str) -> str:
214
+ """Format a value for display in comparison table.
215
+
216
+ Args:
217
+ value: Value to format
218
+ field: Field name (for context)
219
+
220
+ Returns:
221
+ Formatted string for display
222
+ """
223
+ if value is None or value == "[dim]<none>[/dim]":
224
+ return "[dim]<none>[/dim]"
225
+
226
+ # Truncate long values
227
+ value_str = str(value)
228
+ if len(value_str) > 47:
229
+ return value_str[:44] + "..."
230
+
231
+ return value_str
232
+
233
+ def _is_mergeable_field(
234
+ self, field: str, local_value: Any, remote_value: Any
235
+ ) -> bool:
236
+ """Check if a field can be automatically merged.
237
+
238
+ Args:
239
+ field: Field name
240
+ local_value: Local value
241
+ remote_value: Remote value
242
+
243
+ Returns:
244
+ True if field is mergeable
245
+ """
246
+ # Lists can often be merged
247
+ if isinstance(local_value, list) and isinstance(remote_value, list):
248
+ return True
249
+
250
+ # Tags, labels, etc. can be merged
251
+ mergeable_fields = {"tags", "labels", "assignees", "dependencies"}
252
+ if field in mergeable_fields:
253
+ return True
254
+
255
+ return False
256
+
257
+ def _show_detailed_diff(
258
+ self,
259
+ field: str,
260
+ local_value: Any,
261
+ remote_value: Any,
262
+ base_value: Any,
263
+ ) -> None:
264
+ """Show detailed diff of values.
265
+
266
+ Args:
267
+ field: Field name
268
+ local_value: Local value
269
+ remote_value: Remote value
270
+ base_value: Baseline value
271
+ """
272
+ self.console.print("\n[bold]Detailed Diff:[/bold]\n")
273
+
274
+ # Format values for syntax highlighting
275
+ local_formatted = self._format_value_for_diff(local_value)
276
+ remote_formatted = self._format_value_for_diff(remote_value)
277
+
278
+ # Display side-by-side if possible
279
+ diff_table = Table.grid(padding=(0, 2))
280
+ diff_table.add_column("Local", style="blue", width=40)
281
+ diff_table.add_column("Remote", style="magenta", width=40)
282
+
283
+ if base_value is not None:
284
+ base_formatted = self._format_value_for_diff(base_value)
285
+ self.console.print(
286
+ Panel(
287
+ Syntax(base_formatted, "yaml", theme="monokai", line_numbers=False),
288
+ title="[dim]Baseline[/dim]",
289
+ border_style="dim",
290
+ )
291
+ )
292
+ self.console.print()
293
+
294
+ # Show local and remote side by side
295
+ self.console.print("[bold blue]Local:[/bold blue]")
296
+ self.console.print(
297
+ Syntax(local_formatted, "yaml", theme="monokai", line_numbers=False)
298
+ )
299
+ self.console.print()
300
+
301
+ self.console.print("[bold magenta]Remote:[/bold magenta]")
302
+ self.console.print(
303
+ Syntax(remote_formatted, "yaml", theme="monokai", line_numbers=False)
304
+ )
305
+ self.console.print()
306
+
307
+ def _format_value_for_diff(self, value: Any) -> str:
308
+ """Format value for syntax-highlighted diff display."""
309
+ if isinstance(value, (list, dict)):
310
+ import json
311
+
312
+ return json.dumps(value, indent=2)
313
+ return str(value)
314
+
315
+ def _get_manual_edit(
316
+ self,
317
+ field: str,
318
+ local_value: Any,
319
+ remote_value: Any,
320
+ ) -> str | None:
321
+ """Get manual edit from user.
322
+
323
+ Args:
324
+ field: Field name
325
+ local_value: Local value
326
+ remote_value: Remote value
327
+
328
+ Returns:
329
+ Manually edited value or None if cancelled
330
+ """
331
+ self.console.print("\n[bold]Manual Edit:[/bold]")
332
+ self.console.print(
333
+ "[dim]Enter the value you want to use, or press Ctrl+C to cancel[/dim]\n"
334
+ )
335
+
336
+ self.console.print(f"Current local: {local_value}")
337
+ self.console.print(f"Current remote: {remote_value}")
338
+ self.console.print()
339
+
340
+ try:
341
+ manual_value = Prompt.ask(f"Enter value for '{field}'")
342
+ if manual_value:
343
+ return manual_value
344
+ return None
345
+ except KeyboardInterrupt:
346
+ self.console.print("\n[yellow]Manual edit cancelled[/yellow]")
347
+ return None
348
+
349
+ def show_conflict_summary(
350
+ self,
351
+ conflicts: list[Conflict],
352
+ resolutions: dict[str, dict[str, str]],
353
+ ) -> None:
354
+ """Show summary of conflict resolutions.
355
+
356
+ Args:
357
+ conflicts: Original list of conflicts
358
+ resolutions: Resolutions chosen by user
359
+ """
360
+ self.console.print(
361
+ "\n[bold cyan]═══ Conflict Resolution Summary ═══[/bold cyan]\n"
362
+ )
363
+
364
+ summary_table = Table(show_header=True)
365
+ summary_table.add_column("Issue ID", style="cyan", width=15)
366
+ summary_table.add_column("Field", width=20)
367
+ summary_table.add_column("Resolution", width=20)
368
+
369
+ for conflict in conflicts:
370
+ issue_resolutions = resolutions.get(conflict.issue_id, {})
371
+ for field in conflict.field_names:
372
+ resolution = issue_resolutions.get(field, "SKIP")
373
+ resolution_display = resolution
374
+ if resolution.startswith("MANUAL:"):
375
+ resolution_display = "MANUAL"
376
+
377
+ summary_table.add_row(
378
+ conflict.issue_id[:12],
379
+ field,
380
+ resolution_display,
381
+ )
382
+
383
+ self.console.print(summary_table)
384
+ self.console.print()
385
+
386
+ # Show statistics
387
+ total_fields = sum(len(c.field_names) for c in conflicts)
388
+ resolved_fields = sum(
389
+ len([r for r in resolutions.get(c.issue_id, {}).values() if r != "SKIP"])
390
+ for c in conflicts
391
+ )
392
+
393
+ self.console.print(
394
+ f"[green]Resolved:[/green] {resolved_fields}/{total_fields} fields"
395
+ )
396
+ self.console.print()