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,460 @@
1
+ """Entity-level health scanning service.
2
+
3
+ Provides detailed diagnostics on individual entities (Issues, Milestones, Projects)
4
+ including validation of descriptions, comments, dependencies, and other metadata.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from roadmap.common.constants import Status
10
+ from roadmap.common.logging import get_logger
11
+ from roadmap.common.observability.instrumentation import traced
12
+ from roadmap.core.domain.comment import Comment
13
+ from roadmap.core.domain.issue import Issue
14
+ from roadmap.core.domain.milestone import Milestone
15
+ from roadmap.core.domain.project import Project
16
+
17
+ # Re-export shared models for backward compatibility
18
+ from roadmap.core.services.health.health_models import (
19
+ EntityHealthReport,
20
+ EntityType,
21
+ HealthIssue,
22
+ HealthSeverity,
23
+ )
24
+ from roadmap.core.services.health.issue_health_scanner import IssueHealthScanner
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ class EntityHealthScanner:
30
+ """Scans entities for health issues.
31
+
32
+ Checks for:
33
+ - Missing or empty descriptions
34
+ - Malformed or empty comments
35
+ - Circular dependencies
36
+ - Broken dependencies (referencing non-existent entities)
37
+ - Missing estimates (when required by milestone)
38
+ - Due date issues (overdue, unrealistic)
39
+ - Assignee issues (unassigned when expected, previous_assignee mismatch)
40
+ - Progress tracking issues (inconsistent progress state)
41
+ """
42
+
43
+ def __init__(self, core: Any = None):
44
+ """Initialize the scanner.
45
+
46
+ Args:
47
+ core: Optional Core object with access to entities. If not provided,
48
+ the scanner can still work on individual entities passed to methods.
49
+ """
50
+ self.core = core
51
+ self._entity_cache: dict[str, Any] = {} # Cache of entity_id -> entity
52
+
53
+ @traced("scan_issue")
54
+ def scan_issue(self, issue: Issue) -> EntityHealthReport:
55
+ """Delegate issue scanning to the extracted `IssueHealthScanner`.
56
+
57
+ This keeps the original public API while the implementation lives in
58
+ `issue_health_scanner.IssueHealthScanner` for easier maintenance.
59
+ """
60
+ scanner = IssueHealthScanner(core=self.core)
61
+ return scanner.scan_issue(issue)
62
+
63
+ @traced("scan_milestone")
64
+ def scan_milestone(self, milestone: Milestone) -> EntityHealthReport:
65
+ """Scan a single milestone for health problems.
66
+
67
+ Args:
68
+ milestone: The milestone to scan
69
+
70
+ Returns:
71
+ EntityHealthReport with all issues found
72
+ """
73
+ report = EntityHealthReport(
74
+ entity_id=milestone.name,
75
+ entity_type=EntityType.MILESTONE,
76
+ entity_title=milestone.name,
77
+ status=milestone.status.value if milestone.status else "unknown",
78
+ )
79
+
80
+ # Check description/content
81
+ if not milestone.content or not milestone.content.strip():
82
+ report.issues.append(
83
+ HealthIssue(
84
+ code="missing_content",
85
+ message="Milestone has no content",
86
+ severity=HealthSeverity.WARNING,
87
+ category="content",
88
+ )
89
+ )
90
+
91
+ # Check dates
92
+ if milestone.created and milestone.due_date:
93
+ if milestone.created > milestone.due_date:
94
+ report.issues.append(
95
+ HealthIssue(
96
+ code="invalid_date_range",
97
+ message=f"Created date ({milestone.created.date()}) is after due date ({milestone.due_date.date()})",
98
+ severity=HealthSeverity.ERROR,
99
+ category="structure",
100
+ details={
101
+ "created_date": milestone.created.isoformat(),
102
+ "due_date": milestone.due_date.isoformat(),
103
+ },
104
+ )
105
+ )
106
+
107
+ # Check progress
108
+ if (
109
+ milestone.calculated_progress is not None
110
+ and milestone.status == Status.CLOSED
111
+ ):
112
+ if milestone.calculated_progress != 100:
113
+ report.issues.append(
114
+ HealthIssue(
115
+ code="inconsistent_completion",
116
+ message=f"Milestone marked Done but only {milestone.calculated_progress}% complete",
117
+ severity=HealthSeverity.WARNING,
118
+ category="structure",
119
+ details={"calculated_progress": milestone.calculated_progress},
120
+ )
121
+ )
122
+
123
+ return report
124
+
125
+ @traced("scan_project")
126
+ def scan_project(self, project: Project) -> EntityHealthReport:
127
+ """Scan a single project for health problems.
128
+
129
+ Args:
130
+ project: The project to scan
131
+
132
+ Returns:
133
+ EntityHealthReport with all issues found
134
+ """
135
+ report = EntityHealthReport(
136
+ entity_id=project.id,
137
+ entity_type=EntityType.PROJECT,
138
+ entity_title=project.name,
139
+ status=project.status.value if project.status else "unknown",
140
+ )
141
+
142
+ # Check description/content
143
+ if not project.content or not project.content.strip():
144
+ report.issues.append(
145
+ HealthIssue(
146
+ code="missing_content",
147
+ message="Project has no content",
148
+ severity=HealthSeverity.WARNING,
149
+ category="content",
150
+ )
151
+ )
152
+
153
+ # Check owner
154
+ if not project.owner or not project.owner.strip():
155
+ report.issues.append(
156
+ HealthIssue(
157
+ code="missing_owner",
158
+ message="Project has no owner assigned",
159
+ severity=HealthSeverity.WARNING,
160
+ category="structure",
161
+ )
162
+ )
163
+
164
+ return report
165
+
166
+ def scan_all(self) -> list[EntityHealthReport]:
167
+ """Scan all issues, milestones, and projects.
168
+
169
+ Requires core to be initialized.
170
+
171
+ Returns:
172
+ List of EntityHealthReport objects for all entities
173
+
174
+ Raises:
175
+ RuntimeError: If core is not initialized
176
+ """
177
+ if not self.core:
178
+ raise RuntimeError(
179
+ "Core must be initialized to scan all entities. "
180
+ "Pass core to constructor or call scan_issue/milestone/project directly."
181
+ )
182
+
183
+ reports = []
184
+
185
+ # Scan all issues
186
+ try:
187
+ all_issues = self._list_entities("issues", "issue_repository")
188
+ for issue in all_issues:
189
+ reports.append(self.scan_issue(issue))
190
+ except Exception as e:
191
+ logger.error("error_scanning_issues", error=str(e), severity="system_error")
192
+
193
+ # Scan all milestones
194
+ try:
195
+ all_milestones = self._list_entities("milestones", "milestone_repository")
196
+ for milestone in all_milestones:
197
+ reports.append(self.scan_milestone(milestone))
198
+ except Exception as e:
199
+ logger.error(
200
+ "error_scanning_milestones", error=str(e), severity="system_error"
201
+ )
202
+
203
+ # Scan all projects
204
+ try:
205
+ all_projects = self._list_entities("projects", "project_repository")
206
+ for project in all_projects:
207
+ reports.append(self.scan_project(project))
208
+ except Exception as e:
209
+ logger.error(
210
+ "error_scanning_projects", error=str(e), severity="system_error"
211
+ )
212
+
213
+ return reports
214
+
215
+ def _list_entities(self, coordinator_attr: str, repository_attr: str) -> list[Any]:
216
+ """List entities using coordinator API first, with legacy fallback.
217
+
218
+ Args:
219
+ coordinator_attr: Coordinator attribute name on core (e.g., "issues")
220
+ repository_attr: Legacy repository attribute name on core
221
+
222
+ Returns:
223
+ List of entities
224
+
225
+ Raises:
226
+ AttributeError: If neither API is available
227
+ """
228
+ coordinator = getattr(self.core, coordinator_attr, None)
229
+ if coordinator is not None and hasattr(coordinator, "list"):
230
+ return coordinator.list()
231
+
232
+ repository = getattr(self.core, repository_attr, None)
233
+ if repository is not None and hasattr(repository, "list"):
234
+ return repository.list()
235
+
236
+ raise AttributeError(
237
+ f"Core has neither '{coordinator_attr}.list()' nor '{repository_attr}.list()'"
238
+ )
239
+
240
+ # Private methods for individual checks
241
+
242
+ def _check_issue_description(self, issue: Issue, report: EntityHealthReport):
243
+ """Check issue description/content."""
244
+ if not issue.content or not issue.content.strip():
245
+ report.issues.append(
246
+ HealthIssue(
247
+ code="missing_description",
248
+ message="Issue has no description or content",
249
+ severity=HealthSeverity.INFO,
250
+ category="content",
251
+ )
252
+ )
253
+
254
+ def _check_issue_comments(self, issue: Issue, report: EntityHealthReport):
255
+ """Check issue comments for problems."""
256
+ if not issue.comments:
257
+ return
258
+
259
+ errors = self._validate_comment_thread(issue.comments)
260
+ for error_msg in errors:
261
+ # Parse error message to determine severity and code
262
+ if "Duplicate" in error_msg:
263
+ code = "duplicate_comment_id"
264
+ severity = HealthSeverity.ERROR
265
+ elif "circular" in error_msg.lower():
266
+ code = "circular_comment_reference"
267
+ severity = HealthSeverity.ERROR
268
+ elif "datetime" in error_msg:
269
+ code = "invalid_comment_datetime"
270
+ severity = HealthSeverity.ERROR
271
+ elif "Empty" in error_msg:
272
+ code = "empty_comment_body"
273
+ severity = HealthSeverity.WARNING
274
+ elif "author" in error_msg.lower():
275
+ code = "missing_comment_author"
276
+ severity = HealthSeverity.WARNING
277
+ else:
278
+ code = "invalid_comment"
279
+ severity = HealthSeverity.WARNING
280
+
281
+ report.issues.append(
282
+ HealthIssue(
283
+ code=code,
284
+ message=error_msg,
285
+ severity=severity,
286
+ category="content",
287
+ details={"comment_count": len(issue.comments)},
288
+ )
289
+ )
290
+
291
+ def _check_issue_estimates(self, issue: Issue, report: EntityHealthReport):
292
+ """Check issue estimation."""
293
+ # Only warn about missing estimates for in-progress or done issues
294
+ if issue.status not in (Status.IN_PROGRESS, Status.CLOSED):
295
+ return
296
+
297
+ if issue.estimated_hours is None:
298
+ report.issues.append(
299
+ HealthIssue(
300
+ code="missing_estimate",
301
+ message=f"Issue is {issue.status.value} but has no time estimate",
302
+ severity=HealthSeverity.INFO,
303
+ category="structure",
304
+ )
305
+ )
306
+ elif issue.estimated_hours <= 0:
307
+ report.issues.append(
308
+ HealthIssue(
309
+ code="invalid_estimate",
310
+ message=f"Estimated hours must be positive, got {issue.estimated_hours}",
311
+ severity=HealthSeverity.WARNING,
312
+ category="structure",
313
+ details={"estimated_hours": issue.estimated_hours},
314
+ )
315
+ )
316
+
317
+ def _check_issue_dates(self, issue: Issue, report: EntityHealthReport):
318
+ """Check issue date consistency."""
319
+ if issue.actual_start_date and issue.actual_end_date:
320
+ if issue.actual_start_date > issue.actual_end_date:
321
+ report.issues.append(
322
+ HealthIssue(
323
+ code="invalid_date_range",
324
+ message="Start date is after end date",
325
+ severity=HealthSeverity.ERROR,
326
+ category="structure",
327
+ details={
328
+ "actual_start_date": issue.actual_start_date.isoformat(),
329
+ "actual_end_date": issue.actual_end_date.isoformat(),
330
+ },
331
+ )
332
+ )
333
+
334
+ if issue.due_date and issue.actual_end_date:
335
+ if issue.actual_end_date > issue.due_date:
336
+ report.issues.append(
337
+ HealthIssue(
338
+ code="missed_due_date",
339
+ message=f"Issue completed after due date ({issue.due_date.date()})",
340
+ severity=HealthSeverity.INFO,
341
+ category="structure",
342
+ details={
343
+ "due_date": issue.due_date.isoformat(),
344
+ "completed_date": issue.actual_end_date.isoformat(),
345
+ },
346
+ )
347
+ )
348
+
349
+ def _check_issue_assignee(self, issue: Issue, report: EntityHealthReport):
350
+ """Check issue assignee consistency."""
351
+ if issue.previous_assignee and not issue.handoff_date:
352
+ report.issues.append(
353
+ HealthIssue(
354
+ code="missing_handoff_date",
355
+ message=f"Issue was handed off from {issue.previous_assignee} but no handoff date recorded",
356
+ severity=HealthSeverity.WARNING,
357
+ category="structure",
358
+ details={"previous_assignee": issue.previous_assignee},
359
+ )
360
+ )
361
+
362
+ def _check_issue_progress(self, issue: Issue, report: EntityHealthReport):
363
+ """Check issue progress tracking."""
364
+ if issue.progress_percentage is not None:
365
+ if not (0 <= issue.progress_percentage <= 100):
366
+ report.issues.append(
367
+ HealthIssue(
368
+ code="invalid_progress_percentage",
369
+ message=f"Progress must be 0-100, got {issue.progress_percentage}",
370
+ severity=HealthSeverity.ERROR,
371
+ category="structure",
372
+ details={"progress_percentage": issue.progress_percentage},
373
+ )
374
+ )
375
+
376
+ # Check progress vs status consistency
377
+ if issue.status == Status.CLOSED and issue.progress_percentage != 100:
378
+ report.issues.append(
379
+ HealthIssue(
380
+ code="inconsistent_completion",
381
+ message=f"Issue marked Done but only {issue.progress_percentage or 0}% complete",
382
+ severity=HealthSeverity.WARNING,
383
+ category="structure",
384
+ details={"progress_percentage": issue.progress_percentage},
385
+ )
386
+ )
387
+
388
+ def _check_issue_status_consistency(self, issue: Issue, report: EntityHealthReport):
389
+ """Check overall status consistency."""
390
+ if issue.status == Status.CLOSED and not issue.actual_end_date:
391
+ report.issues.append(
392
+ HealthIssue(
393
+ code="missing_completion_date",
394
+ message="Issue marked Done but has no actual end date recorded",
395
+ severity=HealthSeverity.WARNING,
396
+ category="structure",
397
+ )
398
+ )
399
+
400
+ if issue.actual_start_date and issue.status == Status.TODO:
401
+ report.issues.append(
402
+ HealthIssue(
403
+ code="inconsistent_status",
404
+ message="Issue has a start date but status is still TODO",
405
+ severity=HealthSeverity.WARNING,
406
+ category="structure",
407
+ details={"actual_start_date": issue.actual_start_date.isoformat()},
408
+ )
409
+ )
410
+
411
+ @staticmethod
412
+ def _validate_comment_thread(comments: list[Comment]) -> list[str]:
413
+ """Validate comment threads for errors.
414
+
415
+ Returns list of error messages found.
416
+ """
417
+ errors = []
418
+ comment_ids = set()
419
+
420
+ for comment in comments:
421
+ # Check for duplicate IDs
422
+ if comment.id in comment_ids:
423
+ errors.append(f"Duplicate comment ID: {comment.id}")
424
+ comment_ids.add(comment.id)
425
+
426
+ # Check for empty body
427
+ if not comment.body or not comment.body.strip():
428
+ errors.append(f"Comment {comment.id}: Empty comment body")
429
+
430
+ # Check for empty author
431
+ if not comment.author or not comment.author.strip():
432
+ errors.append(f"Comment {comment.id}: Missing comment author")
433
+
434
+ # Check for invalid datetime fields
435
+ if not isinstance(comment.created_at, type(comment.created_at)):
436
+ errors.append(
437
+ f"Comment {comment.id}: created_at is not a valid datetime"
438
+ )
439
+
440
+ # Check for circular references in reply chains
441
+ for comment in comments:
442
+ if comment.in_reply_to:
443
+ visited = set()
444
+ current_id = comment.in_reply_to
445
+ while current_id:
446
+ if current_id in visited:
447
+ errors.append(
448
+ f"Comment {comment.id}: Circular reference in reply chain"
449
+ )
450
+ break
451
+ visited.add(current_id)
452
+ # Find the comment this replies to
453
+ current_comment = next(
454
+ (c for c in comments if c.id == current_id), None
455
+ )
456
+ current_id = (
457
+ current_comment.in_reply_to if current_comment else None
458
+ )
459
+
460
+ return errors
@@ -0,0 +1,193 @@
1
+ """Service for repairing malformed YAML in issue files."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from roadmap.common.logging import get_logger
9
+ from roadmap.common.logging.error_logging import (
10
+ log_error_with_context,
11
+ )
12
+ from roadmap.common.observability.instrumentation import traced
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class FileRepairResult:
18
+ """Result of file repair operation."""
19
+
20
+ def __init__(self):
21
+ """Initialize FileRepairResult."""
22
+ self.fixed_files: list[str] = []
23
+ self.errors: list[str] = []
24
+
25
+ def add_fixed(self, file_rel: str) -> None:
26
+ """Record a fixed file."""
27
+ self.fixed_files.append(file_rel)
28
+
29
+ def add_error(self, file_rel: str) -> None:
30
+ """Record a file that couldn't be fixed."""
31
+ self.errors.append(file_rel)
32
+
33
+
34
+ class FileRepairService:
35
+ """Repairs common malformed YAML issues in issue files."""
36
+
37
+ @traced("repair_files")
38
+ def repair_files(
39
+ self, issues_dir: Path, malformed_files: list[str], dry_run: bool = False
40
+ ) -> FileRepairResult:
41
+ """Fix common malformed YAML issues in issue files.
42
+
43
+ Args:
44
+ issues_dir: Directory containing issue files
45
+ malformed_files: List of relative paths to malformed files
46
+ dry_run: If True, don't write changes to disk
47
+
48
+ Returns:
49
+ FileRepairResult with fixed_files and errors lists
50
+ """
51
+ result = FileRepairResult()
52
+
53
+ for file_rel in malformed_files:
54
+ file_path = issues_dir / file_rel
55
+
56
+ try:
57
+ content = file_path.read_text(encoding="utf-8")
58
+
59
+ # Extract frontmatter
60
+ if not content.startswith("---"):
61
+ result.add_error(file_rel)
62
+ continue
63
+
64
+ parts = content.split("---", 2)
65
+ if len(parts) < 3:
66
+ result.add_error(file_rel)
67
+ continue
68
+
69
+ frontmatter_str, markdown_content = parts[1], parts[2]
70
+
71
+ # Parse and fix common issues
72
+ try:
73
+ frontmatter = yaml.safe_load(frontmatter_str)
74
+ if not isinstance(frontmatter, dict):
75
+ result.add_error(file_rel)
76
+ continue
77
+ except yaml.YAMLError:
78
+ result.add_error(file_rel)
79
+ continue
80
+
81
+ # Fix git_commits and git_branches
82
+ self._normalize_git_data(frontmatter)
83
+
84
+ # Reconstruct the file
85
+ fixed_frontmatter = yaml.dump(
86
+ frontmatter, default_flow_style=False, sort_keys=False
87
+ )
88
+ fixed_content = f"---\n{fixed_frontmatter}---\n{markdown_content}"
89
+
90
+ if not dry_run:
91
+ try:
92
+ file_path.write_text(fixed_content, encoding="utf-8")
93
+ except Exception as write_error:
94
+ log_error_with_context(
95
+ write_error,
96
+ operation="write_repaired_file",
97
+ entity_type="File",
98
+ entity_id=file_rel,
99
+ )
100
+ result.add_error(file_rel)
101
+ logger.warning(
102
+ "Failed to write repaired file",
103
+ file=file_rel,
104
+ error=str(write_error),
105
+ severity="operational",
106
+ )
107
+ continue
108
+
109
+ result.add_fixed(file_rel)
110
+ logger.info("Fixed malformed file", file=file_rel)
111
+
112
+ except Exception as e:
113
+ log_error_with_context(
114
+ e,
115
+ operation="repair_file",
116
+ entity_type="File",
117
+ entity_id=file_rel,
118
+ )
119
+ result.add_error(file_rel)
120
+ logger.warning(
121
+ "Failed to fix file",
122
+ file=file_rel,
123
+ error=str(e),
124
+ severity="operational",
125
+ )
126
+
127
+ return result
128
+
129
+ @staticmethod
130
+ @staticmethod
131
+ def _fix_git_commits(frontmatter: dict[str, Any]) -> None:
132
+ """Fix git_commits list format.
133
+
134
+ Converts list of strings to list of dicts with 'hash' key.
135
+
136
+ Args:
137
+ frontmatter: Frontmatter dict to fix
138
+ """
139
+ if "git_commits" not in frontmatter or not isinstance(
140
+ frontmatter["git_commits"], list
141
+ ):
142
+ return
143
+
144
+ fixed_commits = []
145
+ for commit in frontmatter["git_commits"]:
146
+ if isinstance(commit, str):
147
+ # Convert string commit hash to dict
148
+ fixed_commits.append({"hash": commit})
149
+ else:
150
+ fixed_commits.append(commit)
151
+ frontmatter["git_commits"] = fixed_commits
152
+
153
+ @staticmethod
154
+ def _fix_git_branches(frontmatter: dict[str, Any]) -> None:
155
+ """Fix git_branches list format.
156
+
157
+ Converts list of dicts to list of strings.
158
+
159
+ Args:
160
+ frontmatter: Frontmatter dict to fix
161
+ """
162
+ if "git_branches" not in frontmatter or not isinstance(
163
+ frontmatter["git_branches"], list
164
+ ):
165
+ return
166
+
167
+ fixed_branches = []
168
+ for branch in frontmatter["git_branches"]:
169
+ if isinstance(branch, dict):
170
+ # Convert dict to string - prefer 'name' field
171
+ if "name" in branch:
172
+ fixed_branches.append(branch["name"])
173
+ elif isinstance(branch, str):
174
+ fixed_branches.append(branch)
175
+ else:
176
+ fixed_branches.append(str(branch))
177
+ elif isinstance(branch, str):
178
+ fixed_branches.append(branch)
179
+ else:
180
+ # Convert any other type to string
181
+ fixed_branches.append(str(branch))
182
+ frontmatter["git_branches"] = fixed_branches
183
+
184
+ @staticmethod
185
+ def _normalize_git_data(frontmatter: dict[str, Any]) -> None:
186
+ """Normalize git_commits and git_branches in frontmatter.
187
+
188
+ Fixes:
189
+ - git_commits: list of strings → list of dicts with 'hash' key
190
+ - git_branches: list of dicts → list of strings
191
+ """
192
+ FileRepairService._fix_git_commits(frontmatter)
193
+ FileRepairService._fix_git_branches(frontmatter)