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,480 @@
1
+ """Timezone utilities for the roadmap CLI tool.
2
+
3
+ This module provides timezone-aware datetime handling utilities to support
4
+ distributed teams across multiple timezones. All datetime objects should be
5
+ stored in UTC and displayed in the user's local timezone.
6
+
7
+ Key Principles:
8
+ 1. Store all datetimes in UTC
9
+ 2. Parse user input in their timezone, convert to UTC for storage
10
+ 3. Display datetimes in user's preferred timezone
11
+ 4. Maintain backward compatibility during migration
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ from datetime import UTC, datetime, tzinfo
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from structlog import get_logger
21
+
22
+ logger = get_logger()
23
+
24
+ # Import zoneinfo with fallback for older Python versions
25
+ try:
26
+ from zoneinfo import ZoneInfo # type: ignore[attr-defined]
27
+
28
+ ZONEINFO_AVAILABLE = True
29
+ except ImportError:
30
+ # Fallback for Python < 3.9
31
+ try:
32
+ from backports.zoneinfo import ZoneInfo # type: ignore
33
+
34
+ ZONEINFO_AVAILABLE = True
35
+ except ImportError:
36
+ ZONEINFO_AVAILABLE = False
37
+ # Use basic timezone handling as fallback
38
+
39
+ class ZoneInfo(tzinfo):
40
+ """Minimal ZoneInfo fallback for older Python versions."""
41
+
42
+ def __init__(self, name: str):
43
+ """Initialize ZoneInfo fallback.
44
+
45
+ Args:
46
+ name: Timezone name.
47
+ """
48
+ self.name = name
49
+ if name == "UTC":
50
+ self._tz = UTC
51
+ else:
52
+ # For other timezones, we'll use UTC as fallback
53
+ # This is not ideal but maintains basic functionality
54
+ self._tz = UTC
55
+
56
+ def utcoffset(self, dt: datetime | None) -> Any:
57
+ """Get the UTC offset for this timezone.
58
+
59
+ Args:
60
+ dt: The datetime to get the offset for.
61
+
62
+ Returns:
63
+ The UTC offset as a timedelta.
64
+ """
65
+ return self._tz.utcoffset(dt)
66
+
67
+ def tzname(self, dt: datetime | None) -> str | None:
68
+ """Get the timezone name.
69
+
70
+ Args:
71
+ dt: The datetime to get the timezone name for.
72
+
73
+ Returns:
74
+ The timezone name as a string.
75
+ """
76
+ return self._tz.tzname(dt)
77
+
78
+ def dst(self, dt: datetime | None) -> Any:
79
+ """Get the daylight saving time offset.
80
+
81
+ Args:
82
+ dt: The datetime to get the DST offset for.
83
+
84
+ Returns:
85
+ The DST offset as a timedelta, or None if not applicable.
86
+ """
87
+ return self._tz.dst(dt)
88
+
89
+ def __repr__(self):
90
+ """Return string representation of ZoneInfo."""
91
+ return f"ZoneInfo('{self.name}')"
92
+
93
+
94
+ class TimezoneManager:
95
+ """Manages timezone operations for the roadmap CLI."""
96
+
97
+ DEFAULT_TIMEZONE = "UTC"
98
+
99
+ def __init__(self, user_timezone: str | None = None):
100
+ """Initialize timezone manager with user's preferred timezone."""
101
+ self.user_timezone = user_timezone or self._detect_system_timezone()
102
+ self._validate_timezone(self.user_timezone)
103
+
104
+ def _get_tz_from_env(self) -> str | None:
105
+ """Get timezone from TZ environment variable.
106
+
107
+ Returns:
108
+ Timezone name or None
109
+ """
110
+ try:
111
+ tz_env = os.environ.get("TZ")
112
+ if tz_env:
113
+ ZoneInfo(tz_env) # Validate it
114
+ return tz_env
115
+ except Exception as e:
116
+ logger.debug(
117
+ "failed_to_validate_tz_env", error=str(e), severity="operational"
118
+ )
119
+ return None
120
+
121
+ def _get_tz_from_etc_timezone(self) -> str | None:
122
+ """Get timezone from /etc/timezone file.
123
+
124
+ Returns:
125
+ Timezone name or None
126
+ """
127
+ try:
128
+ timezone_file = Path("/etc/timezone")
129
+ if timezone_file.exists():
130
+ tz_name = timezone_file.read_text().strip()
131
+ ZoneInfo(tz_name) # Validate it
132
+ return tz_name
133
+ except (OSError, FileNotFoundError, Exception):
134
+ pass
135
+ return None
136
+
137
+ def _get_tz_from_localtime_symlink(self) -> str | None:
138
+ """Get timezone from /etc/localtime symlink.
139
+
140
+ Returns:
141
+ Timezone name or None
142
+ """
143
+ try:
144
+ localtime_path = Path("/etc/localtime")
145
+ if localtime_path.is_symlink():
146
+ target = localtime_path.readlink()
147
+ # Extract timezone from path like /usr/share/zoneinfo/America/New_York
148
+ if "zoneinfo" in str(target):
149
+ tz_parts = str(target).split("zoneinfo/")
150
+ if len(tz_parts) > 1:
151
+ tz_name = tz_parts[1]
152
+ ZoneInfo(tz_name) # Validate it
153
+ return tz_name
154
+ except (OSError, Exception):
155
+ pass
156
+ return None
157
+
158
+ def _detect_system_timezone(self) -> str:
159
+ """Detect the system's timezone."""
160
+ if ZONEINFO_AVAILABLE:
161
+ # Try TZ environment variable
162
+ tz = self._get_tz_from_env()
163
+ if tz:
164
+ return tz
165
+
166
+ # Only Unix-like systems
167
+ if sys.platform != "win32":
168
+ # Check /etc/timezone
169
+ tz = self._get_tz_from_etc_timezone()
170
+ if tz:
171
+ return tz
172
+
173
+ # Check /etc/localtime symlink
174
+ tz = self._get_tz_from_localtime_symlink()
175
+ if tz:
176
+ return tz
177
+
178
+ # Fallback to UTC if detection fails
179
+ return self.DEFAULT_TIMEZONE
180
+
181
+ def _validate_timezone(self, tz_name: str) -> None:
182
+ """Validate that the timezone name is valid."""
183
+ try:
184
+ ZoneInfo(tz_name)
185
+ except Exception as e:
186
+ raise ValueError(f"Invalid timezone '{tz_name}': {e}") from e
187
+
188
+ def now_utc(self) -> datetime:
189
+ """Get current datetime in UTC with timezone information."""
190
+ return datetime.now(UTC)
191
+
192
+ def now_local(self) -> datetime:
193
+ """Get current datetime in user's timezone."""
194
+ return self.now_utc().astimezone(ZoneInfo(self.user_timezone))
195
+
196
+ def parse_user_input(
197
+ self, date_str: str, input_timezone: str | None = None
198
+ ) -> datetime:
199
+ """Parse user date input and convert to UTC.
200
+
201
+ Args:
202
+ date_str: Date string in various formats
203
+ input_timezone: Timezone to assume for the input (defaults to user timezone)
204
+
205
+ Returns:
206
+ Timezone-aware datetime in UTC
207
+ """
208
+ input_tz = input_timezone or self.user_timezone
209
+
210
+ try:
211
+ # Try to parse as ISO format first
212
+ if "T" in date_str:
213
+ # Handle ISO format: 2025-12-31T23:59:59
214
+ if date_str.endswith("Z"):
215
+ # Already UTC
216
+ return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
217
+ elif "+" in date_str or date_str.count("-") > 2:
218
+ # Has timezone info
219
+ return datetime.fromisoformat(date_str)
220
+ else:
221
+ # No timezone info, assume user timezone
222
+ naive_dt = datetime.fromisoformat(date_str)
223
+ return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(UTC)
224
+ else:
225
+ # Try to parse as date only: 2025-12-31
226
+ if date_str.count("-") == 2:
227
+ naive_dt = datetime.strptime(date_str, "%Y-%m-%d")
228
+ return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(UTC)
229
+
230
+ # Try other common formats
231
+ formats = [
232
+ "%Y-%m-%d %H:%M:%S",
233
+ "%Y/%m/%d %H:%M:%S",
234
+ "%Y-%m-%d %H:%M",
235
+ "%Y/%m/%d %H:%M",
236
+ "%m/%d/%Y %H:%M:%S",
237
+ "%m/%d/%Y %H:%M",
238
+ "%m/%d/%Y",
239
+ ]
240
+
241
+ for fmt in formats:
242
+ try:
243
+ naive_dt = datetime.strptime(date_str, fmt)
244
+ return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(
245
+ UTC
246
+ )
247
+ except ValueError as e:
248
+ logger.debug(
249
+ "date_format_failed",
250
+ format_pattern=fmt,
251
+ input=date_str,
252
+ error=str(e),
253
+ )
254
+ continue
255
+
256
+ raise ValueError(f"Unable to parse date string: {date_str}")
257
+
258
+ except Exception as e:
259
+ raise ValueError(f"Invalid date format '{date_str}': {e}") from e
260
+
261
+ def format_for_user(self, dt: datetime, format_str: str | None = None) -> str:
262
+ """Format datetime for display to user in their timezone.
263
+
264
+ Args:
265
+ dt: Timezone-aware datetime (preferably UTC)
266
+ format_str: strftime format string
267
+
268
+ Returns:
269
+ Formatted datetime string in user's timezone
270
+ """
271
+ if dt.tzinfo is None:
272
+ # Handle naive datetimes by assuming UTC
273
+ dt = dt.replace(tzinfo=UTC)
274
+
275
+ # Convert to user's timezone
276
+ local_dt = dt.astimezone(ZoneInfo(self.user_timezone))
277
+
278
+ # Default format includes timezone
279
+ if format_str is None:
280
+ format_str = "%Y-%m-%d %H:%M:%S %Z"
281
+
282
+ return local_dt.strftime(format_str)
283
+
284
+ def _format_relative_seconds(
285
+ self, total_seconds: float, diff_seconds: float
286
+ ) -> str:
287
+ """Format relative time in seconds.
288
+
289
+ Args:
290
+ total_seconds: Absolute seconds difference
291
+ diff_seconds: Signed seconds difference
292
+
293
+ Returns:
294
+ Formatted string
295
+ """
296
+ if total_seconds < 60:
297
+ return "just now"
298
+ elif total_seconds < 3600: # Less than 1 hour
299
+ minutes = int(total_seconds // 60)
300
+ unit = "minute" if minutes == 1 else "minutes"
301
+ direction = "ago" if diff_seconds < 0 else "from now"
302
+ return f"{minutes} {unit} {direction}"
303
+ elif total_seconds < 86400: # Less than 1 day
304
+ hours = int(total_seconds // 3600)
305
+ unit = "hour" if hours == 1 else "hours"
306
+ direction = "ago" if diff_seconds < 0 else "from now"
307
+ return f"{hours} {unit} {direction}"
308
+ else: # Days
309
+ days = int(total_seconds // 86400)
310
+ unit = "day" if days == 1 else "days"
311
+ direction = "ago" if diff_seconds < 0 else "from now"
312
+ return f"{days} {unit} {direction}"
313
+
314
+ def format_relative(self, dt: datetime) -> str:
315
+ """Format datetime as relative time (e.g., '2 hours ago', 'in 3 days').
316
+
317
+ Args:
318
+ dt: Timezone-aware datetime
319
+
320
+ Returns:
321
+ Human-readable relative time string
322
+ """
323
+ if dt.tzinfo is None:
324
+ dt = dt.replace(tzinfo=UTC)
325
+
326
+ now = self.now_utc()
327
+ diff = dt - now
328
+
329
+ # Calculate time difference
330
+ total_seconds = abs(diff.total_seconds())
331
+ return self._format_relative_seconds(total_seconds, diff.total_seconds())
332
+
333
+ def is_timezone_aware(self, dt: datetime) -> bool:
334
+ """Check if datetime is timezone-aware."""
335
+ return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
336
+
337
+ def make_aware(self, dt: datetime, tz: str | None = None) -> datetime:
338
+ """Convert naive datetime to timezone-aware.
339
+
340
+ Args:
341
+ dt: Naive datetime
342
+ tz: Timezone to assign (defaults to user timezone)
343
+
344
+ Returns:
345
+ Timezone-aware datetime in UTC
346
+ """
347
+ if self.is_timezone_aware(dt):
348
+ return dt.astimezone(UTC)
349
+
350
+ assumed_tz = tz or self.user_timezone
351
+ return dt.replace(tzinfo=ZoneInfo(assumed_tz)).astimezone(UTC)
352
+
353
+ def get_common_timezones(self) -> list:
354
+ """Get list of common timezone names."""
355
+ if ZONEINFO_AVAILABLE:
356
+ try:
357
+ from zoneinfo import available_timezones
358
+
359
+ # Return a curated list of common timezones
360
+ common = [
361
+ "UTC",
362
+ "US/Eastern",
363
+ "US/Central",
364
+ "US/Mountain",
365
+ "US/Pacific",
366
+ "Europe/London",
367
+ "Europe/Paris",
368
+ "Europe/Berlin",
369
+ "Asia/Tokyo",
370
+ "Asia/Shanghai",
371
+ "Asia/Kolkata",
372
+ "Australia/Sydney",
373
+ "Australia/Melbourne",
374
+ "America/New_York",
375
+ "America/Chicago",
376
+ "America/Denver",
377
+ "America/Los_Angeles",
378
+ "America/Toronto",
379
+ "America/Mexico_City",
380
+ "America/Sao_Paulo",
381
+ ]
382
+ # Filter to only include available timezones
383
+ available = available_timezones()
384
+ return [tz for tz in common if tz in available]
385
+ except ImportError:
386
+ logger.debug("zoneinfo_import_failed", severity="operational")
387
+
388
+ # Fallback list for systems without zoneinfo
389
+ return [
390
+ "UTC",
391
+ "US/Eastern",
392
+ "US/Central",
393
+ "US/Mountain",
394
+ "US/Pacific",
395
+ "Europe/London",
396
+ "Europe/Paris",
397
+ "Europe/Berlin",
398
+ "Asia/Tokyo",
399
+ "Asia/Shanghai",
400
+ ]
401
+
402
+
403
+ # Global timezone manager instance
404
+ _global_timezone_manager: TimezoneManager | None = None
405
+
406
+
407
+ def get_timezone_manager(user_timezone: str | None = None) -> TimezoneManager:
408
+ """Get or create the global timezone manager instance."""
409
+ global _global_timezone_manager
410
+
411
+ if _global_timezone_manager is None or user_timezone is not None:
412
+ _global_timezone_manager = TimezoneManager(user_timezone)
413
+
414
+ return _global_timezone_manager
415
+
416
+
417
+ def now_utc() -> datetime:
418
+ """Get current UTC datetime."""
419
+ return get_timezone_manager().now_utc()
420
+
421
+
422
+ def now_local(user_timezone: str | None = None) -> datetime:
423
+ """Get current datetime in user's timezone."""
424
+ return get_timezone_manager(user_timezone).now_local()
425
+
426
+
427
+ def parse_datetime(date_str: str, input_timezone: str | None = None) -> datetime:
428
+ """Parse user-input datetime string."""
429
+ return get_timezone_manager().parse_user_input(date_str, input_timezone)
430
+
431
+
432
+ def format_datetime(
433
+ dt: datetime, format_str: str | None = None, user_timezone: str | None = None
434
+ ) -> str:
435
+ """Format datetime for user display."""
436
+ return get_timezone_manager(user_timezone).format_for_user(dt, format_str)
437
+
438
+
439
+ def format_relative_time(dt: datetime, user_timezone: str | None = None) -> str:
440
+ """Format relative time representation."""
441
+ return get_timezone_manager(user_timezone).format_relative(dt)
442
+
443
+
444
+ def migrate_naive_datetime(dt: datetime, assumed_timezone: str = "UTC") -> datetime:
445
+ """Migrate a naive datetime to timezone-aware UTC.
446
+
447
+ This function is used during the migration process to convert existing
448
+ naive datetimes to timezone-aware datetimes.
449
+
450
+ Args:
451
+ dt: Naive datetime to migrate
452
+ assumed_timezone: Timezone to assume for the naive datetime
453
+
454
+ Returns:
455
+ Timezone-aware datetime in UTC
456
+ """
457
+ if dt.tzinfo is not None:
458
+ # Already timezone-aware, convert to UTC
459
+ return dt.astimezone(UTC)
460
+
461
+ # Assume the specified timezone and convert to UTC
462
+ return dt.replace(tzinfo=ZoneInfo(assumed_timezone)).astimezone(UTC)
463
+
464
+
465
+ def ensure_timezone_aware(dt: datetime, assumed_timezone: str = "UTC") -> datetime:
466
+ """Ensure a datetime is timezone-aware, converting naive datetimes as needed.
467
+
468
+ Args:
469
+ dt: Datetime that may be naive or timezone-aware
470
+ assumed_timezone: Timezone to assume for naive datetimes
471
+
472
+ Returns:
473
+ Timezone-aware datetime in UTC
474
+ """
475
+ if dt.tzinfo is not None:
476
+ # Already timezone-aware, convert to UTC for consistency
477
+ return dt.astimezone(UTC)
478
+
479
+ # Naive datetime - assume specified timezone and convert to UTC
480
+ return dt.replace(tzinfo=ZoneInfo(assumed_timezone)).astimezone(UTC)
@@ -0,0 +1,37 @@
1
+ """Unified Validation Framework for Roadmap CLI.
2
+
3
+ This module provides centralized validation utilities, field validators, and consistent
4
+ validation error handling to eliminate duplicate validation patterns across the codebase.
5
+
6
+ Key Features:
7
+ - Centralized field validation for common data types
8
+ - Reusable validation patterns for issues, milestones, and projects
9
+ - Consistent validation error reporting
10
+ - Extensible validation framework for custom validators
11
+ - Integration with error handling framework
12
+ """
13
+
14
+ from roadmap.common.validation.field_validator import FieldValidator
15
+ from roadmap.common.validation.result import ValidationResult
16
+ from roadmap.common.validation.roadmap_validator import RoadmapValidator
17
+ from roadmap.common.validation.schema_validator import SchemaValidator
18
+ from roadmap.common.validation.validators import (
19
+ ValidationType,
20
+ validate_and_raise,
21
+ validate_frontmatter_structure,
22
+ )
23
+
24
+ __all__ = [
25
+ "ValidationResult",
26
+ "FieldValidator",
27
+ "SchemaValidator",
28
+ "RoadmapValidator",
29
+ "ValidationType",
30
+ "validate_and_raise",
31
+ "validate_frontmatter_structure",
32
+ # Global instance for backward compatibility
33
+ "default_validator",
34
+ ]
35
+
36
+ # Global validator instance
37
+ default_validator = RoadmapValidator()
@@ -0,0 +1,104 @@
1
+ """Individual field validator with configurable rules."""
2
+
3
+ import re
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from .result import ValidationResult
8
+
9
+
10
+ class FieldValidator:
11
+ """Individual field validator with configurable rules."""
12
+
13
+ def __init__(
14
+ self,
15
+ field_name: str,
16
+ required: bool = False,
17
+ min_length: int | None = None,
18
+ max_length: int | None = None,
19
+ pattern: str | None = None,
20
+ enum_values: list[Any] | None = None,
21
+ custom_validator: Callable[[Any], tuple[bool, str]] | None = None,
22
+ allow_none: bool | None = None,
23
+ ):
24
+ """Initialize FieldValidator.
25
+
26
+ Args:
27
+ field_name: Name of the field to validate.
28
+ required: Whether the field is required.
29
+ min_length: Minimum length if applicable.
30
+ max_length: Maximum length if applicable.
31
+ pattern: Regex pattern to match against.
32
+ enum_values: List of allowed enum values.
33
+ custom_validator: Optional custom validation function.
34
+ allow_none: Whether None values are allowed.
35
+ """
36
+ self.field_name = field_name
37
+ self.required = required
38
+ self.min_length = min_length
39
+ self.max_length = max_length
40
+ self.pattern = re.compile(pattern) if pattern else None
41
+ self.enum_values = enum_values
42
+ self.custom_validator = custom_validator
43
+ # If allow_none is not specified, set it based on required status
44
+ self.allow_none = allow_none if allow_none is not None else not required
45
+
46
+ def validate(self, value: Any) -> ValidationResult:
47
+ """Validate a field value."""
48
+ result = ValidationResult(field=self.field_name)
49
+
50
+ # Check if value is None first
51
+ if value is None:
52
+ if self.required and not self.allow_none:
53
+ result.add_error(f"Field '{self.field_name}' is required")
54
+ return result
55
+
56
+ # Apply all validation rules
57
+ self._validate_required(value, result)
58
+ self._validate_enum(value, result)
59
+ self._validate_pattern(value, result)
60
+ self._validate_length(value, result)
61
+ self._validate_custom(value, result)
62
+
63
+ return result
64
+
65
+ def _validate_required(self, value: Any, result: ValidationResult) -> None:
66
+ """Validate that required field is not empty."""
67
+ if self.required and (value is None or value == ""):
68
+ result.add_error(f"Field '{self.field_name}' is required")
69
+
70
+ def _validate_enum(self, value: Any, result: ValidationResult) -> None:
71
+ """Validate that value is one of allowed enum values."""
72
+ if self.enum_values is not None and value not in self.enum_values:
73
+ result.add_error(
74
+ f"Field '{self.field_name}' must be one of: "
75
+ f"{', '.join(str(v) for v in self.enum_values)}"
76
+ )
77
+
78
+ def _validate_pattern(self, value: Any, result: ValidationResult) -> None:
79
+ """Validate that string value matches pattern."""
80
+ if self.pattern is not None and isinstance(value, str):
81
+ if not self.pattern.match(value):
82
+ result.add_error(f"Field '{self.field_name}' format is invalid")
83
+
84
+ def _validate_length(self, value: Any, result: ValidationResult) -> None:
85
+ """Validate string length constraints."""
86
+ if not isinstance(value, str):
87
+ return
88
+
89
+ if self.min_length is not None and len(value) < self.min_length:
90
+ result.add_error(
91
+ f"Field '{self.field_name}' must be at least {self.min_length} characters"
92
+ )
93
+
94
+ if self.max_length is not None and len(value) > self.max_length:
95
+ result.add_error(
96
+ f"Field '{self.field_name}' must be no more than {self.max_length} characters"
97
+ )
98
+
99
+ def _validate_custom(self, value: Any, result: ValidationResult) -> None:
100
+ """Validate using custom validator function."""
101
+ if self.custom_validator is not None:
102
+ is_valid, error_msg = self.custom_validator(value)
103
+ if not is_valid:
104
+ result.add_error(f"Field '{self.field_name}' {error_msg}")
@@ -0,0 +1,37 @@
1
+ """Validation result tracking and error aggregation."""
2
+
3
+
4
+ class ValidationResult:
5
+ """Result of a validation operation."""
6
+
7
+ def __init__(
8
+ self,
9
+ is_valid: bool = True,
10
+ errors: list[str] | None = None,
11
+ field: str | None = None,
12
+ ):
13
+ """Initialize ValidationResult.
14
+
15
+ Args:
16
+ is_valid: Whether validation passed.
17
+ errors: List of validation error messages.
18
+ field: Name of the field being validated.
19
+ """
20
+ self.is_valid = is_valid
21
+ self.errors = errors or []
22
+ self.field = field
23
+
24
+ def add_error(self, error: str):
25
+ """Add an error to the validation result."""
26
+ self.errors.append(error)
27
+ self.is_valid = False
28
+
29
+ def merge(self, other: "ValidationResult"):
30
+ """Merge another validation result into this one."""
31
+ if not other.is_valid:
32
+ self.is_valid = False
33
+ self.errors.extend(other.errors)
34
+
35
+ def __bool__(self) -> bool:
36
+ """Return whether validation passed."""
37
+ return self.is_valid