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,431 @@
1
+ """Manages persistence of sync state for three-way merge operations.
2
+
3
+ The new SyncState model tracks three snapshots:
4
+ - base_issues: The agreed-upon state from last successful sync
5
+ - local_issues: Current local state (populated before sync)
6
+ - remote_issues: Current remote state (fetched during sync)
7
+
8
+ This manager persists the base_issues snapshot and sync metadata.
9
+ """
10
+
11
+ import json
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ from structlog import get_logger
16
+
17
+ from roadmap.core.domain.issue import Issue
18
+ from roadmap.core.services.sync.sync_state import IssueBaseState, SyncState
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class SyncStateManager:
24
+ """Manages loading and saving sync state from git-based baselines."""
25
+
26
+ def __init__(self, roadmap_dir: Path, db_manager=None):
27
+ """Initialize manager with roadmap directory.
28
+
29
+ Args:
30
+ roadmap_dir: Path to .roadmap directory
31
+ db_manager: Optional DatabaseManager instance for persistent storage
32
+ """
33
+ self.roadmap_dir = roadmap_dir
34
+ self.db_manager = db_manager
35
+ self.state_file = roadmap_dir / "sync_state.json"
36
+
37
+ def load_sync_state(self) -> SyncState | None:
38
+ """Load sync state from git-based baseline metadata.
39
+
40
+ Returns:
41
+ SyncState if baseline metadata exists and is valid, None otherwise
42
+ """
43
+ # Git-based baselines are retrieved via SyncRetrievalOrchestrator
44
+ # This method is kept for compatibility but delegates to git history
45
+ logger.debug(
46
+ "sync_state_load_deprecated",
47
+ reason="Use SyncRetrievalOrchestrator.get_baseline_state() instead",
48
+ )
49
+ return None
50
+
51
+ def save_sync_state(self, state: SyncState) -> bool:
52
+ """Save sync state to git-based baseline metadata.
53
+
54
+ Args:
55
+ state: SyncState to save
56
+
57
+ Returns:
58
+ True if save was successful
59
+ """
60
+ # Git-based baselines are managed via SyncRetrievalOrchestrator
61
+ # This method is deprecated in favor of git history + YAML metadata
62
+ logger.debug(
63
+ "sync_state_save_deprecated",
64
+ reason="Use SyncRetrievalOrchestrator baseline management instead",
65
+ )
66
+ return True
67
+
68
+ def save_sync_state_to_db(self, state: SyncState) -> bool:
69
+ """Save sync state to database (preferred storage method).
70
+
71
+ Persists the base_issues snapshot and last_sync_time metadata.
72
+
73
+ Args:
74
+ state: SyncState to save
75
+
76
+ Returns:
77
+ True if save was successful
78
+ """
79
+ if not self.db_manager:
80
+ logger.debug(
81
+ "sync_state_db_save_skipped",
82
+ reason="no_database_manager",
83
+ )
84
+ return False
85
+
86
+ try:
87
+ import json
88
+
89
+ conn = self.db_manager._get_connection()
90
+
91
+ logger.debug(
92
+ "sync_state_db_save_starting",
93
+ base_issues_count=len(state.base_issues),
94
+ sync_time=state.last_sync_time,
95
+ )
96
+
97
+ # Save metadata - only last_sync_time (no backend in new model)
98
+ if state.last_sync_time:
99
+ conn.execute(
100
+ "INSERT OR REPLACE INTO sync_metadata (key, value, updated_at) VALUES (?, ?, ?)",
101
+ (
102
+ "last_sync",
103
+ state.last_sync_time.isoformat(),
104
+ datetime.now(UTC).isoformat(),
105
+ ),
106
+ )
107
+
108
+ # Save each base issue state
109
+ for issue_id, base_state in state.base_issues.items():
110
+ conn.execute(
111
+ """INSERT OR REPLACE INTO sync_base_state
112
+ (issue_id, status, assignee, title, description, labels, synced_at)
113
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
114
+ (
115
+ issue_id,
116
+ base_state.status,
117
+ base_state.assignee,
118
+ base_state.title,
119
+ base_state.description,
120
+ json.dumps(base_state.labels or []),
121
+ datetime.now(UTC).isoformat(),
122
+ ),
123
+ )
124
+
125
+ conn.commit()
126
+ logger.info(
127
+ "sync_state_saved_to_db",
128
+ base_issues_count=len(state.base_issues),
129
+ )
130
+ return True
131
+
132
+ except Exception as e:
133
+ logger.error(
134
+ "sync_state_db_save_error",
135
+ error=str(e),
136
+ error_type=type(e).__name__,
137
+ severity="system_error",
138
+ )
139
+ return False
140
+
141
+ def load_sync_state_from_db(self) -> SyncState | None:
142
+ """Load sync state from database.
143
+
144
+ Returns:
145
+ SyncState with base_issues if database has valid state, None otherwise
146
+ """
147
+ if not self.db_manager:
148
+ logger.debug(
149
+ "sync_state_db_load_skipped",
150
+ reason="no_database_manager",
151
+ )
152
+ return None
153
+
154
+ try:
155
+ conn = self.db_manager._get_connection()
156
+
157
+ # Get metadata
158
+ metadata = {}
159
+ for row in conn.execute("SELECT key, value FROM sync_metadata"):
160
+ metadata[row[0]] = row[1]
161
+
162
+ if not metadata.get("last_sync"):
163
+ logger.debug(
164
+ "sync_state_db_empty",
165
+ reason="no_last_sync_metadata",
166
+ )
167
+ return None
168
+
169
+ last_sync_time = datetime.fromisoformat(metadata["last_sync"])
170
+
171
+ # Create new SyncState with metadata
172
+ state = SyncState(
173
+ last_sync_time=last_sync_time,
174
+ )
175
+
176
+ # Load base states from database
177
+ try:
178
+ for row in conn.execute(
179
+ "SELECT issue_id, status, assignee, title, description, labels FROM sync_base_state"
180
+ ):
181
+ issue_id, status, assignee, title, description, labels_json = row
182
+ base_state = IssueBaseState(
183
+ id=issue_id,
184
+ status=status,
185
+ title=title or "",
186
+ assignee=assignee,
187
+ description=description or "",
188
+ labels=json.loads(labels_json) if labels_json else [],
189
+ )
190
+ state.base_issues[issue_id] = base_state
191
+ except Exception as e:
192
+ logger.warning(
193
+ "sync_state_db_load_issues_failed",
194
+ error=str(e),
195
+ note="metadata_loaded_but_issues_skipped",
196
+ )
197
+
198
+ logger.info(
199
+ "sync_state_loaded_from_db",
200
+ base_issues_count=len(state.base_issues),
201
+ )
202
+ return state
203
+
204
+ except Exception as e:
205
+ logger.error(
206
+ "sync_state_db_load_error",
207
+ error=str(e),
208
+ error_type=type(e).__name__,
209
+ severity="system_error",
210
+ )
211
+ return None
212
+
213
+ def create_base_state_from_issue(self, issue: Issue) -> IssueBaseState:
214
+ """Create a base state snapshot from an Issue object.
215
+
216
+ Args:
217
+ issue: Issue object to snapshot
218
+
219
+ Returns:
220
+ IssueBaseState representing the current state of the issue
221
+ """
222
+ # Extract status value properly - handle enum types
223
+ status_value = issue.status
224
+ if hasattr(status_value, "value"):
225
+ # It's an enum, use the value
226
+ status_value = status_value.value
227
+ elif status_value is None:
228
+ status_value = "unknown"
229
+ else:
230
+ status_value = str(status_value)
231
+
232
+ return IssueBaseState(
233
+ id=issue.id,
234
+ status=status_value,
235
+ title=issue.title,
236
+ assignee=issue.assignee,
237
+ description=issue.content or "",
238
+ labels=issue.labels or [],
239
+ updated_at=datetime.now(UTC),
240
+ )
241
+
242
+ def save_base_state(self, issue: Issue, remote_version: bool = False) -> bool:
243
+ """Update the base state for a single issue after successful sync.
244
+
245
+ This method is called after successfully syncing an issue to update
246
+ the persisted state so it won't be re-synced next time.
247
+
248
+ Args:
249
+ issue: Issue object that was synced
250
+ remote_version: If True, marks this as the remote version (post-push)
251
+
252
+ Returns:
253
+ True if update was successful
254
+ """
255
+ try:
256
+ logger.debug(
257
+ "base_state_update_starting",
258
+ issue_id=issue.id,
259
+ issue_title=issue.title[:50] if issue.title else "",
260
+ remote_version=remote_version,
261
+ )
262
+
263
+ # Load current sync state or create new one
264
+ state = self.load_sync_state_from_db()
265
+ if state is None:
266
+ logger.info(
267
+ "creating_new_sync_state_for_issue",
268
+ issue_id=issue.id,
269
+ reason="no_existing_state",
270
+ )
271
+ state = SyncState(
272
+ last_sync_time=datetime.now(UTC),
273
+ )
274
+
275
+ # Create new base state for this issue
276
+ base_state = self.create_base_state_from_issue(issue)
277
+ state.base_issues[issue.id] = base_state
278
+ state.last_sync_time = datetime.now(UTC)
279
+
280
+ # Save the updated state to database
281
+ success = self.save_sync_state_to_db(state)
282
+ if success:
283
+ logger.info(
284
+ "base_state_updated_successfully",
285
+ issue_id=issue.id,
286
+ remote_version=remote_version,
287
+ status=base_state.status,
288
+ )
289
+ else:
290
+ logger.warning(
291
+ "base_state_update_save_failed",
292
+ issue_id=issue.id,
293
+ severity="operational",
294
+ )
295
+ return success
296
+ except AttributeError as e:
297
+ logger.error(
298
+ "base_state_update_attribute_error",
299
+ issue_id=issue.id if hasattr(issue, "id") else "unknown",
300
+ error=str(e),
301
+ error_type="AttributeError",
302
+ severity="system_error",
303
+ )
304
+ return False
305
+ except Exception as e:
306
+ logger.error(
307
+ "base_state_update_failed",
308
+ issue_id=issue.id if hasattr(issue, "id") else "unknown",
309
+ error=str(e),
310
+ error_type=type(e).__name__,
311
+ severity="system_error",
312
+ )
313
+ return False
314
+
315
+ def create_sync_state_from_issues(
316
+ self,
317
+ issues: list[Issue],
318
+ backend: str = "github",
319
+ ) -> SyncState:
320
+ """Create a complete sync state from a list of issues.
321
+
322
+ Args:
323
+ issues: List of Issue objects
324
+ backend: Backend type (used for logging only, not stored in new model)
325
+
326
+ Returns:
327
+ SyncState with base states for all issues
328
+ """
329
+ try:
330
+ logger.debug(
331
+ "creating_sync_state_from_issues",
332
+ issues_count=len(issues),
333
+ backend=backend,
334
+ )
335
+
336
+ state = SyncState(
337
+ last_sync_time=datetime.now(UTC),
338
+ )
339
+
340
+ for issue in issues:
341
+ try:
342
+ base_state = self.create_base_state_from_issue(issue)
343
+ state.base_issues[issue.id] = base_state
344
+ except Exception as e:
345
+ logger.warning(
346
+ "skipping_issue_in_state_creation",
347
+ issue_id=issue.id if hasattr(issue, "id") else "unknown",
348
+ error=str(e),
349
+ severity="operational",
350
+ )
351
+ continue
352
+
353
+ logger.info(
354
+ "sync_state_created_successfully",
355
+ issues_count=len(issues),
356
+ backend=backend,
357
+ issues_in_state=len(state.base_issues),
358
+ )
359
+ return state
360
+ except Exception as e:
361
+ logger.error(
362
+ "sync_state_creation_failed",
363
+ issues_count=len(issues) if isinstance(issues, list) else "unknown",
364
+ backend=backend,
365
+ error=str(e),
366
+ error_type=type(e).__name__,
367
+ severity="system_error",
368
+ )
369
+ # Return empty state as fallback
370
+ return SyncState(
371
+ last_sync_time=datetime.now(UTC),
372
+ )
373
+
374
+ def migrate_json_to_db(self) -> bool:
375
+ """Migrate sync state from JSON file to database (legacy operation).
376
+
377
+ One-time operation for existing installations to move from file-based
378
+ to database-based sync state storage. With the new model, this operation
379
+ should not be needed as state is created fresh from issues.
380
+
381
+ Returns:
382
+ True if migration was successful or not needed
383
+ """
384
+ if not self.db_manager:
385
+ logger.debug(
386
+ "sync_state_migration_skipped",
387
+ reason="no_database_manager",
388
+ )
389
+ return False
390
+
391
+ # Check if JSON file exists
392
+ if not self.state_file.exists():
393
+ logger.debug(
394
+ "sync_state_migration_not_needed",
395
+ reason="no_legacy_json_file",
396
+ )
397
+ return True
398
+
399
+ try:
400
+ logger.info("sync_state_migration_starting", source="json_file")
401
+ logger.warning(
402
+ "sync_state_migration_deprecated",
403
+ note="new_model_creates_fresh_state_from_issues",
404
+ )
405
+
406
+ # Archive the JSON file
407
+ try:
408
+ archive_path = self.state_file.with_suffix(".json.backup")
409
+ self.state_file.rename(archive_path)
410
+ logger.info(
411
+ "sync_state_legacy_json_archived",
412
+ archived_to=str(archive_path),
413
+ )
414
+ return True
415
+ except OSError as e:
416
+ logger.warning(
417
+ "sync_state_migration_archive_failed",
418
+ error=str(e),
419
+ note="json_file_remains",
420
+ severity="operational",
421
+ )
422
+ return False
423
+
424
+ except Exception as e:
425
+ logger.error(
426
+ "sync_state_migration_error",
427
+ error=str(e),
428
+ error_type=type(e).__name__,
429
+ severity="system_error",
430
+ )
431
+ return False
@@ -0,0 +1,98 @@
1
+ """Helpers for normalizing remote sync state and extracting timestamps.
2
+
3
+ This module contains pure helpers extracted from SyncStateComparator to
4
+ encapsulate timestamp parsing and remote object normalization so they can be
5
+ tested and reused independently.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ import structlog
14
+
15
+ logger = structlog.get_logger()
16
+
17
+
18
+ def extract_timestamp(
19
+ data: dict[str, Any] | object,
20
+ timestamp_field: str = "updated_at",
21
+ logger: Any | None = None,
22
+ ) -> datetime | None:
23
+ """Extract and parse a timestamp from remote issue data.
24
+
25
+ Mirrors the behavior previously implemented on SyncStateComparator.
26
+ Accepts an optional `logger` to preserve debug logging when available.
27
+ """
28
+ try:
29
+ if isinstance(data, dict):
30
+ ts = data.get(timestamp_field)
31
+ else:
32
+ ts = getattr(data, timestamp_field, None)
33
+
34
+ if ts is None:
35
+ return None
36
+
37
+ if isinstance(ts, datetime):
38
+ return ts
39
+
40
+ if isinstance(ts, str):
41
+ if ts.endswith("Z"):
42
+ ts = ts[:-1] + "+00:00"
43
+ return datetime.fromisoformat(ts)
44
+
45
+ return None
46
+
47
+ except Exception as e: # pragma: no cover - defensive logging
48
+ if logger is not None:
49
+ try:
50
+ logger.debug(
51
+ "timestamp_extraction_error",
52
+ field=timestamp_field,
53
+ error=str(e),
54
+ )
55
+ except Exception as logging_error:
56
+ logger.error(
57
+ "logger_failed",
58
+ operation="log_timestamp_extraction_error",
59
+ error=str(logging_error),
60
+ )
61
+ return None
62
+
63
+
64
+ def normalize_remote_state(
65
+ remote: Any, logger: Any | None = None
66
+ ) -> dict[str, Any] | None:
67
+ """Normalize remote representations into a plain dict for comparison.
68
+
69
+ Accepts either a dict-like remote or an object. Returns a dict or None.
70
+ """
71
+ try:
72
+ if isinstance(remote, dict) or remote is None:
73
+ return remote
74
+
75
+ return {
76
+ "id": getattr(remote, "id", None),
77
+ "backend_id": getattr(remote, "backend_id", None),
78
+ "title": getattr(remote, "title", None),
79
+ "status": getattr(remote, "status", None),
80
+ "assignee": getattr(remote, "assignee", None),
81
+ "milestone": getattr(remote, "milestone", None),
82
+ "description": getattr(remote, "headline", None)
83
+ or getattr(remote, "description", None),
84
+ "labels": list(getattr(remote, "labels", []) or []),
85
+ "updated_at": getattr(remote, "updated_at", None)
86
+ or getattr(remote, "updated", None),
87
+ }
88
+ except Exception as e: # pragma: no cover - defensive logging
89
+ if logger is not None:
90
+ try:
91
+ logger.debug("normalize_remote_state_error", error=str(e))
92
+ except Exception as logging_error:
93
+ logger.error(
94
+ "logger_failed",
95
+ operation="log_normalize_remote_state_error",
96
+ error=str(logging_error),
97
+ )
98
+ return None
@@ -0,0 +1,78 @@
1
+ """Three-way merge helpers for building IssueChange objects.
2
+
3
+ This module provides a single helper that constructs an `IssueChange` using
4
+ callable dependencies passed in from `SyncStateComparator`. Keeping the
5
+ implementation here makes it easier to unit-test the three-way logic in
6
+ isolation while the comparator remains a thin facade.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ from roadmap.core.services.sync.sync_report import IssueChange
15
+ from roadmap.core.services.sync.sync_state import IssueBaseState
16
+
17
+
18
+ def build_issue_change(
19
+ issue_id: str,
20
+ local: Any | None,
21
+ remote: Any | None,
22
+ baseline: IssueBaseState | None,
23
+ *,
24
+ resolve_title: Callable[[str, Any, Any, Any], str],
25
+ normalize_remote_state: Callable[[Any], dict | None],
26
+ compute_changes: Callable[[Any, Any], dict],
27
+ compute_changes_remote: Callable[[Any, Any], dict],
28
+ handle_first_sync_semantics: Callable[[IssueChange, Any, Any, Any], None],
29
+ detect_and_flag_conflicts: Callable[[IssueChange, Any, Any], None],
30
+ extract_timestamp: Callable[[Any, str], Any],
31
+ fields_to_sync: list[str],
32
+ logger: Any | None = None,
33
+ ) -> IssueChange:
34
+ """Construct an IssueChange for a single issue using three-way context.
35
+
36
+ Most of the behavior is the same as the old `_build_issue_change` on
37
+ `SyncStateComparator` but extracted to allow focused testing. The helper
38
+ depends on several callables passed in from the comparator to avoid
39
+ duplicating logic or breaking encapsulation.
40
+ """
41
+ title = resolve_title(issue_id, local, remote, baseline)
42
+ change = IssueChange(issue_id=issue_id, title=title)
43
+ change.baseline_state = baseline
44
+ change.local_state = local
45
+
46
+ remote_state = normalize_remote_state(remote)
47
+ change.remote_state = remote_state
48
+
49
+ change.local_changes = compute_changes(baseline, local) if local is not None else {}
50
+ change.remote_changes = (
51
+ compute_changes_remote(baseline, remote_state)
52
+ if remote_state is not None
53
+ else {}
54
+ )
55
+
56
+ if baseline is None:
57
+ handle_first_sync_semantics(change, local, remote_state, baseline)
58
+ else:
59
+ detect_and_flag_conflicts(change, local, remote_state)
60
+
61
+ if change.local_changes and change.remote_changes:
62
+ change.conflict_type = "both_changed"
63
+ change.has_conflict = True
64
+ elif change.local_changes:
65
+ change.conflict_type = "local_only"
66
+ elif change.remote_changes:
67
+ change.conflict_type = "remote_only"
68
+ else:
69
+ change.conflict_type = "no_change"
70
+
71
+ last_sync = None
72
+ if baseline is not None and getattr(baseline, "updated_at", None):
73
+ last_sync = baseline.updated_at
74
+ elif remote is not None:
75
+ last_sync = extract_timestamp(remote, "updated_at")
76
+ change.last_sync_time = last_sync
77
+
78
+ return change