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,263 @@
1
+ """Service for persisting issues to local storage (YAML files)."""
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ from structlog import get_logger
7
+
8
+ from roadmap.core.domain.issue import Issue
9
+ from roadmap.core.models.sync_models import SyncIssue
10
+
11
+ if TYPE_CHECKING:
12
+ from roadmap.infrastructure.coordination.core import RoadmapCore # noqa: F401
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class IssuePersistenceService:
18
+ """Handles persistence of issues to local storage (YAML files).
19
+
20
+ Centralizes all operations for:
21
+ - Updating the github_issue field on Issue objects
22
+ - Managing remote_ids dictionaries
23
+ - Saving issues to YAML repository
24
+
25
+ This eliminates the duplication of save patterns in pull_issue() and push_issue().
26
+ """
27
+
28
+ @staticmethod
29
+ def update_issue_with_remote_id(
30
+ issue: Issue,
31
+ backend_name: str,
32
+ backend_id: int | str,
33
+ ) -> None:
34
+ """Update an issue's remote_ids dictionary with a new backend mapping.
35
+
36
+ Args:
37
+ issue: Issue to update
38
+ backend_name: Backend name (e.g., "github")
39
+ backend_id: ID from remote backend
40
+ """
41
+ if issue.remote_ids is None:
42
+ issue.remote_ids = {}
43
+
44
+ issue.remote_ids[backend_name] = str(backend_id)
45
+
46
+ logger.debug(
47
+ "updated_remote_ids",
48
+ issue_id=issue.id,
49
+ backend_name=backend_name,
50
+ backend_id=backend_id,
51
+ )
52
+
53
+ @staticmethod
54
+ def update_github_issue_number(issue: Issue, github_number: int) -> None:
55
+ """Update the github_issue field on an Issue object.
56
+
57
+ This field is used for quick GitHub lookups without querying remote_ids.
58
+
59
+ Args:
60
+ issue: Issue to update
61
+ github_number: GitHub issue number to set
62
+ """
63
+ issue.github_issue = github_number
64
+
65
+ logger.debug(
66
+ "updated_github_issue_number",
67
+ issue_id=issue.id,
68
+ github_issue=github_number,
69
+ )
70
+
71
+ @staticmethod
72
+ def save_issue(issue: Issue, core: "RoadmapCore") -> bool:
73
+ """Save an issue to local YAML repository.
74
+
75
+ Args:
76
+ issue: Issue to save
77
+ core: RoadmapCore for accessing repositories
78
+
79
+ Returns:
80
+ True if save succeeded, False otherwise
81
+ """
82
+ try:
83
+ if not issue or not issue.id:
84
+ logger.warning(
85
+ "cannot_save_invalid_issue",
86
+ issue_id=getattr(issue, "id", None),
87
+ )
88
+ return False
89
+
90
+ repo = core.issue_service.repository
91
+ if not repo:
92
+ logger.error(
93
+ "issue_repository_not_available",
94
+ issue_id=issue.id,
95
+ )
96
+ return False
97
+
98
+ attempts = 0
99
+ max_attempts = 3
100
+ while True:
101
+ try:
102
+ repo.save(issue)
103
+ break
104
+ except OSError as e:
105
+ attempts += 1
106
+ if attempts >= max_attempts:
107
+ raise
108
+ backoff = 0.1 * (2 ** (attempts - 1))
109
+ logger.warning(
110
+ "issue_persistence_retrying",
111
+ issue_id=issue.id,
112
+ attempt=attempts,
113
+ delay_seconds=backoff,
114
+ error=str(e),
115
+ )
116
+ time.sleep(backoff)
117
+
118
+ logger.info(
119
+ "issue_saved_to_yaml",
120
+ issue_id=issue.id,
121
+ title=issue.title,
122
+ )
123
+ return True
124
+
125
+ except AttributeError as e:
126
+ logger.error(
127
+ "issue_persistence_attribute_error",
128
+ issue_id=getattr(issue, "id", "unknown"),
129
+ error=str(e),
130
+ error_type="AttributeError",
131
+ )
132
+ return False
133
+ except Exception as e:
134
+ logger.error(
135
+ "issue_persistence_save_failed",
136
+ issue_id=getattr(issue, "id", "unknown"),
137
+ error=str(e),
138
+ error_type=type(e).__name__,
139
+ exc_info=True,
140
+ )
141
+ return False
142
+
143
+ @staticmethod
144
+ def apply_sync_issue_to_local(
145
+ local_issue: Issue,
146
+ sync_issue: SyncIssue,
147
+ ) -> None:
148
+ """Apply data from a SyncIssue to update a local Issue object.
149
+
150
+ Used when pulling changes from remote backend to update local state.
151
+ Preserves local ID while updating fields from remote.
152
+
153
+ Args:
154
+ local_issue: Local issue to update (ID preserved)
155
+ sync_issue: Remote sync_issue with new data
156
+ """
157
+ # Update core fields from sync_issue
158
+ local_issue.title = sync_issue.title or local_issue.title
159
+ local_issue.assignee = sync_issue.assignee or local_issue.assignee
160
+ local_issue.milestone = sync_issue.milestone or local_issue.milestone
161
+ local_issue.labels = sync_issue.labels or local_issue.labels
162
+
163
+ # Update timestamps
164
+ if sync_issue.updated_at:
165
+ local_issue.updated = sync_issue.updated_at
166
+
167
+ # Update remote_ids from sync_issue (important for tracking)
168
+ if sync_issue.remote_ids:
169
+ if local_issue.remote_ids is None:
170
+ local_issue.remote_ids = {}
171
+ local_issue.remote_ids.update(sync_issue.remote_ids)
172
+
173
+ # Merge metadata while preserving local metadata
174
+ if sync_issue.metadata:
175
+ if local_issue.github_sync_metadata is None:
176
+ local_issue.github_sync_metadata = {}
177
+ local_issue.github_sync_metadata.update(sync_issue.metadata)
178
+
179
+ logger.debug(
180
+ "applied_sync_issue_to_local",
181
+ issue_id=local_issue.id,
182
+ title=local_issue.title,
183
+ from_backend=sync_issue.backend_name,
184
+ )
185
+
186
+ @staticmethod
187
+ def get_issue_from_repo(
188
+ issue_id: str,
189
+ core: "RoadmapCore",
190
+ ) -> Issue | None:
191
+ """Retrieve an issue from the local repository.
192
+
193
+ Args:
194
+ issue_id: Issue ID to retrieve
195
+ core: RoadmapCore for accessing repositories
196
+
197
+ Returns:
198
+ Issue if found, None otherwise
199
+ """
200
+ try:
201
+ repo = core.issue_service.repository
202
+ issue = repo.get(issue_id)
203
+
204
+ if issue:
205
+ logger.debug(
206
+ "retrieved_issue_from_repo",
207
+ issue_id=issue_id,
208
+ title=issue.get("title")
209
+ if isinstance(issue, dict)
210
+ else issue.title,
211
+ )
212
+ else:
213
+ logger.debug("issue_not_found_in_repo", issue_id=issue_id)
214
+
215
+ return issue
216
+
217
+ except Exception as e:
218
+ logger.error(
219
+ "failed_to_retrieve_issue_from_repo",
220
+ issue_id=issue_id,
221
+ error=str(e),
222
+ )
223
+ return None
224
+
225
+ @staticmethod
226
+ def is_github_linked(issue: Issue) -> bool:
227
+ """Check if an issue is already linked to GitHub.
228
+
229
+ Args:
230
+ issue: Issue to check
231
+
232
+ Returns:
233
+ True if issue has github_issue set or github in remote_ids
234
+ """
235
+ if hasattr(issue, "github_issue") and issue.github_issue:
236
+ return True
237
+
238
+ if issue.remote_ids and "github" in issue.remote_ids:
239
+ return True
240
+
241
+ return False
242
+
243
+ @staticmethod
244
+ def get_github_issue_number(issue: Issue) -> int | str | None:
245
+ """Extract the GitHub issue number from an Issue.
246
+
247
+ Checks both github_issue field and remote_ids dict.
248
+
249
+ Args:
250
+ issue: Issue to extract from
251
+
252
+ Returns:
253
+ GitHub issue number if found, None otherwise
254
+ """
255
+ # First check direct field (property)
256
+ if issue.github_issue is not None:
257
+ return issue.github_issue
258
+
259
+ # Then check remote_ids
260
+ if issue.remote_ids and "github" in issue.remote_ids:
261
+ return issue.remote_ids["github"]
262
+
263
+ return None
@@ -0,0 +1,243 @@
1
+ """Service for converting between SyncIssue and local Issue domain objects."""
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import Any
5
+
6
+ from structlog import get_logger
7
+
8
+ from roadmap.common.constants import Priority, Status
9
+ from roadmap.core.domain.issue import Issue
10
+ from roadmap.core.models.sync_models import SyncIssue
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class IssueStateService:
16
+ """Handles conversion between SyncIssue (remote) and Issue (local) domain objects.
17
+
18
+ Centralizes field mapping, normalization, and type conversions to eliminate
19
+ duplication between pull_issue() and push_issue() methods.
20
+ """
21
+
22
+ @staticmethod
23
+ def sync_issue_to_issue(issue_id: str, sync_issue: SyncIssue) -> Issue:
24
+ """Convert a SyncIssue (from backend) to a local Issue domain object.
25
+
26
+ Args:
27
+ issue_id: Local issue ID to assign
28
+ sync_issue: Remote SyncIssue with backend data
29
+
30
+ Returns:
31
+ Issue object ready for local storage/update
32
+
33
+ Raises:
34
+ ValueError: If required fields are missing
35
+ """
36
+ IssueStateService._validate_sync_issue_input(issue_id, sync_issue)
37
+ IssueStateService._warn_if_missing_sync_title(issue_id, sync_issue)
38
+
39
+ try:
40
+ created_at, updated_at = IssueStateService._resolve_sync_timestamps(
41
+ sync_issue
42
+ )
43
+ status = IssueStateService.normalize_status(sync_issue.status)
44
+ content = IssueStateService._resolve_sync_content(sync_issue)
45
+
46
+ issue = Issue(
47
+ id=issue_id,
48
+ title=sync_issue.title or "Untitled",
49
+ content=content,
50
+ status=status,
51
+ priority=Priority.MEDIUM, # Default priority (not in SyncIssue)
52
+ labels=sync_issue.labels or [],
53
+ assignee=sync_issue.assignee,
54
+ milestone=sync_issue.milestone,
55
+ created=created_at,
56
+ updated=updated_at,
57
+ )
58
+
59
+ if sync_issue.remote_ids:
60
+ issue.remote_ids = sync_issue.remote_ids.copy()
61
+
62
+ issue.github_sync_metadata = IssueStateService._build_sync_metadata(
63
+ sync_issue
64
+ )
65
+
66
+ logger.info(
67
+ "converted_sync_issue_to_issue",
68
+ issue_id=issue_id,
69
+ title=issue.title,
70
+ status=status.value,
71
+ from_backend=sync_issue.backend_name,
72
+ )
73
+
74
+ return issue
75
+ except ValueError as e:
76
+ logger.error(
77
+ "sync_issue_conversion_validation_error",
78
+ issue_id=issue_id,
79
+ error=str(e),
80
+ )
81
+ raise
82
+ except Exception as e:
83
+ logger.error(
84
+ "sync_issue_conversion_failed",
85
+ issue_id=issue_id,
86
+ backend_name=sync_issue.backend_name,
87
+ error=str(e),
88
+ error_type=type(e).__name__,
89
+ exc_info=True,
90
+ )
91
+ raise
92
+
93
+ @staticmethod
94
+ def _validate_sync_issue_input(issue_id: str, sync_issue: SyncIssue | None) -> None:
95
+ """Validate required inputs for sync issue conversion."""
96
+ if not issue_id or not issue_id.strip():
97
+ logger.error("cannot_convert_sync_issue_empty_issue_id")
98
+ raise ValueError("issue_id cannot be empty")
99
+ if not sync_issue:
100
+ logger.error("cannot_convert_none_sync_issue")
101
+ raise ValueError("sync_issue cannot be None")
102
+
103
+ @staticmethod
104
+ def _warn_if_missing_sync_title(issue_id: str, sync_issue: SyncIssue) -> None:
105
+ """Emit warning when remote issue title is missing."""
106
+ if sync_issue.title and sync_issue.title.strip():
107
+ return
108
+ logger.warning(
109
+ "sync_issue_missing_title",
110
+ issue_id=issue_id,
111
+ backend_name=sync_issue.backend_name,
112
+ backend_id=sync_issue.backend_id,
113
+ )
114
+
115
+ @staticmethod
116
+ def _resolve_sync_timestamps(sync_issue: SyncIssue) -> tuple[datetime, datetime]:
117
+ """Resolve created/updated timestamps for a sync issue."""
118
+ created_at = sync_issue.created_at or datetime.now(UTC)
119
+ updated_at = sync_issue.updated_at or datetime.now(UTC)
120
+ return created_at, updated_at
121
+
122
+ @staticmethod
123
+ def _resolve_sync_content(sync_issue: SyncIssue) -> str:
124
+ """Resolve normalized content from sync issue headline."""
125
+ if sync_issue.headline and sync_issue.headline.strip():
126
+ return sync_issue.headline
127
+ return ""
128
+
129
+ @staticmethod
130
+ def _build_sync_metadata(sync_issue: SyncIssue) -> dict[str, Any]:
131
+ """Build metadata payload persisted on local issue."""
132
+ return {
133
+ "backend_name": sync_issue.backend_name,
134
+ "backend_id": sync_issue.backend_id,
135
+ **(sync_issue.metadata or {}),
136
+ }
137
+
138
+ @staticmethod
139
+ def issue_to_push_payload(issue: Issue) -> dict[str, Any]:
140
+ """Convert a local Issue to a push payload for remote backend.
141
+
142
+ Extracts fields that are relevant for pushing to GitHub/remote.
143
+
144
+ Args:
145
+ issue: Local Issue to convert
146
+
147
+ Returns:
148
+ Dictionary with fields safe for remote API
149
+
150
+ Raises:
151
+ ValueError: If issue is invalid or missing required fields
152
+ """
153
+ if not issue:
154
+ logger.error("cannot_build_push_payload_none_issue")
155
+ raise ValueError("issue cannot be None")
156
+
157
+ if not issue.title or not issue.title.strip():
158
+ logger.error(
159
+ "cannot_build_push_payload_empty_title",
160
+ issue_id=issue.id,
161
+ )
162
+ raise ValueError("issue.title cannot be empty")
163
+
164
+ try:
165
+ # Normalize status to backend format
166
+ state = "open"
167
+ if issue.status == Status.CLOSED:
168
+ state = "closed"
169
+
170
+ payload: dict[str, Any] = {
171
+ "title": issue.title,
172
+ "body": issue.content or "",
173
+ "state": state,
174
+ }
175
+
176
+ # Handle labels - may be comma-separated strings
177
+ if issue.labels:
178
+ labels_list: list[str] = []
179
+ for label in issue.labels:
180
+ if isinstance(label, str):
181
+ labels_list.extend(
182
+ [
183
+ label_item.strip()
184
+ for label_item in label.split(",")
185
+ if label_item.strip()
186
+ ]
187
+ )
188
+ else:
189
+ labels_list.append(str(label).strip())
190
+
191
+ if labels_list:
192
+ payload["labels"] = labels_list
193
+
194
+ logger.info(
195
+ "issue_converted_to_push_payload",
196
+ issue_id=issue.id,
197
+ title=issue.title,
198
+ state=state,
199
+ has_labels=bool(payload.get("labels")),
200
+ )
201
+
202
+ return payload
203
+ except ValueError as e:
204
+ logger.error(
205
+ "push_payload_validation_error",
206
+ issue_id=issue.id,
207
+ error=str(e),
208
+ )
209
+ raise
210
+ except Exception as e:
211
+ logger.error(
212
+ "push_payload_creation_failed",
213
+ issue_id=issue.id,
214
+ title=issue.title,
215
+ error=str(e),
216
+ error_type=type(e).__name__,
217
+ exc_info=True,
218
+ )
219
+ raise
220
+
221
+ @staticmethod
222
+ def normalize_status(status_value: str | None) -> Status:
223
+ """Normalize a status string from any backend to local Status enum.
224
+
225
+ Args:
226
+ status_value: Status string from backend
227
+
228
+ Returns:
229
+ Normalized Status enum value
230
+ """
231
+ if not status_value:
232
+ return Status.TODO
233
+
234
+ status_lower = status_value.lower()
235
+
236
+ if status_lower in ("closed", "done", "completed", "resolved"):
237
+ return Status.CLOSED
238
+ elif status_lower in ("in_progress", "in progress", "active", "started"):
239
+ return Status.IN_PROGRESS
240
+ elif status_lower in ("on_hold", "on hold", "blocked", "paused"):
241
+ return Status.BLOCKED
242
+ else:
243
+ return Status.TODO
@@ -0,0 +1,129 @@
1
+ """Filters local changes from baseline for sync operations.
2
+
3
+ Extracted from SyncMergeEngine to separate change filtering logic.
4
+ """
5
+
6
+ from structlog import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class LocalChangeFilter:
12
+ """Filters and identifies local changes since baseline."""
13
+
14
+ @staticmethod
15
+ def filter_unchanged_from_base(
16
+ issues: list, current_local: dict, base_state_issues: dict
17
+ ) -> list:
18
+ """Filter out unchanged issues, keeping only changed or new ones.
19
+
20
+ Args:
21
+ issues: List of issues to filter
22
+ current_local: Dict of current local issues keyed by ID
23
+ base_state_issues: Dict of baseline state issues keyed by ID
24
+
25
+ Returns:
26
+ Filtered list containing only changed or new issues
27
+ """
28
+ if not base_state_issues:
29
+ logger.debug(
30
+ "filter_no_base_state",
31
+ input_count=len(issues),
32
+ reason="first_sync_no_previous_state",
33
+ )
34
+ return issues
35
+
36
+ filtered = []
37
+ skipped_count = 0
38
+ new_count = 0
39
+ changed_count = 0
40
+
41
+ for issue in issues:
42
+ issue_id = issue.id if hasattr(issue, "id") else issue
43
+ if issue_id not in current_local:
44
+ logger.debug(
45
+ "filter_issue_not_in_local",
46
+ issue_id=issue_id,
47
+ reason="might_be_stale",
48
+ )
49
+ continue
50
+ if issue_id not in base_state_issues:
51
+ logger.debug(
52
+ "filter_new_local_issue",
53
+ issue_id=issue_id,
54
+ reason="not_in_previous_sync",
55
+ )
56
+ new_count += 1
57
+ filtered.append(issue)
58
+ continue
59
+
60
+ local_issue = current_local[issue_id]
61
+ base_state = base_state_issues[issue_id]
62
+
63
+ fields_to_check = {
64
+ "status": lambda obj: (
65
+ obj.status.value
66
+ if hasattr(obj.status, "value")
67
+ else str(obj.status)
68
+ ),
69
+ "assignee": lambda obj: obj.assignee,
70
+ "description": lambda obj: obj.description,
71
+ "labels": lambda obj: sorted(obj.labels or []),
72
+ }
73
+
74
+ has_local_changes = False
75
+ changed_fields = []
76
+
77
+ for field_name, getter in fields_to_check.items():
78
+ try:
79
+ local_value = getter(local_issue)
80
+ except Exception as e:
81
+ logger.warning(
82
+ "filter_field_extraction_failed",
83
+ issue_id=issue_id,
84
+ field=field_name,
85
+ error=str(e),
86
+ )
87
+ local_value = None
88
+
89
+ base_value = getattr(base_state, field_name, None)
90
+
91
+ if local_value != base_value:
92
+ logger.debug(
93
+ "filter_local_change_detected",
94
+ issue_id=issue_id,
95
+ field=field_name,
96
+ base_value=base_value,
97
+ local_value=local_value,
98
+ )
99
+ changed_fields.append(field_name)
100
+ has_local_changes = True
101
+
102
+ if has_local_changes:
103
+ logger.debug(
104
+ "filter_issue_has_local_changes",
105
+ issue_id=issue_id,
106
+ changed_fields=changed_fields,
107
+ )
108
+ changed_count += 1
109
+ filtered.append(issue)
110
+ else:
111
+ logger.debug(
112
+ "filter_issue_unchanged_since_sync",
113
+ issue_id=issue_id,
114
+ reason="no_local_modifications",
115
+ )
116
+ skipped_count += 1
117
+
118
+ logger.info(
119
+ "filter_complete",
120
+ input_count=len(issues),
121
+ output_count=len(filtered),
122
+ skipped_count=skipped_count,
123
+ new_count=new_count,
124
+ changed_count=changed_count,
125
+ filtered_out_percentage=round((skipped_count / len(issues) * 100), 1)
126
+ if len(issues) > 0
127
+ else 0,
128
+ )
129
+ return filtered
@@ -0,0 +1,71 @@
1
+ """Processes pull results from sync operations.
2
+
3
+ Extracted from SyncMergeEngine to separate pull processing logic.
4
+ """
5
+
6
+ from structlog import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class PullResultProcessor:
12
+ """Processes results from pull operations."""
13
+
14
+ @staticmethod
15
+ def process_pull_result(fetched) -> tuple[int, list, list]:
16
+ """Process pull result and extract metrics.
17
+
18
+ Args:
19
+ fetched: Pull result (list or report object)
20
+
21
+ Returns:
22
+ Tuple of (pulled_count, pull_errors, pulled_remote_ids)
23
+ """
24
+ pulled_count = 0
25
+ pull_errors = []
26
+ pulled_remote_ids = []
27
+
28
+ if isinstance(fetched, list):
29
+ pulled_items = [r for r in fetched if r]
30
+ pulled_count = len(pulled_items)
31
+ for item in pulled_items:
32
+ try:
33
+ rid = getattr(item, "backend_id", None) or getattr(item, "id", None)
34
+ if rid is not None:
35
+ pulled_remote_ids.append(str(rid))
36
+ except Exception as e:
37
+ logger.debug("pulled_item_id_extraction_failed", error=str(e))
38
+ continue
39
+ else:
40
+ pull_report = fetched
41
+ if getattr(pull_report, "errors", None):
42
+ try:
43
+ err_keys = (
44
+ list(pull_report.errors.keys())
45
+ if isinstance(pull_report.errors, dict)
46
+ else []
47
+ )
48
+ except Exception as e:
49
+ logger.debug("error_keys_extraction_failed", error=str(e))
50
+ err_keys = []
51
+ pull_errors = err_keys
52
+ logger.warning(
53
+ "pull_batch_had_errors",
54
+ error_count=len(pull_errors),
55
+ errors=str(pull_report.errors)[:200],
56
+ )
57
+
58
+ pulled_raw = getattr(pull_report, "pulled", None)
59
+ if pulled_raw is None:
60
+ pulled_count = 0
61
+ pulled_remote_ids = []
62
+ else:
63
+ try:
64
+ pulled_iter = list(pulled_raw)
65
+ except Exception as e:
66
+ logger.debug("pulled_iter_conversion_failed", error=str(e))
67
+ pulled_iter = [pulled_raw]
68
+ pulled_remote_ids = [str(i) for i in pulled_iter]
69
+ pulled_count = len(pulled_remote_ids)
70
+
71
+ return pulled_count, pull_errors, pulled_remote_ids