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,348 @@
1
+ """File locking mechanism for roadmap YAML files."""
2
+
3
+ import errno
4
+ import fcntl
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import time
9
+ from contextlib import AbstractContextManager
10
+ from datetime import UTC, datetime, timedelta
11
+ from pathlib import Path
12
+ from threading import Lock
13
+ from typing import Any
14
+
15
+ from structlog import get_logger
16
+
17
+ from roadmap.common.utils.file_utils import ensure_directory_exists
18
+
19
+ logger = get_logger()
20
+
21
+
22
+ class FileLockError(Exception):
23
+ """Exception raised for file locking errors."""
24
+
25
+ pass
26
+
27
+
28
+ class FileLockTimeout(FileLockError):
29
+ """Exception raised when file lock times out."""
30
+
31
+ pass
32
+
33
+
34
+ class FileLock:
35
+ """File lock implementation using fcntl (Unix/Linux/macOS) or msvcrt (Windows)."""
36
+
37
+ def __init__(
38
+ self, file_path: Path, timeout: float = 30.0, check_interval: float = 0.1
39
+ ):
40
+ """Initialize FileLock.
41
+
42
+ Args:
43
+ file_path: Path to file to lock.
44
+ timeout: Lock acquisition timeout in seconds.
45
+ check_interval: Interval in seconds between lock checks.
46
+ """
47
+ self.file_path = file_path
48
+ self.lock_path = file_path.with_suffix(file_path.suffix + ".lock")
49
+ self.timeout = timeout
50
+ self.check_interval = check_interval
51
+ self.lock_file = None
52
+ self.acquired = False
53
+
54
+ # Lock metadata
55
+ self.lock_info = {
56
+ "pid": os.getpid(),
57
+ "timestamp": datetime.now(UTC).isoformat(),
58
+ "file": str(file_path),
59
+ "host": os.uname().nodename if hasattr(os, "uname") else "unknown",
60
+ }
61
+
62
+ def __enter__(self):
63
+ """Context manager entry."""
64
+ self.acquire()
65
+ return self
66
+
67
+ def __exit__(self, _exc_type, _exc_val, _exc_tb):
68
+ """Context manager exit."""
69
+ self.release()
70
+
71
+ def acquire(self) -> bool:
72
+ """Acquire the file lock."""
73
+ if self.acquired:
74
+ return True
75
+
76
+ start_time = time.time()
77
+
78
+ while time.time() - start_time < self.timeout:
79
+ try:
80
+ # Create lock file
81
+ self.lock_file = open(self.lock_path, "w")
82
+
83
+ # Try to acquire exclusive lock
84
+ fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
85
+
86
+ # Write lock metadata
87
+ json.dump(self.lock_info, self.lock_file, indent=2)
88
+ self.lock_file.flush()
89
+
90
+ self.acquired = True
91
+ return True
92
+
93
+ except OSError as e:
94
+ if e.errno in (errno.EAGAIN, errno.EACCES):
95
+ # Lock is already held, wait and retry
96
+ if self.lock_file:
97
+ self.lock_file.close()
98
+ self.lock_file = None
99
+ time.sleep(self.check_interval)
100
+ continue
101
+ else:
102
+ # Other error
103
+ if self.lock_file:
104
+ self.lock_file.close()
105
+ self.lock_file = None
106
+ raise FileLockError(f"Failed to acquire lock: {e}") from e
107
+
108
+ # Timeout reached
109
+ if self.lock_file:
110
+ self.lock_file.close()
111
+ self.lock_file = None
112
+ raise FileLockTimeout(f"Failed to acquire lock within {self.timeout} seconds")
113
+
114
+ def release(self) -> bool:
115
+ """Release the file lock."""
116
+ if not self.acquired:
117
+ return True
118
+
119
+ try:
120
+ if self.lock_file:
121
+ fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
122
+ self.lock_file.close()
123
+ self.lock_file = None
124
+
125
+ # Remove lock file
126
+ if self.lock_path.exists():
127
+ self.lock_path.unlink()
128
+
129
+ self.acquired = False
130
+ return True
131
+
132
+ except Exception as e:
133
+ raise FileLockError(f"Failed to release lock: {e}") from e
134
+
135
+ def is_locked(self) -> bool:
136
+ """Check if the file is currently locked."""
137
+ if not self.lock_path.exists():
138
+ return False
139
+
140
+ try:
141
+ # Try to open and lock the file
142
+ with open(self.lock_path) as f:
143
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
144
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
145
+ return False # No lock held
146
+ except OSError as e:
147
+ logger.debug("lock_check_failed", error=str(e))
148
+ return True # Lock is held
149
+
150
+ def get_lock_info(self) -> dict[str, Any] | None:
151
+ """Get information about the current lock holder."""
152
+ if not self.lock_path.exists():
153
+ return None
154
+
155
+ try:
156
+ with open(self.lock_path) as f:
157
+ return json.load(f)
158
+ except (OSError, json.JSONDecodeError):
159
+ return None
160
+
161
+ def force_unlock(self) -> bool:
162
+ """Force unlock by removing the lock file (use with caution)."""
163
+ try:
164
+ if self.lock_path.exists():
165
+ self.lock_path.unlink()
166
+ return True
167
+ except Exception as e:
168
+ logger.error("force_unlock_failed", error=str(e), severity="system_error")
169
+ return False
170
+
171
+
172
+ class LockManager:
173
+ """Manager for handling multiple file locks and lock policies."""
174
+
175
+ def __init__(self, default_timeout: float = 30.0):
176
+ """Initialize LockManager.
177
+
178
+ Args:
179
+ default_timeout: Default timeout for lock acquisition in seconds.
180
+ """
181
+ self.default_timeout = default_timeout
182
+ self.active_locks: dict[str, FileLock] = {}
183
+ self._global_lock = Lock() # Thread safety
184
+
185
+ def lock_file(
186
+ self, file_path: Path, timeout: float | None = None
187
+ ) -> AbstractContextManager[FileLock]:
188
+ """Get a context manager for file locking."""
189
+ timeout = timeout or self.default_timeout
190
+ file_str = str(file_path)
191
+
192
+ with self._global_lock:
193
+ if file_str in self.active_locks:
194
+ # Reuse existing lock
195
+ return self.active_locks[file_str]
196
+
197
+ # Create new lock
198
+ lock = FileLock(file_path, timeout)
199
+ self.active_locks[file_str] = lock
200
+ return lock
201
+
202
+ def is_file_locked(self, file_path: Path) -> bool:
203
+ """Check if a file is currently locked."""
204
+ lock = FileLock(file_path)
205
+ return lock.is_locked()
206
+
207
+ def get_lock_info(self, file_path: Path) -> dict[str, Any] | None:
208
+ """Get lock information for a file."""
209
+ lock = FileLock(file_path)
210
+ return lock.get_lock_info()
211
+
212
+ def force_unlock_file(self, file_path: Path) -> bool:
213
+ """Force unlock a file (use with extreme caution)."""
214
+ lock = FileLock(file_path)
215
+ return lock.force_unlock()
216
+
217
+ def cleanup_stale_locks(self, max_age_hours: float = 24.0) -> int:
218
+ """Clean up stale lock files older than max_age_hours."""
219
+ cleaned = 0
220
+ max_age = timedelta(hours=max_age_hours)
221
+ current_time = datetime.now(UTC)
222
+
223
+ # Find all .lock files
224
+ for lock_file in Path(".").rglob("*.lock"):
225
+ try:
226
+ # Check file age
227
+ file_time = datetime.fromtimestamp(lock_file.stat().st_mtime)
228
+ if current_time - file_time > max_age:
229
+ # Check if the lock is actually stale
230
+ lock = FileLock(lock_file.with_suffix(""))
231
+ if not lock.is_locked():
232
+ lock_file.unlink()
233
+ cleaned += 1
234
+ except Exception as e:
235
+ logger.debug(
236
+ "stale_lock_cleanup_failed", lock_file=str(lock_file), error=str(e)
237
+ )
238
+ continue
239
+
240
+ return cleaned
241
+
242
+ def get_all_locks(
243
+ self, search_dir: Path | None = None
244
+ ) -> dict[str, dict[str, Any]]:
245
+ """Get information about all current locks."""
246
+ locks = {}
247
+ search_path = search_dir or Path(".")
248
+
249
+ for lock_file in search_path.rglob("*.lock"):
250
+ try:
251
+ original_file = lock_file.with_suffix("")
252
+ lock = FileLock(original_file)
253
+ lock_info = lock.get_lock_info()
254
+ if lock_info:
255
+ locks[str(original_file)] = lock_info
256
+ except Exception as e:
257
+ logger.debug(
258
+ "lock_info_retrieval_failed", lock_file=str(lock_file), error=str(e)
259
+ )
260
+ continue
261
+
262
+ return locks
263
+
264
+
265
+ class LockedFileOperations:
266
+ """File operations with automatic locking."""
267
+
268
+ def __init__(self, lock_manager: LockManager | None = None):
269
+ """Initialize LockedFileOperations.
270
+
271
+ Args:
272
+ lock_manager: LockManager instance. Defaults to new LockManager.
273
+ """
274
+ self.lock_manager = lock_manager or LockManager()
275
+
276
+ def read_file_locked(self, file_path: Path, timeout: float | None = None) -> str:
277
+ """Read a file with locking."""
278
+ with self.lock_manager.lock_file(file_path, timeout):
279
+ return file_path.read_text(encoding="utf-8")
280
+
281
+ def write_file_locked(
282
+ self,
283
+ file_path: Path,
284
+ content: str,
285
+ timeout: float | None = None,
286
+ backup: bool = True,
287
+ ) -> bool:
288
+ """Write a file with locking and optional backup."""
289
+ with self.lock_manager.lock_file(file_path, timeout):
290
+ try:
291
+ # Create backup if requested and file exists
292
+ if backup and file_path.exists():
293
+ backup_path = file_path.with_suffix(f".backup.{int(time.time())}")
294
+ backup_path.write_text(
295
+ file_path.read_text(encoding="utf-8"), encoding="utf-8"
296
+ )
297
+
298
+ # Ensure directory exists
299
+ ensure_directory_exists(file_path.parent)
300
+
301
+ # Write to temporary file first for atomic operation
302
+ with tempfile.NamedTemporaryFile(
303
+ mode="w",
304
+ encoding="utf-8",
305
+ dir=file_path.parent,
306
+ prefix=f".{file_path.name}.",
307
+ suffix=".tmp",
308
+ delete=False,
309
+ ) as tmp_file:
310
+ tmp_file.write(content)
311
+ tmp_path = Path(tmp_file.name)
312
+
313
+ # Atomic move
314
+ tmp_path.replace(file_path)
315
+ return True
316
+
317
+ except Exception as e:
318
+ # Clean up temp file if it exists
319
+ if "tmp_path" in locals() and tmp_path.exists():
320
+ tmp_path.unlink()
321
+ raise FileLockError(f"Failed to write file: {e}") from e
322
+
323
+ def update_file_locked(
324
+ self, file_path: Path, updater_func, timeout: float | None = None
325
+ ) -> Any:
326
+ """Update a file using an updater function with locking."""
327
+ with self.lock_manager.lock_file(file_path, timeout):
328
+ # Read current content
329
+ if file_path.exists():
330
+ content = file_path.read_text(encoding="utf-8")
331
+ else:
332
+ content = ""
333
+
334
+ # Apply updater function
335
+ new_content = updater_func(content)
336
+
337
+ # Write back if changed
338
+ if new_content != content:
339
+ self.write_file_locked(
340
+ file_path, new_content, timeout=None, backup=True
341
+ )
342
+
343
+ return new_content
344
+
345
+
346
+ # Global instances
347
+ lock_manager = LockManager()
348
+ locked_file_ops = LockedFileOperations(lock_manager)
@@ -0,0 +1,92 @@
1
+ """YAML file parsing for .roadmap markdown files."""
2
+
3
+ import hashlib
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from roadmap.common.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class FileParser:
16
+ """Handles parsing and hashing of markdown files with YAML frontmatter."""
17
+
18
+ @staticmethod
19
+ def calculate_file_hash(file_path: Path) -> str:
20
+ """Calculate SHA-256 hash of file content."""
21
+ try:
22
+ with open(file_path, "rb") as f:
23
+ return hashlib.sha256(f.read()).hexdigest()
24
+ except Exception as e:
25
+ logger.warning(
26
+ "failed_to_calculate_hash",
27
+ file_path=str(file_path),
28
+ error=str(e),
29
+ severity="operational",
30
+ )
31
+ return ""
32
+
33
+ @staticmethod
34
+ def parse_yaml_frontmatter(file_path: Path) -> dict[str, Any]:
35
+ """Parse YAML frontmatter from markdown file.
36
+
37
+ Expects format:
38
+ ---
39
+ key: value
40
+ ---
41
+ # Markdown content
42
+ """
43
+ try:
44
+ with open(file_path, encoding="utf-8") as f:
45
+ content = f.read()
46
+
47
+ # Check for frontmatter delimiters
48
+ if not content.startswith("---\n"):
49
+ return {}
50
+
51
+ # Find the end of frontmatter
52
+ try:
53
+ end_marker = content.index("\n---\n", 4)
54
+ frontmatter = content[4:end_marker]
55
+ loaded = yaml.safe_load(frontmatter)
56
+ return loaded if isinstance(loaded, dict) else {}
57
+ except ValueError:
58
+ # No end marker found, treat entire file as YAML
59
+ loaded = yaml.safe_load(content)
60
+ return loaded if isinstance(loaded, dict) else {}
61
+
62
+ except Exception as e:
63
+ logger.error(
64
+ "failed_to_parse_yaml",
65
+ file_path=str(file_path),
66
+ error=str(e),
67
+ severity="data_error",
68
+ )
69
+ return {}
70
+
71
+ @staticmethod
72
+ def extract_file_metadata(file_path: Path) -> dict[str, Any]:
73
+ """Extract file metadata without parsing YAML.
74
+
75
+ Returns:
76
+ Dictionary with 'hash', 'size', and 'modified_time' keys
77
+ """
78
+ try:
79
+ file_stat = file_path.stat()
80
+ return {
81
+ "hash": FileParser.calculate_file_hash(file_path),
82
+ "size": file_stat.st_size,
83
+ "modified_time": datetime.fromtimestamp(file_stat.st_mtime),
84
+ }
85
+ except Exception as e:
86
+ logger.error(
87
+ "failed_to_extract_metadata",
88
+ file_path=str(file_path),
89
+ error=str(e),
90
+ severity="data_error",
91
+ )
92
+ return {}
@@ -0,0 +1,107 @@
1
+ """File synchronization manager for syncing .roadmap files to database.
2
+
3
+ This module coordinates synchronization of markdown files in the .roadmap directory
4
+ with the SQLite database. It delegates to focused managers for parsing, sync state,
5
+ and entity-specific coordination.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from roadmap.adapters.persistence.file_parser import FileParser
12
+ from roadmap.adapters.persistence.sync_orchestrator import SyncOrchestrator
13
+ from roadmap.common.logging import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class FileSynchronizer:
19
+ """Manages synchronization of .roadmap files to the database.
20
+
21
+ Delegates to specialized managers for parsing, sync state tracking,
22
+ and entity-specific synchronization coordination.
23
+ """
24
+
25
+ def __init__(self, get_connection, transaction_context):
26
+ """Initialize the file synchronizer.
27
+
28
+ Args:
29
+ get_connection: Callable that returns a database connection
30
+ transaction_context: Context manager for transactions
31
+ """
32
+ self._orchestrator = SyncOrchestrator(get_connection, transaction_context)
33
+ self._parser = FileParser()
34
+ self._get_connection = get_connection
35
+ self._transaction = transaction_context
36
+
37
+ def get_file_sync_status(self, file_path: str) -> dict[str, Any] | None:
38
+ """Get sync status for a file."""
39
+ conn = self._get_connection()
40
+ row = conn.execute(
41
+ """
42
+ SELECT file_path, content_hash, file_size, last_modified
43
+ FROM file_sync_state WHERE file_path = ?
44
+ """,
45
+ (file_path,),
46
+ ).fetchone()
47
+ return dict(row) if row else None
48
+
49
+ def update_file_sync_status(
50
+ self, file_path: str, content_hash: str, file_size: int, last_modified: Any
51
+ ) -> None:
52
+ """Update sync status for a file."""
53
+ with self._transaction() as conn:
54
+ conn.execute(
55
+ """
56
+ INSERT OR REPLACE INTO file_sync_state
57
+ (file_path, content_hash, file_size, last_modified)
58
+ VALUES (?, ?, ?, ?)
59
+ """,
60
+ (file_path, content_hash, file_size, last_modified),
61
+ )
62
+
63
+ def has_file_changed(self, file_path: Path) -> bool:
64
+ """Check if file has changed since last sync."""
65
+ try:
66
+ if not file_path.exists():
67
+ return True
68
+
69
+ current_hash = self._parser.calculate_file_hash(file_path)
70
+ sync_status = self.get_file_sync_status(str(file_path))
71
+
72
+ if not sync_status:
73
+ return True
74
+
75
+ return current_hash != sync_status["content_hash"]
76
+
77
+ except Exception as e:
78
+ logger.error(
79
+ "failed_to_check_file_changes",
80
+ file_path=str(file_path),
81
+ error=str(e),
82
+ severity="operational",
83
+ )
84
+ return True
85
+
86
+ def sync_directory_incremental(self, roadmap_dir: Path) -> dict[str, Any]:
87
+ """Incrementally sync .roadmap directory to database.
88
+
89
+ Only syncs files that have changed since last sync.
90
+ """
91
+ return self._orchestrator.sync_directory_incremental(roadmap_dir)
92
+
93
+ def full_rebuild_from_git(self, roadmap_dir: Path) -> dict[str, Any]:
94
+ """Full rebuild of database from git files.
95
+
96
+ Clears all data and rebuilds from scratch.
97
+ """
98
+ return self._orchestrator.full_rebuild_from_git(roadmap_dir)
99
+
100
+ def should_do_full_rebuild(self, roadmap_dir: Path, threshold: int = 50) -> bool:
101
+ """Determine if full rebuild is needed vs incremental sync.
102
+
103
+ Args:
104
+ roadmap_dir: Path to .roadmap directory
105
+ threshold: Percentage of files changed before triggering rebuild (default 50%)
106
+ """
107
+ return self._orchestrator.should_do_full_rebuild(roadmap_dir, threshold)
@@ -0,0 +1,140 @@
1
+ """Focused state manager adapters for StateManager.
2
+
3
+ These adapters wrap StateManager to provide focused, single-responsibility
4
+ interfaces. This allows code to depend on focused protocols instead of the
5
+ monolithic StateManager.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from roadmap.core.interfaces import (
11
+ IssueStateManager,
12
+ MilestoneStateManager,
13
+ ProjectStateManager,
14
+ QueryStateManager,
15
+ SyncStateManager,
16
+ )
17
+
18
+
19
+ class FocusedProjectStateManager(ProjectStateManager):
20
+ """Project-focused view of StateManager."""
21
+
22
+ def __init__(self, state_manager):
23
+ """Initialize with StateManager instance."""
24
+ self._state_manager = state_manager
25
+
26
+ def create_project(self, project_data: dict[str, Any]) -> str:
27
+ """Create a new project."""
28
+ return self._state_manager.create_project(project_data)
29
+
30
+ def get_project(self, project_id: str) -> dict[str, Any] | None:
31
+ """Get project by ID."""
32
+ return self._state_manager.get_project(project_id)
33
+
34
+ def list_projects(self) -> list[dict[str, Any]]:
35
+ """List all projects."""
36
+ return self._state_manager.list_projects()
37
+
38
+ def update_project(self, project_id: str, updates: dict[str, Any]) -> bool:
39
+ """Update project."""
40
+ return self._state_manager.update_project(project_id, updates)
41
+
42
+ def delete_project(self, project_id: str) -> bool:
43
+ """Delete project."""
44
+ return self._state_manager.delete_project(project_id)
45
+
46
+ def mark_project_archived(self, project_id: str, archived: bool = True) -> bool:
47
+ """Mark a project as archived or unarchived."""
48
+ return self._state_manager.mark_project_archived(project_id, archived)
49
+
50
+
51
+ class FocusedMilestoneStateManager(MilestoneStateManager):
52
+ """Milestone-focused view of StateManager."""
53
+
54
+ def __init__(self, state_manager):
55
+ """Initialize with StateManager instance."""
56
+ self._state_manager = state_manager
57
+
58
+ def create_milestone(self, milestone_data: dict[str, Any]) -> str:
59
+ """Create a new milestone."""
60
+ return self._state_manager.create_milestone(milestone_data)
61
+
62
+ def get_milestone(self, milestone_id: str) -> dict[str, Any] | None:
63
+ """Get milestone by ID."""
64
+ return self._state_manager.get_milestone(milestone_id)
65
+
66
+ def update_milestone(self, milestone_id: str, updates: dict[str, Any]) -> bool:
67
+ """Update milestone."""
68
+ return self._state_manager.update_milestone(milestone_id, updates)
69
+
70
+ def mark_milestone_archived(self, milestone_id: str, archived: bool = True) -> bool:
71
+ """Mark a milestone as archived or unarchived."""
72
+ return self._state_manager.mark_milestone_archived(milestone_id, archived)
73
+
74
+
75
+ class FocusedIssueStateManager(IssueStateManager):
76
+ """Issue-focused view of StateManager."""
77
+
78
+ def __init__(self, state_manager):
79
+ """Initialize with StateManager instance."""
80
+ self._state_manager = state_manager
81
+
82
+ def create_issue(self, issue_data: dict[str, Any]) -> str:
83
+ """Create a new issue."""
84
+ return self._state_manager.create_issue(issue_data)
85
+
86
+ def get_issue(self, issue_id: str) -> dict[str, Any] | None:
87
+ """Get issue by ID."""
88
+ return self._state_manager.get_issue(issue_id)
89
+
90
+ def update_issue(self, issue_id: str, updates: dict[str, Any]) -> bool:
91
+ """Update issue."""
92
+ return self._state_manager.update_issue(issue_id, updates)
93
+
94
+ def delete_issue(self, issue_id: str) -> bool:
95
+ """Delete issue."""
96
+ return self._state_manager.delete_issue(issue_id)
97
+
98
+ def mark_issue_archived(self, issue_id: str, archived: bool = True) -> bool:
99
+ """Mark an issue as archived or unarchived."""
100
+ return self._state_manager.mark_issue_archived(issue_id, archived)
101
+
102
+
103
+ class FocusedSyncStateManager(SyncStateManager):
104
+ """Sync state-focused view of StateManager."""
105
+
106
+ def __init__(self, state_manager):
107
+ """Initialize with StateManager instance."""
108
+ self._state_manager = state_manager
109
+
110
+ def get_sync_state(self, key: str) -> str | None:
111
+ """Get sync state value."""
112
+ return self._state_manager.get_sync_state(key)
113
+
114
+ def set_sync_state(self, key: str, value: str):
115
+ """Set sync state value."""
116
+ self._state_manager.set_sync_state(key, value)
117
+
118
+
119
+ class FocusedQueryStateManager(QueryStateManager):
120
+ """Query-focused view of StateManager."""
121
+
122
+ def __init__(self, state_manager):
123
+ """Initialize with StateManager instance."""
124
+ self._state_manager = state_manager
125
+
126
+ def get_all_issues(self) -> list[dict[str, Any]]:
127
+ """Get all issues from database."""
128
+ return self._state_manager.get_all_issues()
129
+
130
+ def get_all_milestones(self) -> list[dict[str, Any]]:
131
+ """Get all milestones from database."""
132
+ return self._state_manager.get_all_milestones()
133
+
134
+ def get_milestone_progress(self, milestone_name: str) -> dict[str, int]:
135
+ """Get progress stats for a milestone."""
136
+ return self._state_manager.get_milestone_progress(milestone_name)
137
+
138
+ def get_issues_by_status(self) -> dict[str, int]:
139
+ """Get issue counts by status."""
140
+ return self._state_manager.get_issues_by_status()