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,381 @@
1
+ """Output abstraction layer - Core data structures for structured output.
2
+
3
+ This module provides the foundation for multi-format output (Rich, plain-text, JSON, CSV).
4
+ Commands return structured TableData objects instead of formatted strings, allowing
5
+ the same data to be rendered in multiple formats.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import StrEnum
10
+ from typing import Any
11
+
12
+
13
+ class ColumnType(StrEnum):
14
+ """Supported column data types."""
15
+
16
+ STRING = "string"
17
+ INTEGER = "integer"
18
+ FLOAT = "float"
19
+ BOOLEAN = "boolean"
20
+ DATE = "date"
21
+ DATETIME = "datetime"
22
+ ENUM = "enum"
23
+
24
+
25
+ @dataclass
26
+ class ColumnDef:
27
+ """Metadata for a single table column.
28
+
29
+ This separates column structure from presentation, enabling the same
30
+ column to be rendered in multiple output formats.
31
+
32
+ Attributes:
33
+ name: Column identifier (e.g., "id", "title"). Used for filtering/sorting.
34
+ display_name: Human-readable header (e.g., "ID", "Title").
35
+ type: Column data type (string, integer, date, enum, etc.).
36
+ width: Suggested display width in characters (for text-based formats).
37
+ headline: Help text describing the column purpose.
38
+ display_style: Rich styling (e.g., "cyan", "bold green"). Ignored in plain-text/JSON/CSV.
39
+ enum_values: Valid values for type="enum". None for other types.
40
+ sortable: Can users sort by this column?
41
+ filterable: Can users filter by this column?
42
+
43
+ Example:
44
+ ColumnDef(
45
+ name="status",
46
+ display_name="Status",
47
+ type=ColumnType.ENUM,
48
+ width=10,
49
+ headline="Issue status",
50
+ display_style="yellow",
51
+ enum_values=["open", "closed", "blocked"],
52
+ sortable=True,
53
+ filterable=True,
54
+ )
55
+ """
56
+
57
+ name: str
58
+ display_name: str
59
+ type: ColumnType = ColumnType.STRING
60
+ width: int | None = None
61
+ headline: str = ""
62
+ display_style: str | None = None
63
+ enum_values: list[str] | None = None
64
+ sortable: bool = True
65
+ filterable: bool = True
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ """Export column metadata as dictionary."""
69
+ return {
70
+ "name": self.name,
71
+ "display_name": self.display_name,
72
+ "type": self.type.value,
73
+ "width": self.width,
74
+ "headline": self.headline,
75
+ "display_style": self.display_style,
76
+ "enum_values": self.enum_values,
77
+ "sortable": self.sortable,
78
+ "filterable": self.filterable,
79
+ }
80
+
81
+ @staticmethod
82
+ def from_dict(data: dict[str, Any]) -> "ColumnDef":
83
+ """Import column metadata from dictionary."""
84
+ return ColumnDef(
85
+ name=data["name"],
86
+ display_name=data.get("display_name", data["name"]),
87
+ type=ColumnType(data.get("type", "string")),
88
+ width=data.get("width"),
89
+ headline=data.get("headline", ""),
90
+ display_style=data.get("display_style"),
91
+ enum_values=data.get("enum_values"),
92
+ sortable=data.get("sortable", True),
93
+ filterable=data.get("filterable", True),
94
+ )
95
+
96
+
97
+ @dataclass
98
+ class TableData:
99
+ """Structured table data with metadata.
100
+
101
+ Contains the data and metadata needed to render a table in any format
102
+ (Rich, plain-text, JSON, CSV). This decouples data from presentation.
103
+
104
+ Attributes:
105
+ columns: List of ColumnDef objects defining table structure.
106
+ rows: List of rows, where each row is a list of values matching column order.
107
+ title: Optional table title/heading.
108
+ headline: Optional table description.
109
+ filters_applied: Dictionary of {column_name: filter_value} for active filters.
110
+ sort_by: List of (column_name, "asc"|"desc") tuples for sort order.
111
+ selected_columns: List of column names to display (None = all columns).
112
+ total_count: Total number of rows before filtering (for pagination).
113
+ returned_count: Number of rows returned after filtering.
114
+
115
+ Example:
116
+ table = TableData(
117
+ columns=[
118
+ ColumnDef(name="id", display_name="ID", type=ColumnType.STRING, width=9),
119
+ ColumnDef(name="title", display_name="Title", type=ColumnType.STRING),
120
+ ColumnDef(name="status", display_name="Status", type=ColumnType.ENUM,
121
+ enum_values=["open", "closed"]),
122
+ ],
123
+ rows=[
124
+ ["123", "Fix bug", "open"],
125
+ ["124", "Add feature", "closed"],
126
+ ],
127
+ title="Issues",
128
+ )
129
+ """
130
+
131
+ columns: list[ColumnDef]
132
+ rows: list[list[Any]]
133
+ title: str | None = None
134
+ headline: str | None = None
135
+ filters_applied: dict[str, Any] = field(default_factory=dict)
136
+ sort_by: list[tuple[str, str]] | list = field(default_factory=list)
137
+ selected_columns: list[str] | None = None
138
+ total_count: int | None = None
139
+ returned_count: int | None = None
140
+
141
+ def __post_init__(self):
142
+ """Validate table structure after initialization."""
143
+ # Validate row structure
144
+ if self.rows:
145
+ expected_columns = len(self.columns)
146
+ for i, row in enumerate(self.rows):
147
+ if len(row) != expected_columns:
148
+ raise ValueError(
149
+ f"Row {i} has {len(row)} values but {expected_columns} columns expected"
150
+ )
151
+
152
+ # Set defaults for counts
153
+ if self.total_count is None:
154
+ self.total_count = len(self.rows)
155
+ if self.returned_count is None:
156
+ self.returned_count = len(self.rows)
157
+
158
+ @property
159
+ def active_columns(self) -> list[ColumnDef]:
160
+ """Get columns to display (respecting selected_columns)."""
161
+ if self.selected_columns:
162
+ # Return columns in selected order
163
+ col_map = {col.name: col for col in self.columns}
164
+ return [col_map[name] for name in self.selected_columns if name in col_map]
165
+ return self.columns
166
+
167
+ @property
168
+ def active_rows(self) -> list[list[Any]]:
169
+ """Get rows with only selected columns."""
170
+ if not self.selected_columns:
171
+ return self.rows
172
+
173
+ # Build column index mapping
174
+ col_indices = {col.name: i for i, col in enumerate(self.columns)}
175
+ selected_indices = [
176
+ col_indices[name] for name in self.selected_columns if name in col_indices
177
+ ]
178
+
179
+ # Filter row values
180
+ return [[row[i] for i in selected_indices] for row in self.rows]
181
+
182
+ def filter(self, column: str, value: Any) -> "TableData":
183
+ """Apply filter and return new TableData.
184
+
185
+ Args:
186
+ column: Column name to filter by.
187
+ value: Value(s) to filter for. Can be single value or list for IN filter.
188
+
189
+ Returns:
190
+ New TableData with filtered rows.
191
+
192
+ Raises:
193
+ ValueError: If column doesn't exist or value doesn't match enum values.
194
+ """
195
+ col = self._get_filter_column(column)
196
+ self._validate_enum_filter_values(column, col, value)
197
+ col_index = self._get_column_index(column)
198
+ filtered_rows = self._filter_rows(column_index=col_index, value=value)
199
+
200
+ # Create new TableData with updated state
201
+ new_table = TableData(
202
+ columns=self.columns,
203
+ rows=filtered_rows,
204
+ title=self.title,
205
+ headline=self.headline,
206
+ filters_applied={**self.filters_applied, column: value},
207
+ sort_by=self.sort_by,
208
+ selected_columns=self.selected_columns,
209
+ total_count=self.total_count,
210
+ returned_count=len(filtered_rows),
211
+ )
212
+ return new_table
213
+
214
+ def _get_filter_column(self, column: str) -> ColumnDef:
215
+ """Return filter column definition or raise a validation error."""
216
+ col = next((c for c in self.columns if c.name == column), None)
217
+ if not col:
218
+ raise ValueError(f"Column '{column}' not found")
219
+ if not col.filterable:
220
+ raise ValueError(f"Column '{column}' is not filterable")
221
+ return col
222
+
223
+ def _validate_enum_filter_values(
224
+ self,
225
+ column: str,
226
+ col: ColumnDef,
227
+ value: Any,
228
+ ) -> None:
229
+ """Validate enum filter values against allowed values."""
230
+ if col.type != ColumnType.ENUM or not col.enum_values:
231
+ return
232
+ values_to_check = value if isinstance(value, list) else [value]
233
+ for candidate in values_to_check:
234
+ if candidate not in col.enum_values:
235
+ raise ValueError(
236
+ f"Invalid value '{candidate}' for enum column '{column}'"
237
+ )
238
+
239
+ def _get_column_index(self, column: str) -> int:
240
+ """Return index of the named column."""
241
+ return next(i for i, c in enumerate(self.columns) if c.name == column)
242
+
243
+ def _filter_rows(self, *, column_index: int, value: Any) -> list[list[Any]]:
244
+ """Apply either equality or IN filtering to rows."""
245
+ if isinstance(value, list):
246
+ return [row for row in self.rows if row[column_index] in value]
247
+ return [row for row in self.rows if row[column_index] == value]
248
+
249
+ def sort(self, sort_spec: str | list[tuple[str, str]]) -> "TableData":
250
+ """Apply sorting and return new TableData.
251
+
252
+ Args:
253
+ sort_spec: Either a string (column name, ascending) or
254
+ list of (column_name, "asc"|"desc") tuples.
255
+ Sorts by first column, then second, etc.
256
+
257
+ Returns:
258
+ New TableData with sorted rows.
259
+
260
+ Raises:
261
+ ValueError: If column doesn't exist or is not sortable.
262
+ """
263
+ # Normalize sort_spec to list of tuples
264
+ if isinstance(sort_spec, str):
265
+ sort_spec_list = [(sort_spec, "asc")]
266
+ else:
267
+ sort_spec_list = sort_spec
268
+
269
+ # Validate columns and build sort key function
270
+ col_indices = {}
271
+ sort_directions = {}
272
+
273
+ for col_name, direction in sort_spec_list:
274
+ col = next((c for c in self.columns if c.name == col_name), None)
275
+ if not col:
276
+ raise ValueError(f"Column '{col_name}' not found")
277
+ if not col.sortable:
278
+ raise ValueError(f"Column '{col_name}' is not sortable")
279
+
280
+ col_index = next(
281
+ i for i, c in enumerate(self.columns) if c.name == col_name
282
+ )
283
+ col_indices[col_name] = col_index
284
+ sort_directions[col_name] = direction.lower() == "desc"
285
+
286
+ # Sort rows using multiple keys
287
+ def sort_key(row):
288
+ keys = []
289
+ for col_name, _ in sort_spec_list:
290
+ index = col_indices[col_name]
291
+ value = row[index]
292
+ # Handle None values
293
+ if value is None:
294
+ keys.append((1, value)) # None sorts to end
295
+ else:
296
+ keys.append((0, value))
297
+ return tuple(keys)
298
+
299
+ sorted_rows = sorted(self.rows, key=sort_key)
300
+
301
+ # Apply descending direction
302
+ for col_name, _direction in reversed(sort_spec_list):
303
+ if sort_directions[col_name]:
304
+ sorted_rows.reverse()
305
+
306
+ # Create new TableData with updated state
307
+ new_table = TableData(
308
+ columns=self.columns,
309
+ rows=sorted_rows,
310
+ title=self.title,
311
+ headline=self.headline,
312
+ filters_applied=self.filters_applied,
313
+ sort_by=sort_spec_list,
314
+ selected_columns=self.selected_columns,
315
+ total_count=self.total_count,
316
+ returned_count=self.returned_count,
317
+ )
318
+ return new_table
319
+
320
+ def select_columns(self, columns: list[str]) -> "TableData":
321
+ """Select specific columns to display.
322
+
323
+ Args:
324
+ columns: List of column names to display.
325
+
326
+ Returns:
327
+ New TableData with column selection applied.
328
+
329
+ Raises:
330
+ ValueError: If any column name doesn't exist.
331
+ """
332
+ # Validate all columns exist
333
+ col_names = {c.name for c in self.columns}
334
+ for col in columns:
335
+ if col not in col_names:
336
+ raise ValueError(f"Column '{col}' not found")
337
+
338
+ # Create new TableData with column selection
339
+ new_table = TableData(
340
+ columns=self.columns,
341
+ rows=self.rows,
342
+ title=self.title,
343
+ headline=self.headline,
344
+ filters_applied=self.filters_applied,
345
+ sort_by=self.sort_by,
346
+ selected_columns=columns,
347
+ total_count=self.total_count,
348
+ returned_count=self.returned_count,
349
+ )
350
+ return new_table
351
+
352
+ def to_dict(self) -> dict[str, Any]:
353
+ """Export table as dictionary (useful for JSON)."""
354
+ return {
355
+ "title": self.title,
356
+ "headline": self.headline,
357
+ "columns": [col.to_dict() for col in self.columns],
358
+ "rows": self.active_rows,
359
+ "metadata": {
360
+ "total": self.total_count,
361
+ "returned": self.returned_count,
362
+ "filters_applied": self.filters_applied,
363
+ "sort_by": self.sort_by,
364
+ "selected_columns": self.selected_columns,
365
+ },
366
+ }
367
+
368
+ @staticmethod
369
+ def from_dict(data: dict[str, Any]) -> "TableData":
370
+ """Import table from dictionary."""
371
+ return TableData(
372
+ columns=[ColumnDef.from_dict(col) for col in data.get("columns", [])],
373
+ rows=data.get("rows", []),
374
+ title=data.get("title"),
375
+ headline=data.get("headline") or data.get("description"),
376
+ filters_applied=data.get("metadata", {}).get("filters_applied", {}),
377
+ sort_by=data.get("metadata", {}).get("sort_by", []),
378
+ selected_columns=data.get("metadata", {}).get("selected_columns"),
379
+ total_count=data.get("metadata", {}).get("total"),
380
+ returned_count=data.get("metadata", {}).get("returned"),
381
+ )
@@ -0,0 +1,28 @@
1
+ """Observability and instrumentation utilities.
2
+
3
+ This module provides:
4
+ - Tracing with OpenTelemetry (otel_init)
5
+ - Span context management (observability)
6
+ - Instrumentation decorators (instrumentation)
7
+
8
+ Use submodule imports for clarity:
9
+ from roadmap.common.observability.instrumentation import traced
10
+ from roadmap.common.observability.observability import get_current_span, set_current_span
11
+ from roadmap.common.observability.otel_init import initialize_tracing, is_tracing_enabled
12
+ """
13
+
14
+ from .instrumentation import span_context_processor, traced
15
+ from .observability import Span, create_span, get_current_span, set_current_span
16
+ from .otel_init import get_tracer, initialize_tracing, is_tracing_enabled
17
+
18
+ __all__ = [
19
+ "traced",
20
+ "span_context_processor",
21
+ "Span",
22
+ "create_span",
23
+ "get_current_span",
24
+ "set_current_span",
25
+ "get_tracer",
26
+ "initialize_tracing",
27
+ "is_tracing_enabled",
28
+ ]
@@ -0,0 +1,103 @@
1
+ """Decorators for automatic span creation and timing.
2
+
3
+ Provides @traced decorator for instrumenting functions with automatic
4
+ trace span creation and duration tracking.
5
+ """
6
+
7
+ import functools
8
+ from collections.abc import Callable
9
+ from typing import Any, TypeVar
10
+
11
+ from structlog import get_logger
12
+
13
+ from .observability import create_span, get_current_span, set_current_span
14
+
15
+ logger = get_logger()
16
+
17
+ F = TypeVar("F", bound=Callable[..., Any])
18
+
19
+
20
+ def traced(operation_name: str) -> Callable[[F], F]:
21
+ """Create spans for functions automatically.
22
+
23
+ Wraps a function to create a trace span, track duration, and set
24
+ span context in logs. Works with both sync and async functions.
25
+
26
+ Args:
27
+ operation_name: Name of the operation to use in traces
28
+
29
+ Returns:
30
+ Decorator function
31
+
32
+ Example:
33
+ @traced("fetch_items")
34
+ def get_items():
35
+ return [1, 2, 3]
36
+ """
37
+
38
+ def decorator(func: F) -> F:
39
+ @functools.wraps(func)
40
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
41
+ span = create_span(operation_name)
42
+ previous_span = get_current_span()
43
+
44
+ try:
45
+ set_current_span(span)
46
+ result = func(*args, **kwargs)
47
+
48
+ # Log span completion
49
+ logger.debug(
50
+ "span_completed",
51
+ operation_name=operation_name,
52
+ extra={
53
+ "span_id": span.span_id,
54
+ "parent_span_id": span.parent_span_id,
55
+ "duration_ms": span.duration_ms,
56
+ },
57
+ )
58
+
59
+ return result
60
+ except Exception as exc:
61
+ logger.error(
62
+ "span_failed",
63
+ operation_name=operation_name,
64
+ extra={
65
+ "span_id": span.span_id,
66
+ "parent_span_id": span.parent_span_id,
67
+ "duration_ms": span.duration_ms,
68
+ "error": str(exc),
69
+ },
70
+ exc_info=True,
71
+ )
72
+ raise
73
+ finally:
74
+ set_current_span(previous_span)
75
+
76
+ return wrapper # type: ignore
77
+
78
+ return decorator
79
+
80
+
81
+ def span_context_processor(
82
+ logger: Any, name: str, event_dict: dict[str, Any]
83
+ ) -> dict[str, Any]:
84
+ """Structlog processor to inject span context into logs.
85
+
86
+ Adds span_id and parent_span_id to all log events from within
87
+ an active span.
88
+
89
+ Args:
90
+ logger: The logger instance
91
+ name: Name of the logging call
92
+ event_dict: The event dictionary
93
+
94
+ Returns:
95
+ Modified event dictionary with span context
96
+ """
97
+ span = get_current_span()
98
+ if span:
99
+ event_dict["span_id"] = span.span_id
100
+ event_dict["parent_span_id"] = span.parent_span_id
101
+ event_dict["span_elapsed_ms"] = span.duration_ms
102
+
103
+ return event_dict
@@ -0,0 +1,76 @@
1
+ """Minimal trace context management for distributed tracing.
2
+
3
+ Provides simple span tracking and timing for debugging performance
4
+ and correlating logs within a single CLI invocation.
5
+ """
6
+
7
+ import contextvars
8
+ import time
9
+ import uuid
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+
13
+ # Context variable to store current span
14
+ _current_span: contextvars.ContextVar[Optional["Span"]] = contextvars.ContextVar(
15
+ "current_span", default=None
16
+ )
17
+
18
+
19
+ @dataclass
20
+ class Span:
21
+ """Represents a single operation in a trace."""
22
+
23
+ name: str
24
+ span_id: str
25
+ start_time: float
26
+ parent_span_id: str | None = None
27
+
28
+ @property
29
+ def duration_ms(self) -> float:
30
+ """Get elapsed time in milliseconds."""
31
+ return (time.time() - self.start_time) * 1000
32
+
33
+ def to_dict(self):
34
+ """Convert to dict for logging."""
35
+ return {
36
+ "span_id": self.span_id,
37
+ "parent_span_id": self.parent_span_id,
38
+ "span_duration_ms": self.duration_ms,
39
+ }
40
+
41
+
42
+ def create_span(name: str) -> Span:
43
+ """Create a new span.
44
+
45
+ Args:
46
+ name: Name of the operation being traced
47
+
48
+ Returns:
49
+ Span object representing this operation
50
+ """
51
+ parent = _current_span.get()
52
+ span = Span(
53
+ name=name,
54
+ span_id=uuid.uuid4().hex[:16],
55
+ start_time=time.time(),
56
+ parent_span_id=parent.span_id if parent else None,
57
+ )
58
+ return span
59
+
60
+
61
+ def set_current_span(span: Span | None) -> None:
62
+ """Set the current active span.
63
+
64
+ Args:
65
+ span: Span object to set as current, or None to clear
66
+ """
67
+ _current_span.set(span)
68
+
69
+
70
+ def get_current_span() -> Span | None:
71
+ """Get the current active span.
72
+
73
+ Returns:
74
+ Current Span object or None if no active span
75
+ """
76
+ return _current_span.get()
@@ -0,0 +1,109 @@
1
+ """OpenTelemetry initialization for local tracing.
2
+
3
+ Sets up trace exporter and OTLP integration for local development.
4
+ Exports traces to local OTLP receiver on port 4317.
5
+ """
6
+
7
+ from structlog import get_logger
8
+
9
+ logger = get_logger()
10
+
11
+ # Global tracer instance
12
+ _tracer: object | None = None
13
+
14
+
15
+ def initialize_tracing(service_name: str = "roadmap-cli") -> None:
16
+ """Initialize OpenTelemetry tracing with OTLP exporter.
17
+
18
+ This must be called once at application startup before any tracing
19
+ operations. It configures trace export to a local OTLP receiver
20
+ (typically a collector or Jaeger with OTLP native support).
21
+
22
+ Args:
23
+ service_name: Name of the service for tracing (default: "roadmap-cli")
24
+
25
+ Example:
26
+ from roadmap.common.observability.otel_init import initialize_tracing
27
+ initialize_tracing()
28
+ """
29
+ global _tracer
30
+
31
+ try:
32
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
33
+ OTLPSpanExporter,
34
+ )
35
+ from opentelemetry.sdk.trace import TracerProvider
36
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
37
+
38
+ # Create OTLP exporter (connects to localhost:4317 by default)
39
+ otlp_exporter = OTLPSpanExporter(
40
+ endpoint="http://localhost:4317",
41
+ )
42
+
43
+ # Create tracer provider
44
+ tracer_provider = TracerProvider()
45
+ tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
46
+
47
+ # Set global tracer provider
48
+ from opentelemetry import trace
49
+
50
+ trace.set_tracer_provider(tracer_provider)
51
+
52
+ _tracer = trace.get_tracer(__name__)
53
+
54
+ logger.debug(
55
+ "opentelemetry_tracing_initialized",
56
+ extra={"service": service_name, "exporter": "otlp"},
57
+ )
58
+
59
+ except ImportError as e:
60
+ logger.debug(
61
+ "opentelemetry_not_available",
62
+ error=str(e),
63
+ detail="Tracing features will be disabled. Install with: pip install opentelemetry-exporter-otlp",
64
+ )
65
+ _tracer = None
66
+
67
+
68
+ def is_tracing_enabled() -> bool:
69
+ """Check if tracing has been initialized.
70
+
71
+ Returns:
72
+ True if tracing is available, False otherwise
73
+ """
74
+ return _tracer is not None
75
+
76
+
77
+ def get_tracer():
78
+ """Get the global tracer instance.
79
+
80
+ Returns:
81
+ Tracer instance if initialized, None otherwise
82
+ """
83
+ # If tracer is None, return None
84
+ if _tracer is None:
85
+ return None
86
+
87
+ # If tracer is already callable (e.g., a Mock in tests or a factory),
88
+ # return it directly so existing tests that set Mock() work.
89
+ if callable(_tracer):
90
+ return _tracer
91
+
92
+ # For real Tracer instances (which may not be callable), return a
93
+ # small proxy that is callable and delegates attribute access to the
94
+ # underlying tracer. This keeps `callable(get_tracer())` True while
95
+ # preserving expected tracer behavior.
96
+ class _TracerProxy:
97
+ def __init__(self, tracer):
98
+ self._tracer = tracer
99
+
100
+ def __call__(self, *_args, **_kwargs):
101
+ return self._tracer
102
+
103
+ def __getattr__(self, name):
104
+ return getattr(self._tracer, name)
105
+
106
+ def __repr__(self):
107
+ return f"<TracerProxy for {self._tracer!r}>"
108
+
109
+ return _TracerProxy(_tracer)