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,284 @@
1
+ """Baseline state retrieval using git history and YAML metadata.
2
+
3
+ This module replaces the previous DB-based baseline approach with:
4
+ 1. Local baseline: Retrieved from git history at last_synced timestamp
5
+ 2. Remote baseline: Stored in sync_metadata YAML frontmatter
6
+
7
+ This enables idempotent syncs without complex database state management.
8
+ """
9
+
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from structlog import get_logger
15
+
16
+ from roadmap.core.interfaces.persistence import (
17
+ FileNotFound,
18
+ GitHistoryError,
19
+ PersistenceInterface,
20
+ )
21
+ from roadmap.core.services.sync.sync_state import IssueBaseState
22
+ from roadmap.infrastructure.persistence_gateway import PersistenceGateway
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class BaselineRetrievalError(Exception):
28
+ """Raised when baseline retrieval fails."""
29
+
30
+ pass
31
+
32
+
33
+ class BaselineStateRetriever:
34
+ """Retrieves baseline state from git history and YAML metadata."""
35
+
36
+ def __init__(
37
+ self,
38
+ issues_dir: Path,
39
+ persistence: PersistenceInterface,
40
+ ):
41
+ """Initialize with issues directory and persistence interface.
42
+
43
+ Args:
44
+ issues_dir: Path to issues directory
45
+ persistence: Interface for git history access
46
+ """
47
+ self.issues_dir = issues_dir
48
+ self.persistence = persistence
49
+
50
+ def get_baseline_from_file(self, issue_file: Path) -> IssueBaseState | None:
51
+ """Get baseline state directly from current issue file.
52
+
53
+ Used for initial baseline creation on first sync, where we extract
54
+ the current state directly without looking in git history.
55
+
56
+ Args:
57
+ issue_file: Path to issue file
58
+
59
+ Returns:
60
+ IssueBaseState extracted from file, or None if extraction fails
61
+ """
62
+ try:
63
+ if not issue_file.exists():
64
+ logger.debug(
65
+ "baseline_file_not_found",
66
+ issue_file=str(issue_file),
67
+ )
68
+ return None
69
+
70
+ content = issue_file.read_text(encoding="utf-8")
71
+ return self._extract_baseline_from_content(content, issue_file)
72
+ except Exception as e:
73
+ logger.warning(
74
+ "baseline_from_file_extraction_error",
75
+ issue_file=str(issue_file),
76
+ error=str(e),
77
+ severity="operational",
78
+ )
79
+ return None
80
+
81
+ def get_local_baseline(
82
+ self, issue_file: Path, last_synced: datetime
83
+ ) -> IssueBaseState | None:
84
+ """Get local baseline state from git history.
85
+
86
+ Retrieves the issue file as it existed at the last_synced timestamp,
87
+ then extracts the relevant fields to create a baseline.
88
+
89
+ Args:
90
+ issue_file: Path to issue file
91
+ last_synced: Timestamp of last successful sync
92
+
93
+ Returns:
94
+ IssueBaseState if found, None if file didn't exist at that time
95
+ """
96
+ try:
97
+ # Get file content as it existed at last_synced time
98
+ file_content = self.persistence.get_file_at_timestamp(
99
+ str(issue_file), last_synced
100
+ )
101
+
102
+ if not file_content:
103
+ logger.debug(
104
+ "local_baseline_file_not_found",
105
+ issue_file=str(issue_file),
106
+ last_synced=last_synced.isoformat(),
107
+ )
108
+ return None
109
+
110
+ # Parse the file to extract baseline fields
111
+ baseline = self._extract_baseline_from_content(file_content, issue_file)
112
+
113
+ logger.debug(
114
+ "local_baseline_retrieved",
115
+ issue_file=str(issue_file),
116
+ status=baseline.status if baseline else None,
117
+ )
118
+ return baseline
119
+
120
+ except FileNotFound:
121
+ logger.debug(
122
+ "local_baseline_file_not_in_history",
123
+ issue_file=str(issue_file),
124
+ last_synced=last_synced.isoformat(),
125
+ reason="file_did_not_exist_at_timestamp",
126
+ )
127
+ return None
128
+ except GitHistoryError as e:
129
+ logger.warning(
130
+ "local_baseline_git_error",
131
+ issue_file=str(issue_file),
132
+ error=str(e),
133
+ severity="operational",
134
+ )
135
+ raise BaselineRetrievalError(
136
+ f"Failed to retrieve baseline from git: {e}"
137
+ ) from e
138
+ except Exception as e:
139
+ logger.warning(
140
+ "local_baseline_extraction_error",
141
+ issue_file=str(issue_file),
142
+ error=str(e),
143
+ error_type=type(e).__name__,
144
+ severity="operational",
145
+ )
146
+ return None
147
+
148
+ def get_remote_baseline(self, issue_file: Path) -> IssueBaseState | None:
149
+ """Get remote baseline from sync_metadata in issue file.
150
+
151
+ The sync_metadata YAML frontmatter contains a remote_state snapshot
152
+ that represents the last-synced state from the remote backend.
153
+
154
+ Args:
155
+ issue_file: Path to issue file
156
+
157
+ Returns:
158
+ IssueBaseState extracted from remote_state, or None if not present
159
+ """
160
+ try:
161
+ sync_metadata = PersistenceGateway.load_sync_metadata(issue_file)
162
+
163
+ if not sync_metadata:
164
+ logger.debug(
165
+ "remote_baseline_not_found",
166
+ issue_file=str(issue_file),
167
+ reason="no_sync_metadata",
168
+ )
169
+ return None
170
+
171
+ remote_state = sync_metadata.get("remote_state")
172
+ if not remote_state:
173
+ logger.debug(
174
+ "remote_baseline_not_found",
175
+ issue_file=str(issue_file),
176
+ reason="no_remote_state_in_metadata",
177
+ )
178
+ return None
179
+
180
+ baseline = self._extract_baseline_from_remote_state(
181
+ remote_state, issue_file
182
+ )
183
+
184
+ logger.debug(
185
+ "remote_baseline_retrieved",
186
+ issue_file=str(issue_file),
187
+ status=baseline.status if baseline else None,
188
+ )
189
+ return baseline
190
+
191
+ except Exception as e:
192
+ logger.warning(
193
+ "remote_baseline_extraction_error",
194
+ issue_file=str(issue_file),
195
+ error=str(e),
196
+ error_type=type(e).__name__,
197
+ severity="operational",
198
+ )
199
+ return None
200
+
201
+ def _extract_baseline_from_content(
202
+ self, content: str, issue_file: Path
203
+ ) -> IssueBaseState | None:
204
+ """Extract baseline fields from issue file content.
205
+
206
+ Args:
207
+ content: File content (with YAML frontmatter)
208
+ issue_file: Path to issue file (for logging)
209
+
210
+ Returns:
211
+ IssueBaseState or None if extraction fails
212
+ """
213
+ from roadmap.common.datetime_parser import parse_datetime
214
+
215
+ try:
216
+ frontmatter, _ = PersistenceGateway.parse_frontmatter(content)
217
+
218
+ # Parse updated datetime
219
+ updated_at = datetime.now(UTC)
220
+ if frontmatter.get("updated"):
221
+ updated_value = frontmatter["updated"]
222
+ if isinstance(updated_value, datetime):
223
+ updated_at = updated_value
224
+ else:
225
+ parsed = parse_datetime(str(updated_value), "file")
226
+ if parsed:
227
+ updated_at = parsed
228
+
229
+ return IssueBaseState(
230
+ id=frontmatter.get("id", ""),
231
+ title=frontmatter.get("title", ""),
232
+ status=frontmatter.get("status", "todo"),
233
+ assignee=frontmatter.get("assignee"),
234
+ headline=frontmatter.get("headline", ""),
235
+ content=frontmatter.get("content", ""),
236
+ labels=frontmatter.get("labels", []),
237
+ updated_at=updated_at,
238
+ )
239
+ except Exception as e:
240
+ logger.debug(
241
+ "baseline_extraction_failed",
242
+ issue_file=str(issue_file),
243
+ error=str(e),
244
+ )
245
+ return None
246
+
247
+ def _extract_baseline_from_remote_state(
248
+ self, remote_state: dict[str, Any], issue_file: Path
249
+ ) -> IssueBaseState | None:
250
+ """Extract baseline from remote_state snapshot in sync_metadata.
251
+
252
+ Args:
253
+ remote_state: Remote state dict from sync_metadata
254
+ issue_file: Path to issue file (for logging)
255
+
256
+ Returns:
257
+ IssueBaseState or None if extraction fails
258
+ """
259
+ try:
260
+ # Parse datetime if present
261
+ updated_at = datetime.now(UTC)
262
+ if "updated_at" in remote_state:
263
+ if isinstance(remote_state["updated_at"], str):
264
+ updated_at = datetime.fromisoformat(remote_state["updated_at"])
265
+ else:
266
+ updated_at = remote_state["updated_at"]
267
+
268
+ return IssueBaseState(
269
+ id=remote_state.get("id", ""),
270
+ title=remote_state.get("title", ""),
271
+ status=remote_state.get("status", "open"),
272
+ assignee=remote_state.get("assignee"),
273
+ headline=remote_state.get("headline", ""),
274
+ content=remote_state.get("content", ""),
275
+ labels=remote_state.get("labels", []),
276
+ updated_at=updated_at,
277
+ )
278
+ except Exception as e:
279
+ logger.debug(
280
+ "remote_baseline_extraction_failed",
281
+ issue_file=str(issue_file),
282
+ error=str(e),
283
+ )
284
+ return None
@@ -0,0 +1,425 @@
1
+ """Optimized sync state rebuilding using changed file detection.
2
+
3
+ This module provides intelligent caching strategies for sync state reconstruction,
4
+ using git history to detect which files have actually changed since last baseline.
5
+ This dramatically speeds up the rebuild process for large issue sets.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from structlog import get_logger
11
+
12
+ from roadmap.core.interfaces.persistence import GitHistoryError
13
+ from roadmap.core.services.sync.sync_state import SyncState
14
+ from roadmap.infrastructure.persistence_gateway import PersistenceGateway
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class OptimizedBaselineBuilder:
20
+ """Builds sync state baselines with intelligent caching.
21
+
22
+ Instead of rebuilding the entire sync state from git history, this class:
23
+ 1. Checks which files have changed since a reference point
24
+ 2. Only rebuilds baselines for modified issues
25
+ 3. Reuses cached baseline states for unchanged issues
26
+ 4. Reduces rebuild time from ~50-100ms to ~5-10ms for typical changes
27
+ """
28
+
29
+ def __init__(self, issues_dir: Path):
30
+ """Initialize builder.
31
+
32
+ Args:
33
+ issues_dir: Path to issues directory
34
+ """
35
+ self.issues_dir = issues_dir
36
+ self._progress = None # Optional rich Progress context
37
+
38
+ def set_progress_context(self, progress) -> None:
39
+ """Set optional progress tracker for rebuild operations.
40
+
41
+ Args:
42
+ progress: rich.progress.Progress instance or None
43
+
44
+ Example:
45
+ >>> from rich.progress import Progress
46
+ >>> with Progress() as progress:
47
+ ... builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
48
+ ... builder.set_progress_context(progress)
49
+ ... # Rebuilds will now show progress
50
+ """
51
+ self._progress = progress
52
+
53
+ def _update_progress(
54
+ self,
55
+ description: str,
56
+ completed: int | None = None,
57
+ total: int | None = None,
58
+ ) -> None:
59
+ """Update progress bar if tracking is enabled.
60
+
61
+ Args:
62
+ description: What operation is being performed
63
+ completed: Number of items completed
64
+ total: Total items to complete
65
+
66
+ Internal use only.
67
+ """
68
+ if self._progress is None:
69
+ return
70
+
71
+ try:
72
+ # Update task description if progress tracking is active
73
+ logger.debug(
74
+ "baseline_rebuild_progress",
75
+ description=description,
76
+ completed=completed,
77
+ total=total,
78
+ )
79
+ except Exception as e:
80
+ logger.debug(
81
+ "progress_update_failed",
82
+ operation="report_progress",
83
+ error=str(e),
84
+ action="Continuing without progress update",
85
+ )
86
+ # Silently ignore progress update errors
87
+
88
+ def get_changed_issue_files(
89
+ self,
90
+ git_ref: str = "HEAD~1",
91
+ limit_to_issues: list[str] | None = None,
92
+ ) -> set[str]:
93
+ """Get issue files that changed since a git reference.
94
+
95
+ Args:
96
+ git_ref: Git reference to compare against (default: previous commit)
97
+ limit_to_issues: Optional list of issue IDs to limit detection to
98
+
99
+ Returns:
100
+ Set of issue file paths that changed
101
+
102
+ Examples:
103
+ >>> builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
104
+ >>> changed = builder.get_changed_issue_files("HEAD~1")
105
+ >>> print(changed) # {'.../TASK-123-example.md', ...}
106
+ """
107
+ try:
108
+ changed = PersistenceGateway.get_changed_files_since_commit(
109
+ git_ref, str(self.issues_dir)
110
+ )
111
+
112
+ # Filter to only issue files (*.md)
113
+ issue_files = {f for f in changed if f.endswith(".md")}
114
+
115
+ if limit_to_issues:
116
+ # Filter to specific issues
117
+ limited = {
118
+ f
119
+ for f in issue_files
120
+ if any(issue_id in f for issue_id in limit_to_issues)
121
+ }
122
+ logger.debug(
123
+ "changed_issue_files_filtered",
124
+ total_changed=len(issue_files),
125
+ limited_count=len(limited),
126
+ )
127
+ return limited
128
+
129
+ logger.debug(
130
+ "changed_issue_files_detected",
131
+ count=len(issue_files),
132
+ git_ref=git_ref,
133
+ )
134
+ return issue_files
135
+
136
+ except GitHistoryError as e:
137
+ logger.warning(
138
+ "changed_file_detection_failed",
139
+ error=str(e),
140
+ using_fallback="rebuild_all",
141
+ severity="operational",
142
+ )
143
+ # If git history fails, return empty set to trigger full rebuild
144
+ return set()
145
+
146
+ def should_rebuild_all(
147
+ self,
148
+ cached_state: SyncState | None,
149
+ time_since_last_sync: float | None = None,
150
+ ) -> bool:
151
+ """Determine if full rebuild is necessary.
152
+
153
+ Args:
154
+ cached_state: Previous sync state (if any)
155
+ time_since_last_sync: Seconds elapsed since last sync
156
+
157
+ Returns:
158
+ True if full rebuild recommended, False if incremental is OK
159
+
160
+ Rules:
161
+ - If no cached state: always rebuild
162
+ - If more than 1 hour since last sync: rebuild (safer)
163
+ - If metadata indicates corruption: rebuild
164
+ - Otherwise: incremental update possible
165
+ """
166
+ if not cached_state:
167
+ logger.debug(
168
+ "rebuild_required_reason",
169
+ reason="no_cached_state",
170
+ )
171
+ return True
172
+
173
+ if time_since_last_sync is not None and time_since_last_sync > 3600:
174
+ logger.debug(
175
+ "rebuild_recommended_reason",
176
+ reason="stale_cache_1hour_old",
177
+ seconds_old=time_since_last_sync,
178
+ )
179
+ return True
180
+
181
+ # If we get here, incremental update is safe
182
+ logger.debug(
183
+ "incremental_update_safe",
184
+ cached_issues=len(cached_state.base_issues),
185
+ )
186
+ return False
187
+
188
+ def extract_issue_id_from_path(self, file_path: str) -> str | None:
189
+ """Extract issue ID from file path.
190
+
191
+ Args:
192
+ file_path: Path like 'issues/backlog/TASK-123-example.md'
193
+
194
+ Returns:
195
+ Issue ID like 'TASK-123' or None if not parseable
196
+
197
+ Examples:
198
+ >>> builder.extract_issue_id_from_path("issues/backlog/TASK-123-example.md")
199
+ 'TASK-123'
200
+ >>> builder.extract_issue_id_from_path("invalid.txt")
201
+ None
202
+ """
203
+ try:
204
+ # Extract filename
205
+ filename = Path(file_path).name
206
+ # Remove .md extension
207
+ base = filename.replace(".md", "")
208
+ # Take part before first dash and second dash
209
+ # Format is typically TASK-123-description or similar
210
+ parts = base.split("-")
211
+ if len(parts) >= 2:
212
+ # Check if first part is alphabetic and second is numeric
213
+ if parts[0].isalpha() and parts[1].isdigit():
214
+ # Return TYPE-ID format (e.g., TASK-123)
215
+ issue_id = f"{parts[0]}-{parts[1]}"
216
+ return issue_id
217
+ except Exception as e:
218
+ logger.debug(
219
+ "issue_id_parsing_failed",
220
+ operation="parse_issue_id",
221
+ error=str(e),
222
+ action="Returning None",
223
+ )
224
+ return None
225
+
226
+ def get_issue_files_to_update(
227
+ self,
228
+ all_issue_files: list[Path],
229
+ changed_files: set[str],
230
+ cached_state: SyncState | None,
231
+ ) -> dict[str, Path]:
232
+ """Determine which issue files need baseline updates.
233
+
234
+ Args:
235
+ all_issue_files: All issue file paths
236
+ changed_files: Files that changed since git_ref
237
+ cached_state: Previous sync state (for unchanged issues)
238
+
239
+ Returns:
240
+ Dictionary mapping issue_id to file_path for issues to update
241
+
242
+ Logic:
243
+ - New issues (not in cache): always update
244
+ - Changed files: always update
245
+ - Unchanged files: skip (use cached)
246
+ - Deleted issues: mark for removal
247
+ """
248
+ files_to_update = {}
249
+
250
+ for file_path in all_issue_files:
251
+ relative_path = str(file_path.relative_to(self.issues_dir))
252
+ issue_id = self.extract_issue_id_from_path(relative_path)
253
+
254
+ if not issue_id:
255
+ logger.debug(
256
+ "skipping_unparseable_issue_file",
257
+ file_path=relative_path,
258
+ )
259
+ continue
260
+
261
+ # Check if file changed
262
+ is_changed = any(
263
+ relative_path in f or file_path.name in f for f in changed_files
264
+ )
265
+
266
+ # Check if new (or full rebuild if no cached state)
267
+ is_new = cached_state is None or issue_id not in cached_state.base_issues
268
+
269
+ if is_changed or is_new:
270
+ files_to_update[issue_id] = file_path
271
+ logger.debug(
272
+ "issue_requires_update",
273
+ issue_id=issue_id,
274
+ reason="changed" if is_changed else "new",
275
+ )
276
+ else:
277
+ logger.debug(
278
+ "issue_using_cached_baseline",
279
+ issue_id=issue_id,
280
+ reason="unchanged_file",
281
+ )
282
+
283
+ return files_to_update
284
+
285
+ def get_incremental_update_issues(
286
+ self,
287
+ all_issue_files: list[Path],
288
+ changed_files: set[str],
289
+ cached_state: SyncState,
290
+ ) -> tuple[dict[str, Path], list[str]]:
291
+ """Get issues for incremental baseline update.
292
+
293
+ Args:
294
+ all_issue_files: All current issue files
295
+ changed_files: Files changed since last sync
296
+ cached_state: Previous sync state
297
+
298
+ Returns:
299
+ Tuple of (issues_to_update, issues_to_remove)
300
+ - issues_to_update: {issue_id: file_path} for changed/new issues
301
+ - issues_to_remove: [issue_id] for deleted issues
302
+
303
+ Example:
304
+ >>> files = [Path("issues/backlog/TASK-123-example.md")]
305
+ >>> changed = {"issues/backlog/TASK-123-example.md"}
306
+ >>> updates, removals = builder.get_incremental_update_issues(files, changed, state)
307
+ >>> print(updates) # {"TASK-123": Path(...)}
308
+ >>> print(removals) # []
309
+ """
310
+ # Update progress: analyzing files
311
+ self._update_progress("Analyzing changed files...", 0, len(all_issue_files))
312
+
313
+ # Get issues to update
314
+ issues_to_update = self.get_issue_files_to_update(
315
+ all_issue_files, changed_files, cached_state
316
+ )
317
+
318
+ # Update progress: detecting deletions
319
+ self._update_progress(
320
+ f"Checking for deletions ({len(issues_to_update)} changed)...",
321
+ len(issues_to_update),
322
+ len(all_issue_files),
323
+ )
324
+
325
+ # Find deleted issues (in cache but not in current files)
326
+ current_ids = {self.extract_issue_id_from_path(str(f)) for f in all_issue_files}
327
+ current_ids.discard(None)
328
+
329
+ deleted_ids = [
330
+ issue_id
331
+ for issue_id in cached_state.base_issues.keys()
332
+ if issue_id not in current_ids
333
+ ]
334
+
335
+ if deleted_ids:
336
+ logger.info(
337
+ "detected_deleted_issues",
338
+ count=len(deleted_ids),
339
+ issue_ids=deleted_ids,
340
+ )
341
+
342
+ return issues_to_update, deleted_ids
343
+
344
+ def estimate_rebuild_time(
345
+ self,
346
+ issues_count: int,
347
+ git_operations_per_issue: float = 0.010, # ~10ms per git operation
348
+ parse_overhead: float = 0.001, # ~1ms per file parse
349
+ ) -> float:
350
+ """Estimate time to rebuild baseline state.
351
+
352
+ Args:
353
+ issues_count: Number of issues to rebuild
354
+ git_operations_per_issue: Average time for git operations (seconds)
355
+ parse_overhead: Time to parse each file (seconds)
356
+
357
+ Returns:
358
+ Estimated time in milliseconds
359
+
360
+ Examples:
361
+ >>> builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
362
+ >>> time_ms = builder.estimate_rebuild_time(100)
363
+ >>> print(f"Est. time: {time_ms:.1f}ms") # ~1.1s for 100 issues
364
+ >>> time_ms = builder.estimate_rebuild_time(5)
365
+ >>> print(f"Est. time: {time_ms:.1f}ms") # ~55ms for 5 issues (incremental)
366
+ """
367
+ total_seconds = issues_count * (git_operations_per_issue + parse_overhead)
368
+ return total_seconds * 1000
369
+
370
+
371
+ class CachedBaselineState:
372
+ """Wrapper for baseline state with cache metadata.
373
+
374
+ Tracks whether the state is from cache or freshly rebuilt,
375
+ and provides statistics about the rebuild process.
376
+ """
377
+
378
+ def __init__(
379
+ self,
380
+ state: SyncState,
381
+ from_cache: bool = False,
382
+ rebuilt_issues: int | None = None,
383
+ reused_issues: int | None = None,
384
+ rebuild_time_ms: float | None = None,
385
+ ):
386
+ """Initialize cached baseline state.
387
+
388
+ Args:
389
+ state: The SyncState object
390
+ from_cache: Whether this is from cache (True) or rebuilt (False)
391
+ rebuilt_issues: Count of issues rebuilt
392
+ reused_issues: Count of issues reused from cache
393
+ rebuild_time_ms: Time to rebuild in milliseconds
394
+ """
395
+ self.state = state
396
+ self.from_cache = from_cache
397
+ self.rebuilt_issues = rebuilt_issues or 0
398
+ self.reused_issues = reused_issues or 0
399
+ self.rebuild_time_ms = rebuild_time_ms or 0.0
400
+
401
+ @property
402
+ def is_full_rebuild(self) -> bool:
403
+ """Whether this is a full rebuild (no cache reuse)."""
404
+ return self.reused_issues == 0 and not self.from_cache
405
+
406
+ @property
407
+ def is_incremental(self) -> bool:
408
+ """Whether this is an incremental update (reused some cached issues)."""
409
+ return self.reused_issues > 0
410
+
411
+ def to_dict(self) -> dict:
412
+ """Convert to dictionary for logging.
413
+
414
+ Returns:
415
+ Dictionary with all metadata
416
+ """
417
+ return {
418
+ "from_cache": self.from_cache,
419
+ "is_full_rebuild": self.is_full_rebuild,
420
+ "is_incremental": self.is_incremental,
421
+ "rebuilt_issues": self.rebuilt_issues,
422
+ "reused_issues": self.reused_issues,
423
+ "total_issues": len(self.state.base_issues),
424
+ "rebuild_time_ms": f"{self.rebuild_time_ms:.1f}",
425
+ }