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,330 @@
1
+ """Git history utilities for accessing file content and baselines.
2
+
3
+ This module provides utilities to reconstruct baseline state from git history,
4
+ enabling three-way merge without a database. Key functions:
5
+
6
+ - get_file_at_timestamp: Get file content as it existed at a specific time
7
+ - find_commit_at_time: Find the git commit closest to a timestamp
8
+ - get_file_at_commit: Get file content at a specific commit SHA
9
+ """
10
+
11
+ import subprocess
12
+ from datetime import datetime
13
+
14
+ from roadmap.common.logging import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class GitHistoryError(Exception):
20
+ """Base exception for git history operations."""
21
+
22
+ pass
23
+
24
+
25
+ class NotAGitRepository(GitHistoryError):
26
+ """Raised when not in a git repository."""
27
+
28
+ pass
29
+
30
+
31
+ class FileNotFound(GitHistoryError):
32
+ """Raised when file is not found in git history."""
33
+
34
+ pass
35
+
36
+
37
+ def _run_git_command(args: list[str], cwd: str = ".") -> str:
38
+ """Run a git command and return stdout.
39
+
40
+ Args:
41
+ args: Git command arguments (e.g., ['log', '--oneline'])
42
+ cwd: Working directory for the command
43
+
44
+ Returns:
45
+ Command stdout as string
46
+
47
+ Raises:
48
+ NotAGitRepository: If not in a git repository
49
+ GitHistoryError: If git command fails
50
+ """
51
+ try:
52
+ result = subprocess.run(
53
+ ["git"] + args,
54
+ cwd=cwd,
55
+ capture_output=True,
56
+ text=True,
57
+ check=False,
58
+ )
59
+
60
+ if result.returncode != 0:
61
+ error_msg = result.stderr.strip()
62
+
63
+ # Check for "not a git repository" error
64
+ if "fatal: not a git repository" in error_msg.lower():
65
+ raise NotAGitRepository(f"Not in a git repository: {error_msg}")
66
+
67
+ raise GitHistoryError(f"Git command failed: {error_msg}")
68
+
69
+ return result.stdout.strip()
70
+
71
+ except FileNotFoundError as e:
72
+ raise GitHistoryError("Git not found in PATH") from e
73
+
74
+
75
+ def find_commit_at_time(
76
+ timestamp: datetime | str, file_path: str | None = None, cwd: str = "."
77
+ ) -> str:
78
+ """Find the git commit closest to a given timestamp.
79
+
80
+ Args:
81
+ timestamp: Datetime or ISO 8601 string to search for
82
+ file_path: Optional - only find commits affecting this file
83
+ cwd: Working directory
84
+
85
+ Returns:
86
+ Commit SHA of the closest commit
87
+
88
+ Raises:
89
+ NotAGitRepository: If not in a git repository
90
+ GitHistoryError: If no commits found
91
+ """
92
+ if isinstance(timestamp, str):
93
+ try:
94
+ timestamp = datetime.fromisoformat(timestamp)
95
+ except ValueError as e:
96
+ raise GitHistoryError(f"Invalid timestamp format: {timestamp}") from e
97
+
98
+ timestamp_str = timestamp.isoformat()
99
+
100
+ try:
101
+ args = [
102
+ "log",
103
+ "--format=%H %aI", # SHA and timestamp
104
+ f"--until={timestamp_str}",
105
+ "-1", # Get most recent commit before timestamp
106
+ ]
107
+
108
+ if file_path:
109
+ args.append("--")
110
+ args.append(file_path)
111
+
112
+ output = _run_git_command(args, cwd)
113
+
114
+ if not output:
115
+ # No commits found - try to get the very first commit
116
+ logger.warning(
117
+ "no_commits_before_timestamp",
118
+ timestamp=timestamp_str,
119
+ file_path=file_path,
120
+ severity="operational",
121
+ )
122
+ args_first = ["log", "--format=%H", "--reverse", "-1"]
123
+ if file_path:
124
+ args_first.extend(["--", file_path])
125
+ first_commit = _run_git_command(args_first, cwd)
126
+ if not first_commit:
127
+ raise GitHistoryError("No commits found in repository")
128
+ return first_commit
129
+
130
+ # Parse "SHA TIMESTAMP" format and return SHA
131
+ sha = output.split()[0]
132
+ logger.debug(
133
+ "found_commit_at_time",
134
+ timestamp=timestamp_str,
135
+ commit=sha[:8],
136
+ file_path=file_path,
137
+ )
138
+ return sha
139
+
140
+ except GitHistoryError:
141
+ raise
142
+
143
+
144
+ def get_file_at_commit(file_path: str, commit_sha: str, cwd: str = ".") -> str:
145
+ """Get file content at a specific commit.
146
+
147
+ Args:
148
+ file_path: Path to file (relative to repo root)
149
+ commit_sha: Git commit SHA
150
+ cwd: Working directory
151
+
152
+ Returns:
153
+ File content as string
154
+
155
+ Raises:
156
+ NotAGitRepository: If not in a git repository
157
+ FileNotFound: If file doesn't exist at commit
158
+ GitHistoryError: If git command fails
159
+ """
160
+ try:
161
+ output = _run_git_command(["show", f"{commit_sha}:{file_path}"], cwd)
162
+ logger.debug(
163
+ "retrieved_file_at_commit",
164
+ file=file_path,
165
+ commit=commit_sha[:8],
166
+ )
167
+ return output
168
+
169
+ except GitHistoryError as e:
170
+ if "does not exist" in str(e).lower():
171
+ raise FileNotFound(
172
+ f"File {file_path} not found at commit {commit_sha}"
173
+ ) from e
174
+ raise
175
+
176
+
177
+ def get_file_at_timestamp(
178
+ file_path: str, timestamp: datetime | str, cwd: str = "."
179
+ ) -> str:
180
+ """Get file content as it existed at a specific timestamp.
181
+
182
+ This function reconstructs the baseline by:
183
+ 1. Finding the commit closest to the timestamp
184
+ 2. Retrieving the file content at that commit
185
+
186
+ Args:
187
+ file_path: Path to file (relative to repo root)
188
+ timestamp: Datetime or ISO 8601 string
189
+ cwd: Working directory
190
+
191
+ Returns:
192
+ File content as string
193
+
194
+ Raises:
195
+ NotAGitRepository: If not in a git repository
196
+ FileNotFound: If file didn't exist at timestamp
197
+ GitHistoryError: If git operations fail
198
+ """
199
+ try:
200
+ commit_sha = find_commit_at_time(timestamp, file_path, cwd)
201
+ content = get_file_at_commit(file_path, commit_sha, cwd)
202
+ logger.debug(
203
+ "file_retrieved_at_timestamp",
204
+ file=file_path,
205
+ timestamp=str(timestamp),
206
+ commit=commit_sha[:8],
207
+ )
208
+ return content
209
+
210
+ except (NotAGitRepository, FileNotFound, GitHistoryError):
211
+ raise
212
+
213
+
214
+ def get_file_at_head(file_path: str, cwd: str = ".") -> str:
215
+ """Get current file content from HEAD.
216
+
217
+ Args:
218
+ file_path: Path to file (relative to repo root)
219
+ cwd: Working directory
220
+
221
+ Returns:
222
+ Current file content
223
+
224
+ Raises:
225
+ NotAGitRepository: If not in a git repository
226
+ FileNotFound: If file doesn't exist in HEAD
227
+ GitHistoryError: If git command fails
228
+ """
229
+ return get_file_at_commit(file_path, "HEAD", cwd)
230
+
231
+
232
+ def get_last_modified_time(file_path: str, cwd: str = ".") -> datetime | None:
233
+ """Get the timestamp of the last commit that modified a file.
234
+
235
+ Args:
236
+ file_path: Path to file (relative to repo root)
237
+ cwd: Working directory
238
+
239
+ Returns:
240
+ Datetime of last modification, or None if file not found
241
+
242
+ Raises:
243
+ NotAGitRepository: If not in a git repository
244
+ GitHistoryError: If git command fails
245
+ """
246
+ try:
247
+ output = _run_git_command(["log", "-1", "--format=%aI", "--", file_path], cwd)
248
+
249
+ if not output:
250
+ logger.debug(
251
+ "file_not_in_history",
252
+ file=file_path,
253
+ )
254
+ return None
255
+
256
+ return datetime.fromisoformat(output)
257
+
258
+ except GitHistoryError as e:
259
+ if "does not exist" in str(e).lower():
260
+ return None
261
+ raise
262
+
263
+
264
+ def is_git_repository(cwd: str = ".") -> bool:
265
+ """Check if a directory is a git repository.
266
+
267
+ Args:
268
+ cwd: Directory to check
269
+
270
+ Returns:
271
+ True if directory is a git repository, False otherwise
272
+ """
273
+ try:
274
+ _run_git_command(["rev-parse", "--git-dir"], cwd)
275
+ return True
276
+ except (NotAGitRepository, GitHistoryError):
277
+ return False
278
+
279
+
280
+ def get_repository_root(cwd: str = ".") -> str:
281
+ """Get the root directory of the git repository.
282
+
283
+ Args:
284
+ cwd: Working directory
285
+
286
+ Returns:
287
+ Path to repository root
288
+
289
+ Raises:
290
+ NotAGitRepository: If not in a git repository
291
+ GitHistoryError: If git command fails
292
+ """
293
+ try:
294
+ root = _run_git_command(["rev-parse", "--show-toplevel"], cwd)
295
+ logger.debug("found_repo_root", root=root)
296
+ return root
297
+ except GitHistoryError:
298
+ raise
299
+
300
+
301
+ def get_changed_files_since_commit(ref: str = "HEAD~1", cwd: str = ".") -> set[str]:
302
+ """Get list of files changed since a git reference.
303
+
304
+ Args:
305
+ ref: Git reference (default: previous commit)
306
+ cwd: Working directory
307
+
308
+ Returns:
309
+ Set of file paths that changed
310
+
311
+ Raises:
312
+ NotAGitRepository: If not in a git repository
313
+ GitHistoryError: If git command fails
314
+ """
315
+ try:
316
+ output = _run_git_command(["diff", "--name-only", ref, "HEAD"], cwd)
317
+
318
+ if not output:
319
+ return set()
320
+
321
+ files = set(output.split("\n"))
322
+ logger.debug(
323
+ "found_changed_files",
324
+ ref=ref,
325
+ count=len(files),
326
+ )
327
+ return files
328
+
329
+ except GitHistoryError:
330
+ raise
@@ -0,0 +1,20 @@
1
+ """Parser package with modularized entity-specific parsers.
2
+
3
+ Organizes parser classes across semantic modules:
4
+ - frontmatter.py: YAML frontmatter parsing and serialization (70 LOC)
5
+ - issue.py: Issue entity parsing and serialization (160 LOC)
6
+ - milestone.py: Milestone entity parsing and serialization (130 LOC)
7
+ - project.py: Project entity parsing and serialization (100 LOC)
8
+ """
9
+
10
+ from .frontmatter import FrontmatterParser
11
+ from .issue import IssueParser
12
+ from .milestone import MilestoneParser
13
+ from .project import ProjectParser
14
+
15
+ __all__ = [
16
+ "FrontmatterParser",
17
+ "IssueParser",
18
+ "MilestoneParser",
19
+ "ProjectParser",
20
+ ]
@@ -0,0 +1,152 @@
1
+ """Frontmatter parser for markdown files with YAML headers."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from roadmap.common.utils.file_utils import ensure_directory_exists, file_exists_check
11
+
12
+
13
+ class FrontmatterParser:
14
+ """Parser for markdown files with YAML frontmatter."""
15
+
16
+ FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
17
+
18
+ @classmethod
19
+ def parse_file(cls, file_path: Path) -> tuple[dict[str, Any], str]:
20
+ """Parse a markdown file and return frontmatter and content."""
21
+ if not file_exists_check(file_path):
22
+ raise FileNotFoundError(f"File not found: {file_path}")
23
+
24
+ content = file_path.read_text(encoding="utf-8")
25
+ return cls.parse_content(content)
26
+
27
+ @classmethod
28
+ def parse_content(cls, content: str) -> tuple[dict[str, Any], str]:
29
+ """Parse markdown content and return frontmatter and body."""
30
+ match = cls.FRONTMATTER_PATTERN.match(content)
31
+
32
+ if not match:
33
+ # No frontmatter found, return empty dict and full content
34
+ return {}, content
35
+
36
+ frontmatter_str, markdown_content = match.groups()
37
+
38
+ try:
39
+ loaded = yaml.safe_load(frontmatter_str)
40
+ frontmatter = loaded if isinstance(loaded, dict) else {}
41
+ except yaml.YAMLError as e:
42
+ raise ValueError(f"Invalid YAML frontmatter: {e}") from e
43
+
44
+ return frontmatter, markdown_content.strip()
45
+
46
+ @classmethod
47
+ def serialize_file(
48
+ cls, frontmatter: dict[str, Any], content: str, file_path: Path
49
+ ) -> None:
50
+ """Write frontmatter and content to a markdown file."""
51
+ from roadmap.common.logging import get_logger, get_stack_trace
52
+
53
+ logger = get_logger(__name__)
54
+
55
+ logger.debug(
56
+ "serializing_file",
57
+ file_path=str(file_path),
58
+ stack=get_stack_trace(depth=3),
59
+ )
60
+
61
+ ensure_directory_exists(file_path.parent)
62
+
63
+ # Convert datetime objects to ISO format strings
64
+ serializable_frontmatter = cls._prepare_frontmatter_for_yaml(frontmatter)
65
+
66
+ frontmatter_str = yaml.dump(
67
+ serializable_frontmatter, default_flow_style=False, sort_keys=False
68
+ )
69
+
70
+ # Strip trailing whitespace from frontmatter to prevent ruff errors
71
+ frontmatter_str_stripped = frontmatter_str.rstrip() if frontmatter_str else ""
72
+
73
+ # Ensure content doesn't have trailing whitespace
74
+ content_stripped = content.rstrip()
75
+
76
+ full_content = f"---\n{frontmatter_str_stripped}\n---\n\n{content_stripped}\n"
77
+
78
+ file_path.write_text(full_content, encoding="utf-8")
79
+
80
+ @classmethod
81
+ def _prepare_frontmatter_for_yaml(
82
+ cls, frontmatter: dict[str, Any]
83
+ ) -> dict[str, Any]:
84
+ """Prepare frontmatter for YAML serialization."""
85
+ prepared = {}
86
+ for key, value in frontmatter.items():
87
+ if isinstance(value, datetime):
88
+ prepared[key] = value.isoformat()
89
+ elif hasattr(value, "value"): # Handle enum values
90
+ prepared[key] = value.value
91
+ elif value is None:
92
+ prepared[key] = None
93
+ else:
94
+ prepared[key] = value
95
+ return prepared
96
+
97
+ @classmethod
98
+ def extract_sync_metadata(
99
+ cls, frontmatter: dict[str, Any]
100
+ ) -> dict[str, Any] | None:
101
+ """Extract sync_metadata from frontmatter if present.
102
+
103
+ Args:
104
+ frontmatter: Parsed frontmatter dictionary
105
+
106
+ Returns:
107
+ Sync metadata dictionary or None if not present
108
+ """
109
+ return frontmatter.get("sync_metadata")
110
+
111
+ @classmethod
112
+ def update_sync_metadata(
113
+ cls, frontmatter: dict[str, Any], sync_metadata: dict[str, Any] | None
114
+ ) -> None:
115
+ """Update sync_metadata in frontmatter.
116
+
117
+ Args:
118
+ frontmatter: Frontmatter dictionary to update in-place
119
+ sync_metadata: Sync metadata to set (converts datetime to ISO strings), or None to remove
120
+ """
121
+ if sync_metadata:
122
+ # Prepare nested sync_metadata for YAML serialization
123
+ prepared = {}
124
+ for key, value in sync_metadata.items():
125
+ if isinstance(value, datetime):
126
+ prepared[key] = value.isoformat()
127
+ elif isinstance(value, dict):
128
+ # Handle nested dictionaries (e.g., remote_state)
129
+ prepared[key] = cls._prepare_dict_for_yaml(value)
130
+ elif hasattr(value, "value"): # Handle enum values
131
+ prepared[key] = value.value
132
+ else:
133
+ prepared[key] = value
134
+ frontmatter["sync_metadata"] = prepared
135
+ elif "sync_metadata" in frontmatter:
136
+ # Remove if None
137
+ del frontmatter["sync_metadata"]
138
+
139
+ @classmethod
140
+ def _prepare_dict_for_yaml(cls, data: dict[str, Any]) -> dict[str, Any]:
141
+ """Recursively prepare a dictionary for YAML serialization."""
142
+ prepared = {}
143
+ for key, value in data.items():
144
+ if isinstance(value, datetime):
145
+ prepared[key] = value.isoformat()
146
+ elif isinstance(value, dict):
147
+ prepared[key] = cls._prepare_dict_for_yaml(value)
148
+ elif hasattr(value, "value"): # Handle enum values
149
+ prepared[key] = value.value
150
+ else:
151
+ prepared[key] = value
152
+ return prepared