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,274 @@
1
+ """Issue domain model."""
2
+
3
+ import uuid
4
+ from datetime import UTC, datetime
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field, PrivateAttr, model_validator
8
+
9
+ from roadmap.common.constants import IssueType, Priority, Status
10
+ from roadmap.core.domain.comment import Comment
11
+
12
+
13
+ def now_utc():
14
+ """Get current UTC datetime with timezone awareness."""
15
+ from roadmap.common.utils.timezone_utils import now_utc as tz_now_utc
16
+
17
+ return tz_now_utc()
18
+
19
+
20
+ class Issue(BaseModel):
21
+ """Issue data model."""
22
+
23
+ model_config = {"ser_json_timedelta": "float"}
24
+
25
+ id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
26
+ title: str
27
+ headline: str = "" # Short summary for list views
28
+ priority: Priority = Priority.MEDIUM
29
+ status: Status = Status.TODO
30
+ archived: bool = False # Mark for remote deletion
31
+ issue_type: IssueType = IssueType.OTHER
32
+ milestone: str | None = None
33
+ labels: list[str] = Field(default_factory=list)
34
+ remote_ids: dict[str, str | int] = Field(
35
+ default_factory=dict,
36
+ description="Remote issue IDs keyed by backend name (e.g., {'github': 42, 'gitlab': 123})",
37
+ )
38
+ created: datetime = Field(default_factory=now_utc)
39
+ updated: datetime = Field(default_factory=now_utc)
40
+ assignee: str | None = None
41
+ content: str = "" # Markdown content
42
+ estimated_hours: float | None = None # Estimated time to complete in hours
43
+ due_date: datetime | None = None # When the issue should be completed
44
+ depends_on: list[str] = Field(
45
+ default_factory=list
46
+ ) # List of issue IDs this depends on
47
+ blocks: list[str] = Field(default_factory=list) # List of issue IDs this blocks
48
+ actual_start_date: datetime | None = None # When work actually started
49
+ actual_end_date: datetime | None = None # When work was completed
50
+ progress_percentage: float | None = None # Progress from 0.0 to 100.0
51
+ handoff_notes: str | None = None # Notes for task handoff
52
+ previous_assignee: str | None = None # Who was previously assigned
53
+ handoff_date: datetime | None = None # When the task was handed off
54
+ git_branches: list[str] = Field(
55
+ default_factory=list
56
+ ) # Git branches linked to this issue
57
+ git_commits: list[dict[str, Any]] = Field(
58
+ default_factory=list
59
+ ) # Git commits referencing this issue
60
+ completed_date: str | None = None # ISO string when issue was completed via Git
61
+ comments: list[Comment] = Field(
62
+ default_factory=list
63
+ ) # Comments on this issue for discussion
64
+ file_path: str | None = Field(
65
+ default=None, exclude=True
66
+ ) # Internal: absolute path where issue file is stored
67
+ github_sync_metadata: dict[str, Any] | None = Field(
68
+ default=None, exclude=True
69
+ ) # Internal: sync metadata tracking for GitHub integration
70
+
71
+ _modified: bool = PrivateAttr(default=False)
72
+ _local_changes: dict[str, Any] | None = PrivateAttr(default=None)
73
+
74
+ @model_validator(mode="before")
75
+ @classmethod
76
+ def migrate_github_issue_to_remote_ids(cls, data: Any) -> Any:
77
+ """Migrate legacy github_issue field to remote_ids dict.
78
+
79
+ Handles loading old YAML files that have github_issue field.
80
+ Validates and converts to proper types (string numbers to int).
81
+ """
82
+ if isinstance(data, dict) and "github_issue" in data:
83
+ github_issue = data.pop("github_issue")
84
+ if github_issue is not None:
85
+ # Initialize remote_ids if not present
86
+ if "remote_ids" not in data:
87
+ data["remote_ids"] = {}
88
+
89
+ # Convert string to int if needed
90
+ if isinstance(github_issue, str):
91
+ if not github_issue.isdigit():
92
+ raise ValueError(
93
+ "github_issue: must be an integer or numeric string"
94
+ )
95
+ github_issue = int(github_issue)
96
+ elif not isinstance(github_issue, int):
97
+ raise ValueError("github_issue: must be an integer")
98
+
99
+ # Validate positive
100
+ if github_issue <= 0:
101
+ raise ValueError("github_issue: must be a positive integer")
102
+
103
+ # Migrate github_issue to remote_ids
104
+ data["remote_ids"]["github"] = github_issue
105
+ return data
106
+
107
+ @property
108
+ def github_issue(self) -> int | str | None:
109
+ """Get GitHub issue ID from remote_ids dict.
110
+
111
+ Provides backwards compatibility for code accessing issue.github_issue.
112
+ """
113
+ if self.remote_ids and "github" in self.remote_ids:
114
+ return self.remote_ids["github"]
115
+ return None
116
+
117
+ @github_issue.setter
118
+ def github_issue(self, value: int | str | None) -> None:
119
+ """Set GitHub issue ID in remote_ids dict.
120
+
121
+ Provides backwards compatibility for code setting issue.github_issue.
122
+ Validates that the value is a positive integer.
123
+ """
124
+ if value is None:
125
+ # Remove github key if setting to None
126
+ if self.remote_ids and "github" in self.remote_ids:
127
+ del self.remote_ids["github"]
128
+ else:
129
+ # Convert string to int and validate
130
+ if isinstance(value, str):
131
+ if not value.isdigit():
132
+ raise ValueError(
133
+ "github_issue: must be an integer or numeric string"
134
+ )
135
+ value = int(value)
136
+ elif not isinstance(value, int):
137
+ raise ValueError("github_issue: must be an integer or numeric string")
138
+
139
+ if value <= 0:
140
+ raise ValueError("github_issue: must be a positive integer")
141
+
142
+ if not self.remote_ids:
143
+ self.remote_ids = {}
144
+ self.remote_ids["github"] = value
145
+
146
+ def model_dump(self, **kwargs) -> dict[str, Any]:
147
+ """Override model_dump to properly serialize remote_ids and github_issue.
148
+
149
+ Includes github_issue for backwards compatibility in API/JSON usage.
150
+ For file serialization (YAML), callers should remove github_issue field
151
+ to ensure files use the modern remote_ids format.
152
+ """
153
+ data = super().model_dump(**kwargs)
154
+
155
+ # Add github_issue for backwards compatibility in API responses
156
+ # Callers handling file serialization should remove this field
157
+ data["github_issue"] = self.github_issue
158
+
159
+ return data
160
+
161
+ def model_dump_json(self, **kwargs) -> str:
162
+ """Override model_dump_json to include github_issue property for backwards compatibility."""
163
+ # Get the dumped data which already includes github_issue via model_dump
164
+ data = self.model_dump()
165
+ # Use JSON serialization
166
+ import json
167
+
168
+ return json.dumps(data, default=str)
169
+
170
+ @property
171
+ def is_backlog(self) -> bool:
172
+ """Check if this issue is in the backlog (no milestone assigned)."""
173
+ return self.milestone is None or self.milestone == ""
174
+
175
+ @property
176
+ def milestone_name(self) -> str:
177
+ """Get the milestone name or 'Backlog' if none assigned."""
178
+ return self.milestone if self.milestone else "Backlog"
179
+
180
+ @property
181
+ def estimated_time_display(self) -> str:
182
+ """Get a human-readable display of estimated time."""
183
+ if self.estimated_hours is None:
184
+ return "Not estimated"
185
+
186
+ if self.estimated_hours < 1:
187
+ minutes = int(self.estimated_hours * 60)
188
+ return f"{minutes}m"
189
+ elif self.estimated_hours < 8:
190
+ return f"{self.estimated_hours:.1f}h"
191
+ else:
192
+ days = self.estimated_hours / 8 # Assuming 8-hour work days
193
+ return f"{days:.1f}d"
194
+
195
+ @property
196
+ def is_started(self) -> bool:
197
+ """Check if work has actually started on this issue."""
198
+ return self.actual_start_date is not None
199
+
200
+ @property
201
+ def is_completed(self) -> bool:
202
+ """Check if work has been completed on this issue."""
203
+ return self.actual_end_date is not None
204
+
205
+ @property
206
+ def progress_display(self) -> str:
207
+ """Get a human-readable display of progress as percentage (0-100%).
208
+
209
+ Returns:
210
+ - Empty string for Todo (not yet started)
211
+ - "100%" for Closed
212
+ - Explicit percentage if set
213
+ - "0%" for other statuses (in-progress, blocked, review) when no explicit progress
214
+ """
215
+ if self.progress_percentage is not None:
216
+ return f"{self.progress_percentage:.0f}%"
217
+
218
+ # Infer progress from status when not explicitly set
219
+ if self.status == Status.CLOSED:
220
+ return "100%"
221
+ elif self.status == Status.TODO:
222
+ return "" # Blank for todo - no progress tracking yet
223
+ else:
224
+ # For in-progress, blocked, review - default to 0% if no explicit progress
225
+ return "0%"
226
+
227
+ @property
228
+ def actual_duration_hours(self) -> float | None:
229
+ """Calculate actual duration if both start and end dates are set."""
230
+ if self.actual_start_date and self.actual_end_date:
231
+ delta = self.actual_end_date - self.actual_start_date
232
+ return delta.total_seconds() / 3600 # Convert to hours
233
+ return None
234
+
235
+ @property
236
+ def is_overdue(self) -> bool:
237
+ """Check if the issue is overdue based on estimates and actual start."""
238
+ if not self.actual_start_date or not self.estimated_hours:
239
+ return False
240
+
241
+ if self.actual_end_date:
242
+ # Already completed, check if it took longer than estimated
243
+ actual_hours = self.actual_duration_hours
244
+ return bool(actual_hours and actual_hours > self.estimated_hours)
245
+
246
+ # Still in progress, check if it's taking longer than estimated
247
+ elapsed = datetime.now(UTC) - self.actual_start_date
248
+ elapsed_hours = elapsed.total_seconds() / 3600
249
+ return elapsed_hours > self.estimated_hours
250
+
251
+ @property
252
+ def has_been_handed_off(self) -> bool:
253
+ """Check if this issue has been handed off to another person."""
254
+ return self.previous_assignee is not None and self.handoff_date is not None
255
+
256
+ @property
257
+ def handoff_context_summary(self) -> str:
258
+ """Get a summary of handoff context for display."""
259
+ if not self.has_been_handed_off:
260
+ return "No previous handoff"
261
+
262
+ if self.handoff_notes:
263
+ return f"From {self.previous_assignee}: {self.handoff_notes[:100]}..."
264
+ else:
265
+ return f"Handed off from {self.previous_assignee}"
266
+
267
+ @property
268
+ def filename(self) -> str:
269
+ """Generate filename for this issue."""
270
+ safe_title = "".join(
271
+ c for c in self.title if c.isalnum() or c in (" ", "-", "_")
272
+ ).strip()
273
+ safe_title = safe_title.replace(" ", "-").lower()
274
+ return f"{self.id}-{safe_title}.md"
@@ -0,0 +1,173 @@
1
+ """Milestone domain model."""
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from roadmap.common.constants import MilestoneStatus, RiskLevel, Status
8
+
9
+ from .comment import Comment
10
+
11
+ # Import timezone utilities with circular import protection
12
+ try:
13
+ from roadmap.common.utils.timezone_utils import now_utc
14
+ except ImportError:
15
+ # Fallback during module loading
16
+ def now_utc() -> datetime:
17
+ """Get current UTC datetime.
18
+
19
+ Returns:
20
+ Current datetime in UTC timezone.
21
+ """
22
+ return datetime.now(UTC)
23
+
24
+
25
+ class Milestone(BaseModel):
26
+ """Milestone data model."""
27
+
28
+ name: str
29
+ headline: str = "" # Short summary for list views
30
+ content: str = "" # Markdown content
31
+ due_date: datetime | None = None
32
+ status: MilestoneStatus = MilestoneStatus.OPEN
33
+ archived: bool = False # Mark for remote deletion
34
+ github_milestone: int | None = None
35
+ created: datetime = Field(default_factory=now_utc)
36
+ updated: datetime = Field(default_factory=now_utc)
37
+ project_id: str | None = (
38
+ None # Project this milestone belongs to (for relationship tracking)
39
+ )
40
+
41
+ # Automatic progress tracking fields
42
+ calculated_progress: float | None = None # Auto-calculated from issues
43
+ last_progress_update: datetime | None = None
44
+ completion_velocity: float | None = None # Issues/week
45
+ risk_level: RiskLevel = RiskLevel.LOW
46
+ actual_start_date: datetime | None = None
47
+ actual_end_date: datetime | None = None
48
+ comments: list[Comment] = Field(
49
+ default_factory=list
50
+ ) # Comments on this milestone for discussion
51
+
52
+ file_path: str | None = Field(
53
+ default=None, exclude=True
54
+ ) # Internal: absolute path where milestone file is stored
55
+
56
+ def get_issues(self, all_issues):
57
+ """Get all issues assigned to this milestone.
58
+
59
+ Args:
60
+ all_issues: List of Issue objects to filter
61
+
62
+ Returns:
63
+ List of issues assigned to this milestone
64
+ """
65
+ return [issue for issue in all_issues if issue.milestone == self.name]
66
+
67
+ def get_issue_count(self, all_issues):
68
+ """Get the count of issues assigned to this milestone."""
69
+ return len(self.get_issues(all_issues))
70
+
71
+ def get_completion_percentage(
72
+ self, all_issues, method: str = "effort_weighted"
73
+ ) -> float:
74
+ """Get the completion percentage of this milestone.
75
+
76
+ Args:
77
+ all_issues: List of all issues in the system
78
+ method: 'effort_weighted' or 'count_based'
79
+
80
+ Returns:
81
+ Completion percentage from 0.0 to 100.0
82
+ """
83
+ milestone_issues = self.get_issues(all_issues)
84
+ if not milestone_issues:
85
+ return 0.0
86
+
87
+ if method == "count_based":
88
+ # Simple count-based calculation
89
+ completed_issues = [
90
+ issue for issue in milestone_issues if issue.status == Status.CLOSED
91
+ ]
92
+ return (len(completed_issues) / len(milestone_issues)) * 100.0
93
+ else:
94
+ # Effort-weighted calculation (preferred)
95
+ total_effort = 0.0
96
+ completed_effort = 0.0
97
+
98
+ for issue in milestone_issues:
99
+ effort = (
100
+ issue.estimated_hours or 1.0
101
+ ) # Default to 1 hour if not estimated
102
+ total_effort += effort
103
+
104
+ if issue.status == Status.CLOSED:
105
+ completed_effort += effort
106
+ elif issue.progress_percentage is not None:
107
+ # Partial completion based on progress percentage
108
+ completed_effort += effort * (issue.progress_percentage / 100.0)
109
+
110
+ return (
111
+ (completed_effort / total_effort) * 100.0 if total_effort > 0 else 0.0
112
+ )
113
+
114
+ def get_total_estimated_hours(self, all_issues) -> float:
115
+ """Get the total estimated hours for all issues in this milestone."""
116
+ milestone_issues = self.get_issues(all_issues)
117
+ total_hours = 0.0
118
+ for issue in milestone_issues:
119
+ if issue.estimated_hours is not None:
120
+ total_hours += issue.estimated_hours
121
+ return total_hours
122
+
123
+ def get_remaining_estimated_hours(self, all_issues) -> float:
124
+ """Get the remaining estimated hours for incomplete issues in this milestone."""
125
+ milestone_issues = self.get_issues(all_issues)
126
+ remaining_hours = 0.0
127
+ for issue in milestone_issues:
128
+ if issue.status != Status.CLOSED and issue.estimated_hours is not None:
129
+ remaining_hours += issue.estimated_hours
130
+ return remaining_hours
131
+
132
+ def get_estimated_time_display(self, all_issues) -> str:
133
+ """Get a human-readable display of total estimated time."""
134
+ total_hours = self.get_total_estimated_hours(all_issues)
135
+ if total_hours == 0:
136
+ return "Not estimated"
137
+
138
+ if total_hours < 8:
139
+ return f"{total_hours:.1f}h"
140
+ else:
141
+ days = total_hours / 8 # Assuming 8-hour work days
142
+ return f"{days:.1f}d"
143
+
144
+ def update_automatic_fields(
145
+ self, all_issues, method: str = "effort_weighted"
146
+ ) -> None:
147
+ """Update all automatic progress tracking fields.
148
+
149
+ Args:
150
+ all_issues: List of all issues in the system
151
+ method: Calculation method ('effort_weighted' or 'count_based')
152
+ """
153
+ self.calculated_progress = self.get_completion_percentage(all_issues, method)
154
+ self.last_progress_update = datetime.now(UTC)
155
+ self.updated = datetime.now(UTC)
156
+
157
+ # Update status based on progress
158
+ if self.calculated_progress >= 100.0:
159
+ self.status = MilestoneStatus.CLOSED
160
+ if not self.actual_end_date:
161
+ self.actual_end_date = datetime.now(UTC)
162
+ elif self.calculated_progress > 0:
163
+ if self.status == MilestoneStatus.OPEN and not self.actual_start_date:
164
+ self.actual_start_date = datetime.now(UTC)
165
+
166
+ @property
167
+ def filename(self) -> str:
168
+ """Generate filename for this milestone."""
169
+ safe_name = "".join(
170
+ c for c in self.name if c.isalnum() or c in (" ", "-", "_")
171
+ ).strip()
172
+ safe_name = safe_name.replace(" ", "-").lower()
173
+ return f"{safe_name}.md"
@@ -0,0 +1 @@
1
+ """Port interfaces for domain layer."""
@@ -0,0 +1,24 @@
1
+ """Port for managing baseline state (canonical sync point)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from roadmap.core.services.sync.sync_state import SyncState
6
+
7
+
8
+ class IBaselineRepository(ABC):
9
+ """Port for managing baseline state (canonical sync point)."""
10
+
11
+ @abstractmethod
12
+ def load(self) -> SyncState | None:
13
+ """Load current baseline. Returns None if not set."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def save(self, baseline: SyncState) -> None:
18
+ """Save baseline state."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def clear(self) -> None:
23
+ """Clear baseline (reset sync state)."""
24
+ pass
@@ -0,0 +1,34 @@
1
+ """Port for reading/writing issues to persistent storage."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from roadmap.core.domain.issue import Issue
6
+
7
+
8
+ class IIssueRepository(ABC):
9
+ """Port for reading/writing issues to persistent storage."""
10
+
11
+ @abstractmethod
12
+ def get_all(self) -> list[Issue]:
13
+ """Get all issues including archived."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def get(self, issue_id: str) -> Issue:
18
+ """Get single issue by ID. Raises IssueNotFound."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def save(self, issue: Issue) -> None:
23
+ """Save or update an issue."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def delete(self, issue_id: str) -> None:
28
+ """Permanently delete an issue."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def archive(self, issue_id: str) -> None:
33
+ """Archive an issue (mark state=archived)."""
34
+ pass
@@ -0,0 +1,31 @@
1
+ """Port for communicating with remote systems (GitHub, etc)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from roadmap.common.result import Result
6
+ from roadmap.core.models.sync_models import SyncIssue
7
+ from roadmap.core.services.sync.sync_errors import SyncError
8
+
9
+
10
+ class IRemoteBackendPort(ABC):
11
+ """Port for communicating with remote systems (GitHub, etc)."""
12
+
13
+ @abstractmethod
14
+ def authenticate(self) -> Result[None, SyncError]:
15
+ """Authenticate with remote backend. Returns Ok(None) or Err."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_issues(self) -> Result[dict[str, SyncIssue], SyncError]:
20
+ """Fetch all remote issues. Key is issue ID, value is SyncIssue."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def push_issue(self, issue_id: str, _payload: dict) -> Result[None, SyncError]:
25
+ """Push local issue to remote."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def pull_issue(self, remote_id: str) -> Result[SyncIssue, SyncError]:
30
+ """Pull single remote issue."""
31
+ pass
@@ -0,0 +1,130 @@
1
+ """Project domain model."""
2
+
3
+ import uuid
4
+ from datetime import UTC, datetime
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from roadmap.common.constants import Priority, ProjectStatus
9
+
10
+ from .comment import Comment
11
+ from .milestone import MilestoneStatus, RiskLevel
12
+
13
+
14
+ class Project(BaseModel):
15
+ """Project data model."""
16
+
17
+ id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
18
+ name: str
19
+ headline: str = "" # Short summary for list views
20
+ content: str = "" # Markdown content
21
+ status: ProjectStatus = ProjectStatus.PLANNING
22
+ priority: Priority = Priority.MEDIUM
23
+ owner: str | None = None
24
+ start_date: datetime | None = None
25
+ target_end_date: datetime | None = None
26
+ actual_end_date: datetime | None = None
27
+ created: datetime = Field(default_factory=datetime.now)
28
+ updated: datetime = Field(default_factory=datetime.now)
29
+ milestones: list[str] = Field(default_factory=list) # List of milestone names
30
+ estimated_hours: float | None = None
31
+ actual_hours: float | None = None
32
+ repo_url: str | None = (
33
+ None # URL to the repository (for context/future cross-repo tooling)
34
+ )
35
+
36
+ # Automatic progress tracking fields
37
+ calculated_progress: float | None = None # Auto-calculated from milestones
38
+ last_progress_update: datetime | None = None
39
+ projected_end_date: datetime | None = None # Auto-calculated
40
+ schedule_variance: int | None = None # Days ahead/behind
41
+ completion_velocity: float | None = None # Milestones/week
42
+ risk_level: RiskLevel = RiskLevel.LOW
43
+ comments: list[Comment] = Field(
44
+ default_factory=list
45
+ ) # Comments on this project for discussion
46
+
47
+ file_path: str | None = Field(
48
+ default=None, exclude=True
49
+ ) # Internal: absolute path where project file is stored
50
+
51
+ def get_milestones(self, all_milestones):
52
+ """Get all milestones assigned to this project.
53
+
54
+ Args:
55
+ all_milestones: List of Milestone objects to filter
56
+
57
+ Returns:
58
+ List of milestones assigned to this project
59
+ """
60
+ return [
61
+ milestone
62
+ for milestone in all_milestones
63
+ if milestone.name in self.milestones
64
+ ]
65
+
66
+ def get_milestone_count(self, all_milestones) -> int:
67
+ """Get the count of milestones assigned to this project."""
68
+ return len(self.get_milestones(all_milestones))
69
+
70
+ def calculate_progress(self, all_milestones, all_issues) -> float:
71
+ """Calculate project progress from milestone completion (effort-weighted).
72
+
73
+ Args:
74
+ all_milestones: List of all milestones in the system
75
+ all_issues: List of all issues in the system
76
+
77
+ Returns:
78
+ Progress percentage from 0.0 to 100.0
79
+ """
80
+ project_milestones = self.get_milestones(all_milestones)
81
+ if not project_milestones:
82
+ return 0.0
83
+
84
+ # Use milestone effort (total estimated hours) as weight
85
+ total_weight = 0.0
86
+ completed_weight = 0.0
87
+
88
+ for milestone in project_milestones:
89
+ milestone_weight = milestone.get_total_estimated_hours(all_issues) or 1.0
90
+ total_weight += milestone_weight
91
+
92
+ if milestone.status == MilestoneStatus.CLOSED:
93
+ completed_weight += milestone_weight
94
+ else:
95
+ # Partial completion based on milestone progress
96
+ milestone_progress = (
97
+ milestone.get_completion_percentage(all_issues) / 100.0
98
+ )
99
+ completed_weight += milestone_weight * milestone_progress
100
+
101
+ return (completed_weight / total_weight) * 100.0 if total_weight > 0 else 0.0
102
+
103
+ def update_automatic_fields(self, all_milestones, all_issues) -> None:
104
+ """Update all automatic progress tracking fields.
105
+
106
+ Args:
107
+ all_milestones: List of all milestones in the system
108
+ all_issues: List of all issues in the system
109
+ """
110
+ self.calculated_progress = self.calculate_progress(all_milestones, all_issues)
111
+ self.last_progress_update = datetime.now(UTC)
112
+ self.updated = datetime.now(UTC)
113
+
114
+ # Update status based on progress
115
+ if self.calculated_progress >= 100.0:
116
+ self.status = ProjectStatus.COMPLETED
117
+ if not self.actual_end_date:
118
+ self.actual_end_date = datetime.now(UTC)
119
+ elif self.calculated_progress > 0:
120
+ if self.status == ProjectStatus.PLANNING:
121
+ self.status = ProjectStatus.ACTIVE
122
+
123
+ @property
124
+ def filename(self) -> str:
125
+ """Generate filename for this project."""
126
+ safe_name = "".join(
127
+ c for c in self.name if c.isalnum() or c in (" ", "-", "_")
128
+ ).strip()
129
+ safe_name = safe_name.replace(" ", "-").lower()
130
+ return f"{self.id}-{safe_name}.md"