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,672 @@
1
+ """Specialized synchronizers for different entity types."""
2
+
3
+ import json
4
+ from datetime import date, datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from roadmap.adapters.persistence.file_parser import FileParser
9
+ from roadmap.common.datetime_parser import UnifiedDateTimeParser
10
+ from roadmap.common.logging import get_logger
11
+ from roadmap.core.interfaces.sync_validators import (
12
+ ForeignKeyValidationError,
13
+ ForeignKeyValidator,
14
+ )
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class EntitySyncCoordinator:
20
+ """Base coordinator for syncing entity files to database."""
21
+
22
+ def __init__(self, get_connection, transaction_context):
23
+ """Initialize the coordinator.
24
+
25
+ Args:
26
+ get_connection: Callable that returns a database connection
27
+ transaction_context: Context manager for transactions
28
+ """
29
+ self._get_connection = get_connection
30
+ self._transaction = transaction_context
31
+ self._parser = FileParser()
32
+
33
+ def _get_default_project_id(self) -> str | None:
34
+ """Get the first available project ID for orphaned items."""
35
+ try:
36
+ with self._transaction() as conn:
37
+ result = conn.execute("SELECT id FROM projects LIMIT 1").fetchone()
38
+ return result[0] if result else None
39
+ except Exception as e:
40
+ logger.error(
41
+ "failed_to_get_default_project_id", error=str(e), severity="operational"
42
+ )
43
+ return None
44
+
45
+ def _get_milestone_id_by_name(self, milestone_name: str) -> str | None:
46
+ """Get milestone ID by name."""
47
+ try:
48
+ with self._transaction() as conn:
49
+ result = conn.execute(
50
+ "SELECT id FROM milestones WHERE title = ?", (milestone_name,)
51
+ ).fetchone()
52
+ return result[0] if result else None
53
+ except Exception as e:
54
+ logger.warning(
55
+ "failed_to_find_milestone",
56
+ milestone_name=milestone_name,
57
+ error=str(e),
58
+ severity="operational",
59
+ )
60
+ return None
61
+
62
+ def _normalize_date(self, date_value: Any) -> Any:
63
+ """Normalize date field from YAML."""
64
+ if not date_value:
65
+ return None
66
+ try:
67
+ if isinstance(date_value, str):
68
+ dt = UnifiedDateTimeParser.parse_any_datetime(date_value)
69
+ return dt.date() if dt else None
70
+ return date_value
71
+ except (ValueError, AttributeError):
72
+ return None
73
+
74
+ def _extract_metadata(self, data: dict, exclude_fields: list[str]) -> str | None:
75
+ """Extract non-standard fields as JSON metadata."""
76
+ metadata = {k: v for k, v in data.items() if k not in exclude_fields}
77
+ if not metadata:
78
+ return None
79
+
80
+ return json.dumps(metadata, default=self._json_default)
81
+
82
+ @staticmethod
83
+ def _json_default(value: Any) -> str:
84
+ """Handle JSON serialization of non-standard types."""
85
+ if isinstance(value, (datetime, date)):
86
+ return value.isoformat()
87
+ # Handle datetime-like objects that may have isoformat method
88
+ if hasattr(value, "isoformat") and callable(value.isoformat):
89
+ try:
90
+ result = value.isoformat()
91
+ if isinstance(result, str):
92
+ return result
93
+ except Exception as e:
94
+ logger.warning(
95
+ "isoformat_serialization_failed",
96
+ error=str(e),
97
+ value_type=type(value).__name__,
98
+ severity="operational",
99
+ )
100
+ raise TypeError(
101
+ f"Object of type {type(value).__name__} is not JSON serializable"
102
+ )
103
+
104
+ def _update_sync_status(self, file_path: Path) -> None:
105
+ """Update sync status for a file."""
106
+ metadata = self._parser.extract_file_metadata(file_path)
107
+ if metadata:
108
+ with self._transaction() as conn:
109
+ conn.execute(
110
+ """
111
+ INSERT OR REPLACE INTO file_sync_state
112
+ (file_path, content_hash, file_size, last_modified)
113
+ VALUES (?, ?, ?, ?)
114
+ """,
115
+ (
116
+ str(file_path),
117
+ metadata["hash"],
118
+ metadata["size"],
119
+ metadata["modified_time"],
120
+ ),
121
+ )
122
+
123
+
124
+ class MilestoneFKValidator(ForeignKeyValidator):
125
+ """Validates foreign key constraints for milestone syncing.
126
+
127
+ Ensures that projects table has data before syncing milestones,
128
+ preventing FOREIGN KEY constraint violations.
129
+ """
130
+
131
+ def __init__(self, get_connection):
132
+ """Initialize validator.
133
+
134
+ Args:
135
+ get_connection: Callable that returns database connection
136
+ """
137
+ self._get_connection = get_connection
138
+
139
+ def validate(self) -> None:
140
+ """Validate that projects table has data.
141
+
142
+ Raises:
143
+ ForeignKeyValidationError: If projects table is empty
144
+ RuntimeError: If database connection fails
145
+ """
146
+ try:
147
+ with self._get_connection() as conn:
148
+ result = conn.execute(
149
+ "SELECT COUNT(*) as count FROM projects"
150
+ ).fetchone()
151
+ project_count = result[0] if result else 0
152
+
153
+ if project_count == 0:
154
+ raise ForeignKeyValidationError(
155
+ entity_type="milestone",
156
+ missing_references=[],
157
+ error_details="Projects table is empty. Cannot sync milestones without prerequisite projects. "
158
+ "Ensure ProjectSyncCoordinator syncs all projects before MilestoneSyncCoordinator.",
159
+ )
160
+
161
+ logger.info(
162
+ "milestone_fk_validation_passed",
163
+ project_count=project_count,
164
+ )
165
+
166
+ except ForeignKeyValidationError:
167
+ raise
168
+ except Exception as e:
169
+ logger.error(
170
+ "milestone_fk_validation_error",
171
+ error=str(e),
172
+ error_type=type(e).__name__,
173
+ severity="system_error",
174
+ )
175
+ raise RuntimeError(
176
+ f"Failed to validate milestone FK constraints: {str(e)}"
177
+ ) from e
178
+
179
+ def missing_prerequisites(self) -> list[str]:
180
+ """Return list of empty prerequisite tables.
181
+
182
+ Returns:
183
+ List describing which prerequisites are missing
184
+ """
185
+ try:
186
+ with self._get_connection() as conn:
187
+ result = conn.execute(
188
+ "SELECT COUNT(*) as count FROM projects"
189
+ ).fetchone()
190
+ project_count = result[0] if result else 0
191
+
192
+ if project_count == 0:
193
+ return ["projects (empty)"]
194
+ return []
195
+
196
+ except Exception as e:
197
+ logger.error(
198
+ "Error checking prerequisites",
199
+ error=str(e),
200
+ severity="system_error",
201
+ )
202
+ return ["unknown (database error)"]
203
+
204
+
205
+ class IssueFKValidator(ForeignKeyValidator):
206
+ """Validates foreign key constraints for issue syncing.
207
+
208
+ Ensures that both projects and milestones tables have prerequisite data
209
+ before syncing issues, preventing FOREIGN KEY constraint violations.
210
+ """
211
+
212
+ def __init__(self, get_connection):
213
+ """Initialize validator.
214
+
215
+ Args:
216
+ get_connection: Callable that returns database connection
217
+ """
218
+ self._get_connection = get_connection
219
+
220
+ def validate(self) -> None:
221
+ """Validate that projects table has data.
222
+
223
+ Raises:
224
+ ForeignKeyValidationError: If projects table is empty
225
+ RuntimeError: If database connection fails
226
+
227
+ Notes:
228
+ Issues depend on projects being populated.
229
+ Milestones are optional but must exist if referenced.
230
+ """
231
+ try:
232
+ with self._get_connection() as conn:
233
+ # Check projects (required)
234
+ projects_result = conn.execute(
235
+ "SELECT COUNT(*) as count FROM projects"
236
+ ).fetchone()
237
+ project_count = projects_result[0] if projects_result else 0
238
+
239
+ if project_count == 0:
240
+ raise ForeignKeyValidationError(
241
+ entity_type="issue",
242
+ missing_references=[],
243
+ error_details="Projects table is empty. Cannot sync issues without prerequisite projects. "
244
+ "Ensure ProjectSyncCoordinator syncs all projects before IssueSyncCoordinator.",
245
+ )
246
+
247
+ logger.info(
248
+ "issue_fk_validation_passed",
249
+ project_count=project_count,
250
+ )
251
+
252
+ except ForeignKeyValidationError:
253
+ raise
254
+ except Exception as e:
255
+ logger.error(
256
+ "issue_fk_validation_error",
257
+ error=str(e),
258
+ error_type=type(e).__name__,
259
+ severity="system_error",
260
+ )
261
+ raise RuntimeError(
262
+ f"Failed to validate issue FK constraints: {str(e)}"
263
+ ) from e
264
+
265
+ def missing_prerequisites(self) -> list[str]:
266
+ """Return list of empty prerequisite tables.
267
+
268
+ Returns:
269
+ List describing which prerequisites are missing
270
+ """
271
+ try:
272
+ with self._get_connection() as conn:
273
+ result = conn.execute(
274
+ "SELECT COUNT(*) as count FROM projects"
275
+ ).fetchone()
276
+ project_count = result[0] if result else 0
277
+
278
+ if project_count == 0:
279
+ return ["projects (empty)"]
280
+ return []
281
+
282
+ except Exception as e:
283
+ logger.error(
284
+ "Error checking prerequisites",
285
+ error=str(e),
286
+ severity="system_error",
287
+ )
288
+ return ["unknown (database error)"]
289
+
290
+
291
+ class IssueSyncCoordinator(EntitySyncCoordinator):
292
+ """Handles syncing issue files to database."""
293
+
294
+ def _extract_issue_id(self, issue_data: dict, file_path: Path) -> str:
295
+ """Extract issue ID from data or filename.
296
+
297
+ Args:
298
+ issue_data: Parsed issue data
299
+ file_path: Path to issue file
300
+
301
+ Returns:
302
+ Extracted or derived issue ID
303
+ """
304
+ issue_id = issue_data.get("id")
305
+ if not issue_id:
306
+ stem = file_path.stem
307
+ if stem.startswith("issue-"):
308
+ issue_id = stem[6:]
309
+ else:
310
+ issue_id = stem
311
+ issue_data["id"] = issue_id
312
+ return issue_id
313
+
314
+ def _handle_project_id(self, issue_data: dict, issue_id: str) -> str | None:
315
+ """Get project ID for issue, using default if needed.
316
+
317
+ Args:
318
+ issue_data: Issue data dict
319
+ issue_id: Issue ID
320
+
321
+ Returns:
322
+ Project ID or None if unavailable
323
+ """
324
+ project_id = issue_data.get("project_id")
325
+ if not project_id:
326
+ project_id = self._get_default_project_id()
327
+ if not project_id:
328
+ logger.warning(
329
+ "no_projects_found_for_issue",
330
+ issue_id=issue_id,
331
+ severity="operational",
332
+ )
333
+ return None
334
+ issue_data["project_id"] = project_id
335
+ return project_id
336
+
337
+ def _handle_milestone_field(self, issue_data: dict) -> None:
338
+ """Resolve milestone field (could be name or ID).
339
+
340
+ Args:
341
+ issue_data: Issue data dict to update
342
+ """
343
+ milestone_id = issue_data.get("milestone_id")
344
+ if not milestone_id and "milestone" in issue_data:
345
+ milestone_name = issue_data["milestone"]
346
+ milestone_id = self._get_milestone_id_by_name(milestone_name)
347
+ issue_data["milestone_id"] = milestone_id
348
+
349
+ def _normalize_issue_fields(self, issue_data: dict) -> None:
350
+ """Set defaults and normalize issue fields.
351
+
352
+ Args:
353
+ issue_data: Issue data dict to normalize
354
+ """
355
+ issue_data.setdefault("title", "Untitled")
356
+ issue_data.setdefault("status", "open")
357
+ issue_data.setdefault("priority", "medium")
358
+ issue_data.setdefault("issue_type", issue_data.pop("type", "task"))
359
+ issue_data["due_date"] = self._normalize_date(issue_data.get("due_date"))
360
+
361
+ def sync_issue_file(self, file_path: Path) -> bool:
362
+ """Sync a single issue file to database."""
363
+ try:
364
+ if not file_path.exists():
365
+ logger.warning(
366
+ "issue_file_not_found",
367
+ file_path=str(file_path),
368
+ severity="operational",
369
+ )
370
+ return False
371
+
372
+ # Parse YAML frontmatter
373
+ issue_data = self._parser.parse_yaml_frontmatter(file_path)
374
+ if not issue_data:
375
+ logger.warning(
376
+ "no_yaml_data_found",
377
+ file_path=str(file_path),
378
+ severity="operational",
379
+ )
380
+ return False
381
+
382
+ # Extract issue ID
383
+ issue_id = self._extract_issue_id(issue_data, file_path)
384
+
385
+ # Handle project ID
386
+ project_id = self._handle_project_id(issue_data, issue_id)
387
+ if not project_id:
388
+ return False
389
+
390
+ # Handle milestone field
391
+ self._handle_milestone_field(issue_data)
392
+
393
+ # Normalize fields
394
+ self._normalize_issue_fields(issue_data)
395
+
396
+ # Extract metadata
397
+ exclude_fields = [
398
+ "id",
399
+ "title",
400
+ "description",
401
+ "status",
402
+ "priority",
403
+ "issue_type",
404
+ "assignee",
405
+ "estimate_hours",
406
+ "due_date",
407
+ "project_id",
408
+ "milestone_id",
409
+ ]
410
+ metadata = self._extract_metadata(issue_data, exclude_fields)
411
+
412
+ # Upsert issue
413
+ with self._transaction() as conn:
414
+ conn.execute(
415
+ """
416
+ INSERT OR REPLACE INTO issues
417
+ (id, project_id, milestone_id, title, description, status,
418
+ priority, issue_type, assignee, estimate_hours, due_date, metadata)
419
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
420
+ """,
421
+ (
422
+ issue_data["id"],
423
+ issue_data["project_id"],
424
+ issue_data.get("milestone_id"),
425
+ issue_data["title"],
426
+ issue_data.get("description"),
427
+ issue_data["status"],
428
+ issue_data["priority"],
429
+ issue_data["issue_type"],
430
+ issue_data.get("assignee"),
431
+ issue_data.get("estimate_hours"),
432
+ issue_data["due_date"],
433
+ metadata,
434
+ ),
435
+ )
436
+
437
+ # Update sync status
438
+ self._update_sync_status(file_path)
439
+ logger.info(
440
+ "synced_issue_file",
441
+ issue_id=issue_id,
442
+ file_path=str(file_path),
443
+ )
444
+ return True
445
+
446
+ except Exception as e:
447
+ logger.error(
448
+ "failed_to_sync_issue_file",
449
+ file_path=str(file_path),
450
+ error=str(e),
451
+ severity="operational",
452
+ )
453
+ return False
454
+
455
+
456
+ class MilestoneSyncCoordinator(EntitySyncCoordinator):
457
+ """Handles syncing milestone files to database."""
458
+
459
+ def __init__(self, get_connection, transaction_context):
460
+ """Initialize coordinator with FK validator.
461
+
462
+ Args:
463
+ get_connection: Callable that returns a database connection
464
+ transaction_context: Context manager for transactions
465
+ """
466
+ super().__init__(get_connection, transaction_context)
467
+ self._fk_validator = MilestoneFKValidator(get_connection)
468
+
469
+ def sync_milestone_file(self, file_path: Path) -> bool:
470
+ """Sync a single milestone file to database.
471
+
472
+ Validates foreign key constraints before syncing to prevent
473
+ database corruption from FOREIGN KEY constraint violations.
474
+ """
475
+ try:
476
+ # Validate foreign key constraints first
477
+ try:
478
+ self._fk_validator.validate()
479
+ except ForeignKeyValidationError as fk_error:
480
+ logger.error(
481
+ "milestone_sync_blocked_by_fk_validation",
482
+ file_path=str(file_path),
483
+ entity_type=fk_error.entity_type,
484
+ error=str(fk_error),
485
+ severity="operational",
486
+ )
487
+ return False
488
+
489
+ if not file_path.exists():
490
+ logger.warning(
491
+ "milestone_file_not_found",
492
+ file_path=str(file_path),
493
+ severity="operational",
494
+ )
495
+ return False
496
+
497
+ milestone_data = self._parser.parse_yaml_frontmatter(file_path)
498
+ if not milestone_data:
499
+ logger.warning(
500
+ "milestone_no_yaml_data_found",
501
+ file_path=str(file_path),
502
+ severity="operational",
503
+ )
504
+ return False
505
+
506
+ # Extract milestone ID
507
+ milestone_id = milestone_data.get("id", file_path.stem)
508
+ milestone_data["id"] = milestone_id
509
+
510
+ # Handle missing project_id
511
+ project_id = milestone_data.get("project_id")
512
+ if not project_id:
513
+ project_id = self._get_default_project_id()
514
+ if not project_id:
515
+ logger.warning(
516
+ "no_projects_found_for_milestone",
517
+ milestone_id=milestone_id,
518
+ severity="operational",
519
+ )
520
+ return False
521
+
522
+ # Set defaults and normalize
523
+ title = milestone_data.get("title") or milestone_data.get(
524
+ "name", "Untitled Milestone"
525
+ )
526
+ milestone_data["title"] = title
527
+ milestone_data["status"] = milestone_data.get("status", "open")
528
+ milestone_data["project_id"] = project_id
529
+ milestone_data["progress_percentage"] = milestone_data.get(
530
+ "progress_percentage", 0.0
531
+ )
532
+ milestone_data["due_date"] = self._normalize_date(
533
+ milestone_data.get("due_date")
534
+ )
535
+
536
+ # Extract metadata
537
+ exclude_fields = [
538
+ "id",
539
+ "title",
540
+ "description",
541
+ "status",
542
+ "due_date",
543
+ "progress_percentage",
544
+ "project_id",
545
+ ]
546
+ metadata = self._extract_metadata(milestone_data, exclude_fields)
547
+
548
+ # Upsert milestone
549
+ with self._transaction() as conn:
550
+ conn.execute(
551
+ """
552
+ INSERT OR REPLACE INTO milestones
553
+ (id, project_id, title, description, status, due_date, progress_percentage, metadata)
554
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
555
+ """,
556
+ (
557
+ milestone_data["id"],
558
+ milestone_data["project_id"],
559
+ milestone_data["title"],
560
+ milestone_data.get("description"),
561
+ milestone_data["status"],
562
+ milestone_data["due_date"],
563
+ milestone_data["progress_percentage"],
564
+ metadata,
565
+ ),
566
+ )
567
+
568
+ # Update sync status
569
+ self._update_sync_status(file_path)
570
+ logger.info(
571
+ "synced_milestone_file",
572
+ milestone_id=milestone_id,
573
+ file_path=str(file_path),
574
+ )
575
+ return True
576
+
577
+ except Exception as e:
578
+ logger.error(
579
+ "failed_to_sync_milestone_file",
580
+ file_path=str(file_path),
581
+ error=str(e),
582
+ severity="operational",
583
+ )
584
+ return False
585
+
586
+
587
+ class ProjectSyncCoordinator(EntitySyncCoordinator):
588
+ """Handles syncing project files to database."""
589
+
590
+ def sync_project_file(self, file_path: Path) -> bool:
591
+ """Sync a single project file to database."""
592
+ try:
593
+ if not file_path.exists():
594
+ logger.warning(
595
+ "project_file_not_found",
596
+ file_path=str(file_path),
597
+ severity="operational",
598
+ )
599
+ return False
600
+
601
+ project_data = self._parser.parse_yaml_frontmatter(file_path)
602
+ if not project_data:
603
+ logger.warning(
604
+ "project_no_yaml_data_found",
605
+ file_path=str(file_path),
606
+ severity="operational",
607
+ )
608
+ return False
609
+
610
+ # Extract project ID
611
+ # If not in YAML, extract from filename (format: {uuid}-{name}.md)
612
+ if "id" in project_data:
613
+ project_id = project_data["id"]
614
+ else:
615
+ stem = file_path.stem
616
+ # Check if filename matches UUID pattern (8+ hex chars before hyphen)
617
+ parts = stem.split("-", 1)
618
+ first_part = parts[0]
619
+ # Only treat as UUID if first part is 8+ hex characters
620
+ if len(first_part) >= 8 and all(
621
+ c in "0123456789abcdefABCDEF" for c in first_part
622
+ ):
623
+ project_id = first_part # Use UUID only
624
+ else:
625
+ project_id = stem # Use full filename as ID
626
+ project_data["id"] = project_id
627
+
628
+ # Set defaults
629
+ name = project_data.get("name") or project_data.get(
630
+ "title", "Untitled Project"
631
+ )
632
+ project_data["name"] = name
633
+ project_data["status"] = project_data.get("status", "active")
634
+
635
+ # Extract metadata
636
+ exclude_fields = ["id", "name", "description", "status"]
637
+ metadata = self._extract_metadata(project_data, exclude_fields)
638
+
639
+ # Upsert project
640
+ with self._transaction() as conn:
641
+ conn.execute(
642
+ """
643
+ INSERT OR REPLACE INTO projects
644
+ (id, name, description, status, metadata)
645
+ VALUES (?, ?, ?, ?, ?)
646
+ """,
647
+ (
648
+ project_data["id"],
649
+ project_data["name"],
650
+ project_data.get("description"),
651
+ project_data["status"],
652
+ metadata,
653
+ ),
654
+ )
655
+
656
+ # Update sync status
657
+ self._update_sync_status(file_path)
658
+ logger.info(
659
+ "synced_project_file",
660
+ project_id=project_id,
661
+ file_path=str(file_path),
662
+ )
663
+ return True
664
+
665
+ except Exception as e:
666
+ logger.error(
667
+ "failed_to_sync_project_file",
668
+ file_path=str(file_path),
669
+ error=str(e),
670
+ severity="operational",
671
+ )
672
+ return False