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,245 @@
1
+ """Parser for issue markdown files."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from roadmap.common.datetime_parser import parse_datetime
9
+ from roadmap.common.utils.timezone_utils import now_utc
10
+ from roadmap.core.domain import Issue, IssueType, Priority, Status
11
+
12
+ from ..persistence import enhanced_persistence
13
+ from .frontmatter import FrontmatterParser
14
+
15
+
16
+ class IssueParser:
17
+ """Parser specifically for issue markdown files."""
18
+
19
+ @staticmethod
20
+ def _parse_datetime_field(frontmatter: dict, field_name: str) -> None:
21
+ """Parse a datetime field in frontmatter if present and string."""
22
+ if field_name in frontmatter and isinstance(frontmatter[field_name], str):
23
+ frontmatter[field_name] = parse_datetime(frontmatter[field_name], "file")
24
+
25
+ @staticmethod
26
+ def _parse_enum_field(
27
+ frontmatter: dict, field_name: str, enum_class, file_path: Path
28
+ ) -> None:
29
+ """Parse enum field with validation."""
30
+ if field_name not in frontmatter:
31
+ return
32
+
33
+ try:
34
+ frontmatter[field_name] = enum_class(frontmatter[field_name])
35
+ except ValueError as e:
36
+ valid_values = [v.value for v in enum_class]
37
+ raise ValueError(
38
+ f"Invalid {field_name} '{frontmatter[field_name]}' in {file_path}. "
39
+ f"Valid values are: {', '.join(valid_values)}"
40
+ ) from e
41
+
42
+ @staticmethod
43
+ def _ensure_list_fields(frontmatter: dict) -> None:
44
+ """Ensure dependency fields are lists."""
45
+ if "depends_on" not in frontmatter:
46
+ frontmatter["depends_on"] = []
47
+ if "blocks" not in frontmatter:
48
+ frontmatter["blocks"] = []
49
+
50
+ @classmethod
51
+ def parse_issue_file(cls, file_path: Path) -> Issue:
52
+ """Parse an issue markdown file and return an Issue object."""
53
+ frontmatter, content = FrontmatterParser.parse_file(file_path)
54
+
55
+ # Parse date fields
56
+ cls._parse_datetime_field(frontmatter, "created")
57
+ cls._parse_datetime_field(frontmatter, "updated")
58
+ cls._parse_datetime_field(frontmatter, "actual_start_date")
59
+ cls._parse_datetime_field(frontmatter, "actual_end_date")
60
+ cls._parse_datetime_field(frontmatter, "handoff_date")
61
+
62
+ # Parse enum fields with validation
63
+ cls._parse_enum_field(frontmatter, "priority", Priority, file_path)
64
+ cls._parse_enum_field(frontmatter, "status", Status, file_path)
65
+ cls._parse_enum_field(frontmatter, "issue_type", IssueType, file_path)
66
+
67
+ # Ensure list fields exist
68
+ cls._ensure_list_fields(frontmatter)
69
+
70
+ # Map legacy "description" field to "content" if present
71
+ if "description" in frontmatter and "content" not in frontmatter:
72
+ frontmatter["content"] = frontmatter.pop("description")
73
+ else:
74
+ # Otherwise use the body content
75
+ frontmatter["content"] = content
76
+
77
+ # If headline is not provided, use first line of content as fallback
78
+ if "headline" not in frontmatter or not frontmatter["headline"]:
79
+ first_line = content.split("\n")[0] if content else ""
80
+ frontmatter["headline"] = first_line[:100] # Limit to 100 chars
81
+
82
+ return Issue(**frontmatter)
83
+
84
+ @classmethod
85
+ def save_issue_file(
86
+ cls, issue: Issue, file_path: Path, sync_metadata: dict | None = None
87
+ ) -> None:
88
+ """Save an Issue object to a markdown file.
89
+
90
+ Args:
91
+ issue: Issue object to save
92
+ file_path: Path to write the file to
93
+ sync_metadata: Optional sync metadata to include in frontmatter
94
+ """
95
+ frontmatter = issue.model_dump(exclude={"content", "file_path"})
96
+
97
+ # Add sync_metadata if provided
98
+ if sync_metadata:
99
+ FrontmatterParser.update_sync_metadata(frontmatter, sync_metadata)
100
+
101
+ FrontmatterParser.serialize_file(frontmatter, issue.content, file_path)
102
+
103
+ @classmethod
104
+ def load_sync_metadata(cls, file_path: Path) -> dict | None:
105
+ """Load sync_metadata from an issue file without parsing the full issue.
106
+
107
+ Args:
108
+ file_path: Path to the issue file
109
+
110
+ Returns:
111
+ Sync metadata dictionary or None if not present
112
+ """
113
+ frontmatter, _ = FrontmatterParser.parse_file(file_path)
114
+ return FrontmatterParser.extract_sync_metadata(frontmatter)
115
+
116
+ @classmethod
117
+ def update_issue_sync_metadata(cls, file_path: Path, sync_metadata: dict) -> None:
118
+ """Update only the sync_metadata in an issue file without parsing the full Issue.
119
+
120
+ This is useful for updating sync state without deserializing and re-serializing
121
+ the entire Issue object.
122
+
123
+ Args:
124
+ file_path: Path to the issue file
125
+ sync_metadata: Sync metadata to set
126
+ """
127
+ frontmatter, content = FrontmatterParser.parse_file(file_path)
128
+ FrontmatterParser.update_sync_metadata(frontmatter, sync_metadata)
129
+ FrontmatterParser.serialize_file(frontmatter, content, file_path)
130
+
131
+ @classmethod
132
+ def _validate_enum_field(
133
+ cls, field_name: str, value: str | None, enum_class
134
+ ) -> tuple[bool, Any, str | None]:
135
+ """Validate and convert enum field. Returns (success, value, error_message)."""
136
+ if not value:
137
+ return True, None, None
138
+
139
+ try:
140
+ return True, enum_class(value), None
141
+ except ValueError:
142
+ valid_values = [e.value for e in enum_class]
143
+ error_msg = (
144
+ f"Invalid {field_name}: {value}. Valid: {', '.join(valid_values)}"
145
+ )
146
+ return False, None, error_msg
147
+
148
+ @classmethod
149
+ def _extract_issue_dates(
150
+ cls, data: dict
151
+ ) -> tuple[datetime, datetime, datetime | None]:
152
+ """Extract and parse date fields from data."""
153
+ now = now_utc()
154
+ created = cls._parse_datetime(data.get("created")) or now
155
+ updated = cls._parse_datetime(data.get("updated")) or now
156
+ due_date = cls._parse_datetime(data.get("due_date"))
157
+ return created, updated, due_date
158
+
159
+ @classmethod
160
+ def _build_issue_from_data(
161
+ cls, data: dict, file_path: Path
162
+ ) -> tuple[bool, Issue | None, str | None]:
163
+ """Build Issue object from validated data. Returns (success, issue, error_message)."""
164
+ # Validate priority
165
+ success, priority, error = cls._validate_enum_field(
166
+ "priority", data.get("priority"), Priority
167
+ )
168
+ if not success:
169
+ return False, None, f"{error} in {file_path}"
170
+ priority = priority or Priority.MEDIUM
171
+
172
+ # Validate status
173
+ success, status, error = cls._validate_enum_field(
174
+ "status", data.get("status"), Status
175
+ )
176
+ if not success:
177
+ return False, None, f"{error} in {file_path}"
178
+ status = status or Status.TODO
179
+
180
+ # Validate issue_type
181
+ success, _, error = cls._validate_enum_field(
182
+ "issue_type", data.get("issue_type"), IssueType
183
+ )
184
+ if not success:
185
+ return False, None, f"{error} in {file_path}"
186
+
187
+ # Extract dates
188
+ created, updated, due_date = cls._extract_issue_dates(data)
189
+
190
+ try:
191
+ issue = Issue(
192
+ id=data.get("id") or str(uuid.uuid4())[:8],
193
+ title=data.get("title") or "Untitled Issue",
194
+ priority=priority,
195
+ status=status,
196
+ assignee=data.get("assignee"),
197
+ milestone=data.get("milestone"),
198
+ labels=data.get("labels", []),
199
+ created=created,
200
+ updated=updated,
201
+ due_date=due_date,
202
+ content=data.get("content", ""),
203
+ )
204
+ return True, issue, None
205
+ except Exception as e:
206
+ return False, None, f"Error creating Issue: {e}"
207
+
208
+ @classmethod
209
+ def parse_issue_file_safe(
210
+ cls, file_path: Path
211
+ ) -> tuple[bool, Issue | None, str | None]:
212
+ """Safely parse an issue file with enhanced validation and recovery.
213
+
214
+ Returns:
215
+ (success, issue, error_message)
216
+ """
217
+ is_valid, result = enhanced_persistence.safe_load_with_validation(
218
+ file_path, "issue"
219
+ )
220
+
221
+ if not is_valid:
222
+ # result is a string error message
223
+ return False, None, str(result)
224
+
225
+ try:
226
+ # Convert the validated data to an Issue
227
+ # At this point, result must be a dict since is_valid is True
228
+ data = result if isinstance(result, dict) else {}
229
+ return cls._build_issue_from_data(data, file_path)
230
+ except Exception as e:
231
+ return False, None, f"Error processing issue file: {e}"
232
+
233
+ @classmethod
234
+ def save_issue_file_safe(cls, issue: Issue, file_path: Path) -> tuple[bool, str]:
235
+ """Safely save an issue file with automatic backup."""
236
+ try:
237
+ cls.save_issue_file(issue, file_path)
238
+ return True, "Issue saved successfully"
239
+ except Exception as e:
240
+ return False, f"Error saving issue: {e}"
241
+
242
+ @classmethod
243
+ def _parse_datetime(cls, value: Any) -> datetime | None:
244
+ """Parse datetime from various formats with timezone awareness."""
245
+ return parse_datetime(value, source_type="file", assumed_timezone="UTC")
@@ -0,0 +1,146 @@
1
+ """Parser for milestone markdown files."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from roadmap.common.datetime_parser import parse_datetime
8
+ from roadmap.common.utils.timezone_utils import now_utc
9
+ from roadmap.core.domain import Milestone, MilestoneStatus
10
+
11
+ from ..persistence import enhanced_persistence
12
+ from .frontmatter import FrontmatterParser
13
+
14
+
15
+ class MilestoneParser:
16
+ """Parser specifically for milestone markdown files."""
17
+
18
+ @classmethod
19
+ def parse_milestone_file(cls, file_path: Path) -> Milestone:
20
+ """Parse a milestone markdown file and return a Milestone object."""
21
+ frontmatter, content = FrontmatterParser.parse_file(file_path)
22
+
23
+ # Convert string dates back to datetime objects
24
+ for date_field in ["created", "updated", "due_date"]:
25
+ if date_field in frontmatter and isinstance(frontmatter[date_field], str):
26
+ frontmatter[date_field] = parse_datetime(
27
+ frontmatter[date_field], "file"
28
+ )
29
+
30
+ # Backward-compatibility: some historical files may be missing `name`.
31
+ # Use filename stem so list/view commands remain consistent with disk state.
32
+ if not frontmatter.get("name"):
33
+ frontmatter["name"] = file_path.stem
34
+
35
+ # Convert string enums back to enum objects with validation
36
+ if "status" in frontmatter:
37
+ try:
38
+ frontmatter["status"] = MilestoneStatus(frontmatter["status"])
39
+ except ValueError as e:
40
+ valid_statuses = [s.value for s in MilestoneStatus]
41
+ raise ValueError(
42
+ f"Invalid status '{frontmatter['status']}' in {file_path}. "
43
+ f"Valid status values are: {', '.join(valid_statuses)}"
44
+ ) from e
45
+
46
+ # Map legacy "description" field to "content" if present
47
+ if "description" in frontmatter and "content" not in frontmatter:
48
+ frontmatter["content"] = frontmatter.pop("description")
49
+ else:
50
+ # Otherwise use the body content
51
+ frontmatter["content"] = content
52
+
53
+ # If headline is not provided, use first line of content as fallback
54
+ if "headline" not in frontmatter or not frontmatter["headline"]:
55
+ first_line = content.split("\n")[0] if content else ""
56
+ frontmatter["headline"] = first_line[:100] # Limit to 100 chars
57
+
58
+ return Milestone(**frontmatter)
59
+
60
+ @classmethod
61
+ def save_milestone_file(cls, milestone: Milestone, file_path: Path) -> None:
62
+ """Save a Milestone object to a markdown file."""
63
+ frontmatter = milestone.model_dump(exclude={"content", "file_path"})
64
+ FrontmatterParser.serialize_file(frontmatter, milestone.content, file_path)
65
+
66
+ @classmethod
67
+ def _build_milestone_from_data(
68
+ cls, data: dict, file_path: Path
69
+ ) -> tuple[bool, Milestone | None, str | None]:
70
+ """Build Milestone object from parsed data.
71
+
72
+ Args:
73
+ data: Parsed milestone data
74
+ file_path: Path to milestone file (for error reporting)
75
+
76
+ Returns:
77
+ (success, milestone, error_message)
78
+ """
79
+ try:
80
+ # Handle datetime fields - set defaults if None
81
+ now = now_utc()
82
+ created = cls._parse_datetime(data.get("created")) or now
83
+ updated = cls._parse_datetime(data.get("updated")) or now
84
+ due_date = cls._parse_datetime(data.get("due_date"))
85
+
86
+ milestone = Milestone(
87
+ name=data.get("name") or "Untitled Milestone",
88
+ content=data.get("description", ""),
89
+ status=MilestoneStatus(data.get("status"))
90
+ if data.get("status")
91
+ else MilestoneStatus.OPEN,
92
+ created=created,
93
+ updated=updated,
94
+ due_date=due_date,
95
+ )
96
+ return True, milestone, None
97
+ except ValueError as e:
98
+ # Check if it's an enum validation error
99
+ if "status" in str(e):
100
+ valid_statuses = [s.value for s in MilestoneStatus]
101
+ return (
102
+ False,
103
+ None,
104
+ f"Invalid status in {file_path}: {e}. Valid values: {', '.join(valid_statuses)}",
105
+ )
106
+ return False, None, f"Error creating Milestone object: {e}"
107
+ except Exception as e:
108
+ return False, None, f"Error creating Milestone object: {e}"
109
+
110
+ @classmethod
111
+ def parse_milestone_file_safe(
112
+ cls, file_path: Path
113
+ ) -> tuple[bool, Milestone | None, str | None]:
114
+ """Safely parse a milestone file with enhanced validation and recovery.
115
+
116
+ Returns:
117
+ (success, milestone, error_message)
118
+ """
119
+ is_valid, result = enhanced_persistence.safe_load_with_validation(
120
+ file_path, "milestone"
121
+ )
122
+
123
+ if not is_valid:
124
+ # result is a string error message
125
+ return False, None, str(result)
126
+
127
+ # Convert the validated data to a Milestone
128
+ # At this point, result must be a dict since is_valid is True
129
+ data = result if isinstance(result, dict) else {}
130
+ return cls._build_milestone_from_data(data, file_path)
131
+
132
+ @classmethod
133
+ def save_milestone_file_safe(
134
+ cls, milestone: Milestone, file_path: Path
135
+ ) -> tuple[bool, str]:
136
+ """Safely save a milestone file with automatic backup."""
137
+ try:
138
+ cls.save_milestone_file(milestone, file_path)
139
+ return True, "Milestone saved successfully"
140
+ except Exception as e:
141
+ return False, f"Error saving milestone: {e}"
142
+
143
+ @classmethod
144
+ def _parse_datetime(cls, value: Any) -> datetime | None:
145
+ """Parse datetime from various formats with timezone awareness."""
146
+ return parse_datetime(value, source_type="file", assumed_timezone="UTC")
@@ -0,0 +1,106 @@
1
+ """Parser for project markdown files."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from roadmap.common.datetime_parser import parse_datetime
8
+ from roadmap.common.utils.timezone_utils import now_utc
9
+ from roadmap.core.domain import Priority, Project, ProjectStatus
10
+
11
+ from .frontmatter import FrontmatterParser
12
+
13
+
14
+ class ProjectParser:
15
+ """Parser for project markdown files."""
16
+
17
+ @classmethod
18
+ def parse_project_file(cls, file_path: Path) -> Project:
19
+ """Parse a project file and return a Project instance."""
20
+ frontmatter, content = FrontmatterParser.parse_file(file_path)
21
+
22
+ # Extract ID from frontmatter, or from filename if not present
23
+ project_id = frontmatter.get("id", "")
24
+ if not project_id:
25
+ # Extract ID from filename (format: {id}-{name}.md)
26
+ filename_stem = file_path.stem
27
+ if "-" in filename_stem:
28
+ project_id = filename_stem.split("-", 1)[0]
29
+
30
+ # Convert frontmatter to Project model
31
+ project = Project(
32
+ id=project_id,
33
+ name=frontmatter.get("name", ""),
34
+ headline=frontmatter.get("headline", ""),
35
+ status=ProjectStatus(frontmatter.get("status", "planning")),
36
+ priority=Priority(frontmatter.get("priority", "medium")),
37
+ owner=frontmatter.get("owner"),
38
+ start_date=cls._parse_datetime(frontmatter.get("start_date")),
39
+ target_end_date=cls._parse_datetime(frontmatter.get("target_end_date")),
40
+ actual_end_date=cls._parse_datetime(frontmatter.get("actual_end_date")),
41
+ created=cls._parse_datetime(frontmatter.get("created")) or now_utc(),
42
+ updated=cls._parse_datetime(frontmatter.get("updated")) or now_utc(),
43
+ milestones=frontmatter.get("milestones", []),
44
+ estimated_hours=frontmatter.get("estimated_hours"),
45
+ actual_hours=frontmatter.get("actual_hours"),
46
+ content=content,
47
+ calculated_progress=frontmatter.get("calculated_progress"),
48
+ last_progress_update=cls._parse_datetime(
49
+ frontmatter.get("last_progress_update")
50
+ ),
51
+ projected_end_date=cls._parse_datetime(
52
+ frontmatter.get("projected_end_date")
53
+ ),
54
+ schedule_variance=frontmatter.get("schedule_variance"),
55
+ completion_velocity=frontmatter.get("completion_velocity"),
56
+ risk_level=frontmatter.get("risk_level", "low"),
57
+ )
58
+
59
+ return project
60
+
61
+ @classmethod
62
+ def save_project_file(cls, project: Project, file_path: Path) -> None:
63
+ """Save a project to a markdown file."""
64
+ # Create frontmatter from project data
65
+ frontmatter = {
66
+ "id": project.id,
67
+ "name": project.name,
68
+ "headline": project.headline,
69
+ "status": project.status.value,
70
+ "priority": project.priority.value,
71
+ "owner": project.owner,
72
+ "start_date": project.start_date.isoformat()
73
+ if project.start_date
74
+ else None,
75
+ "target_end_date": project.target_end_date.isoformat()
76
+ if project.target_end_date
77
+ else None,
78
+ "actual_end_date": project.actual_end_date.isoformat()
79
+ if project.actual_end_date
80
+ else None,
81
+ "created": project.created.isoformat(),
82
+ "updated": project.updated.isoformat(),
83
+ "milestones": project.milestones,
84
+ "estimated_hours": project.estimated_hours,
85
+ "actual_hours": project.actual_hours,
86
+ "calculated_progress": project.calculated_progress,
87
+ "last_progress_update": project.last_progress_update.isoformat()
88
+ if project.last_progress_update
89
+ else None,
90
+ "projected_end_date": project.projected_end_date.isoformat()
91
+ if project.projected_end_date
92
+ else None,
93
+ "schedule_variance": project.schedule_variance,
94
+ "completion_velocity": project.completion_velocity,
95
+ "risk_level": project.risk_level.value,
96
+ }
97
+
98
+ # Remove None values
99
+ frontmatter = {k: v for k, v in frontmatter.items() if v is not None}
100
+
101
+ FrontmatterParser.serialize_file(frontmatter, project.content, file_path)
102
+
103
+ @classmethod
104
+ def _parse_datetime(cls, value: Any) -> datetime | None:
105
+ """Parse datetime from various formats with timezone awareness."""
106
+ return parse_datetime(value, source_type="file", assumed_timezone="UTC")