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.
- roadmap/__init__.py +27 -0
- roadmap/adapters/__init__.py +1 -0
- roadmap/adapters/base_paginated_adapter.py +117 -0
- roadmap/adapters/cli/__init__.py +249 -0
- roadmap/adapters/cli/analysis/__init__.py +5 -0
- roadmap/adapters/cli/analysis/commands.py +370 -0
- roadmap/adapters/cli/analysis/presenter.py +125 -0
- roadmap/adapters/cli/archive_operations.py +86 -0
- roadmap/adapters/cli/cli_command_helpers.py +109 -0
- roadmap/adapters/cli/cli_confirmations.py +74 -0
- roadmap/adapters/cli/cli_error_handlers.py +146 -0
- roadmap/adapters/cli/cli_validators.py +39 -0
- roadmap/adapters/cli/commands/sync_status.py +308 -0
- roadmap/adapters/cli/comment/__init__.py +5 -0
- roadmap/adapters/cli/comment/commands.py +106 -0
- roadmap/adapters/cli/config/__init__.py +5 -0
- roadmap/adapters/cli/config/commands.py +198 -0
- roadmap/adapters/cli/console_exports.py +10 -0
- roadmap/adapters/cli/core.py +26 -0
- roadmap/adapters/cli/crud/__init__.py +47 -0
- roadmap/adapters/cli/crud/base_archive.py +208 -0
- roadmap/adapters/cli/crud/base_create.py +154 -0
- roadmap/adapters/cli/crud/base_delete.py +155 -0
- roadmap/adapters/cli/crud/base_restore.py +283 -0
- roadmap/adapters/cli/crud/base_update.py +193 -0
- roadmap/adapters/cli/crud/crud_helpers.py +206 -0
- roadmap/adapters/cli/crud/crud_utils.py +152 -0
- roadmap/adapters/cli/crud/entity_builders.py +374 -0
- roadmap/adapters/cli/data/__init__.py +8 -0
- roadmap/adapters/cli/data/commands.py +149 -0
- roadmap/adapters/cli/decorators.py +208 -0
- roadmap/adapters/cli/dtos/__init__.py +155 -0
- roadmap/adapters/cli/exception_handler.py +105 -0
- roadmap/adapters/cli/git/__init__.py +8 -0
- roadmap/adapters/cli/git/commands.py +475 -0
- roadmap/adapters/cli/git/handlers/__init__.py +1 -0
- roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
- roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
- roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
- roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
- roadmap/adapters/cli/git/hooks_config.py +327 -0
- roadmap/adapters/cli/git/status_display.py +158 -0
- roadmap/adapters/cli/health/__init__.py +8 -0
- roadmap/adapters/cli/health/db_integrity.py +204 -0
- roadmap/adapters/cli/health/enhancer.py +227 -0
- roadmap/adapters/cli/health/fixer.py +295 -0
- roadmap/adapters/cli/health/fixers/__init__.py +10 -0
- roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
- roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
- roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
- roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
- roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
- roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
- roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
- roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
- roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
- roadmap/adapters/cli/health/formatter.py +250 -0
- roadmap/adapters/cli/health/formatters.py +427 -0
- roadmap/adapters/cli/health/scan.py +293 -0
- roadmap/adapters/cli/init/__init__.py +5 -0
- roadmap/adapters/cli/init/commands.py +587 -0
- roadmap/adapters/cli/init.py +9 -0
- roadmap/adapters/cli/issues/__init__.py +70 -0
- roadmap/adapters/cli/issues/archive.py +137 -0
- roadmap/adapters/cli/issues/archive_class.py +253 -0
- roadmap/adapters/cli/issues/block.py +39 -0
- roadmap/adapters/cli/issues/close.py +216 -0
- roadmap/adapters/cli/issues/comment.py +178 -0
- roadmap/adapters/cli/issues/create.py +162 -0
- roadmap/adapters/cli/issues/delete.py +31 -0
- roadmap/adapters/cli/issues/deps.py +161 -0
- roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
- roadmap/adapters/cli/issues/link.py +308 -0
- roadmap/adapters/cli/issues/list.py +402 -0
- roadmap/adapters/cli/issues/lookup.py +96 -0
- roadmap/adapters/cli/issues/progress.py +102 -0
- roadmap/adapters/cli/issues/restore.py +96 -0
- roadmap/adapters/cli/issues/restore_class.py +57 -0
- roadmap/adapters/cli/issues/start.py +165 -0
- roadmap/adapters/cli/issues/sync_status.py +374 -0
- roadmap/adapters/cli/issues/unblock.py +43 -0
- roadmap/adapters/cli/issues/unlink.py +104 -0
- roadmap/adapters/cli/issues/update.py +120 -0
- roadmap/adapters/cli/issues/view.py +36 -0
- roadmap/adapters/cli/layout.py +220 -0
- roadmap/adapters/cli/mappers.py +200 -0
- roadmap/adapters/cli/milestones/__init__.py +52 -0
- roadmap/adapters/cli/milestones/archive.py +125 -0
- roadmap/adapters/cli/milestones/archive_class.py +236 -0
- roadmap/adapters/cli/milestones/assign.py +62 -0
- roadmap/adapters/cli/milestones/close.py +177 -0
- roadmap/adapters/cli/milestones/create.py +131 -0
- roadmap/adapters/cli/milestones/delete.py +27 -0
- roadmap/adapters/cli/milestones/kanban.py +123 -0
- roadmap/adapters/cli/milestones/list.py +87 -0
- roadmap/adapters/cli/milestones/recalculate.py +79 -0
- roadmap/adapters/cli/milestones/restore.py +87 -0
- roadmap/adapters/cli/milestones/restore_class.py +99 -0
- roadmap/adapters/cli/milestones/update.py +61 -0
- roadmap/adapters/cli/milestones/view.py +142 -0
- roadmap/adapters/cli/output_manager.py +319 -0
- roadmap/adapters/cli/presentation/base_presenter.py +121 -0
- roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
- roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
- roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
- roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
- roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
- roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
- roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
- roadmap/adapters/cli/presentation/project_presenter.py +233 -0
- roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
- roadmap/adapters/cli/presentation/table_builders.py +95 -0
- roadmap/adapters/cli/projects/__init__.py +42 -0
- roadmap/adapters/cli/projects/archive.py +127 -0
- roadmap/adapters/cli/projects/archive_class.py +96 -0
- roadmap/adapters/cli/projects/close.py +94 -0
- roadmap/adapters/cli/projects/create.py +50 -0
- roadmap/adapters/cli/projects/delete.py +27 -0
- roadmap/adapters/cli/projects/list.py +227 -0
- roadmap/adapters/cli/projects/restore.py +87 -0
- roadmap/adapters/cli/projects/restore_class.py +56 -0
- roadmap/adapters/cli/projects/update.py +57 -0
- roadmap/adapters/cli/projects/view.py +157 -0
- roadmap/adapters/cli/services/__init__.py +1 -0
- roadmap/adapters/cli/services/daily_summary_service.py +313 -0
- roadmap/adapters/cli/services/export_manager.py +268 -0
- roadmap/adapters/cli/services/milestone_list_service.py +266 -0
- roadmap/adapters/cli/services/project_initialization_service.py +29 -0
- roadmap/adapters/cli/services/project_status_service.py +462 -0
- roadmap/adapters/cli/services/sync_service.py +62 -0
- roadmap/adapters/cli/status.py +408 -0
- roadmap/adapters/cli/styling.py +21 -0
- roadmap/adapters/cli/sync.py +1117 -0
- roadmap/adapters/cli/sync_context.py +432 -0
- roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
- roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
- roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
- roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
- roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
- roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
- roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
- roadmap/adapters/cli/sync_metrics_command.py +190 -0
- roadmap/adapters/cli/sync_presenter.py +58 -0
- roadmap/adapters/cli/sync_validation.py +295 -0
- roadmap/adapters/cli/today.py +52 -0
- roadmap/adapters/cli/utils/click_options.py +38 -0
- roadmap/adapters/git/__init__.py +1 -0
- roadmap/adapters/git/git.py +208 -0
- roadmap/adapters/git/git_branch_manager.py +400 -0
- roadmap/adapters/git/git_command_executor.py +56 -0
- roadmap/adapters/git/git_commit_analyzer.py +334 -0
- roadmap/adapters/git/git_hooks.py +22 -0
- roadmap/adapters/git/git_hooks_manager.py +455 -0
- roadmap/adapters/git/git_repository_info.py +65 -0
- roadmap/adapters/git/hook_installer.py +106 -0
- roadmap/adapters/git/hook_registry.py +97 -0
- roadmap/adapters/git/hook_script_generator.py +58 -0
- roadmap/adapters/git/sync_monitor.py +409 -0
- roadmap/adapters/git/workflow_automation.py +286 -0
- roadmap/adapters/github/__init__.py +1 -0
- roadmap/adapters/github/github.py +360 -0
- roadmap/adapters/github/handlers/__init__.py +17 -0
- roadmap/adapters/github/handlers/base.py +337 -0
- roadmap/adapters/github/handlers/collaborators.py +165 -0
- roadmap/adapters/github/handlers/comments.py +118 -0
- roadmap/adapters/github/handlers/issues.py +150 -0
- roadmap/adapters/github/handlers/labels.py +164 -0
- roadmap/adapters/github/handlers/milestones.py +104 -0
- roadmap/adapters/persistence/__init__.py +35 -0
- roadmap/adapters/persistence/conflict_resolver.py +190 -0
- roadmap/adapters/persistence/database_manager.py +492 -0
- roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
- roadmap/adapters/persistence/file_locking.py +348 -0
- roadmap/adapters/persistence/file_parser.py +92 -0
- roadmap/adapters/persistence/file_synchronizer.py +107 -0
- roadmap/adapters/persistence/focused_managers.py +140 -0
- roadmap/adapters/persistence/git_history.py +330 -0
- roadmap/adapters/persistence/parser/__init__.py +20 -0
- roadmap/adapters/persistence/parser/frontmatter.py +152 -0
- roadmap/adapters/persistence/parser/issue.py +245 -0
- roadmap/adapters/persistence/parser/milestone.py +146 -0
- roadmap/adapters/persistence/parser/project.py +106 -0
- roadmap/adapters/persistence/persistence.py +288 -0
- roadmap/adapters/persistence/repositories/__init__.py +15 -0
- roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
- roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
- roadmap/adapters/persistence/repositories/project_repository.py +168 -0
- roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
- roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
- roadmap/adapters/persistence/storage/__init__.py +36 -0
- roadmap/adapters/persistence/storage/conflicts.py +101 -0
- roadmap/adapters/persistence/storage/connection_manager.py +79 -0
- roadmap/adapters/persistence/storage/issue_storage.py +89 -0
- roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
- roadmap/adapters/persistence/storage/project_storage.py +102 -0
- roadmap/adapters/persistence/storage/queries.py +227 -0
- roadmap/adapters/persistence/storage/state_manager.py +652 -0
- roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
- roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
- roadmap/adapters/persistence/sync_orchestrator.py +293 -0
- roadmap/adapters/persistence/sync_state_tracker.py +86 -0
- roadmap/adapters/persistence/yaml_repositories.py +711 -0
- roadmap/adapters/sync/__init__.py +21 -0
- roadmap/adapters/sync/backend_factory.py +108 -0
- roadmap/adapters/sync/backends/__init__.py +15 -0
- roadmap/adapters/sync/backends/converters.py +209 -0
- roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
- roadmap/adapters/sync/backends/github_client.py +99 -0
- roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
- roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
- roadmap/adapters/sync/backends/services/__init__.py +1 -0
- roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
- roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
- roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
- roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
- roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
- roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
- roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
- roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
- roadmap/adapters/sync/services/__init__.py +11 -0
- roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
- roadmap/adapters/sync/services/conflict_converter.py +85 -0
- roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
- roadmap/adapters/sync/services/issue_state_service.py +243 -0
- roadmap/adapters/sync/services/local_change_filter.py +129 -0
- roadmap/adapters/sync/services/pull_result_processor.py +71 -0
- roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
- roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
- roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
- roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
- roadmap/adapters/sync/services/sync_linking_service.py +329 -0
- roadmap/adapters/sync/services/sync_report_service.py +62 -0
- roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
- roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
- roadmap/adapters/sync/sync_merge_engine.py +416 -0
- roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
- roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
- roadmap/application/services/__init__.py +1 -0
- roadmap/application/services/deduplicate_service.py +517 -0
- roadmap/application/use_cases/__init__.py +1 -0
- roadmap/common/__init__.py +308 -0
- roadmap/common/cache.py +209 -0
- roadmap/common/cli_errors.py +147 -0
- roadmap/common/configuration/__init__.py +33 -0
- roadmap/common/configuration/config_loader.py +264 -0
- roadmap/common/configuration/config_manager.py +164 -0
- roadmap/common/configuration/config_schema.py +145 -0
- roadmap/common/configuration/github/__init__.py +9 -0
- roadmap/common/configuration/github/config_manager.py +154 -0
- roadmap/common/configuration/github/token_resolver.py +84 -0
- roadmap/common/console.py +184 -0
- roadmap/common/constants.py +134 -0
- roadmap/common/datetime_parser.py +319 -0
- roadmap/common/error_formatter.py +84 -0
- roadmap/common/errors/__init__.py +143 -0
- roadmap/common/errors/error_base.py +73 -0
- roadmap/common/errors/error_file.py +82 -0
- roadmap/common/errors/error_git.py +67 -0
- roadmap/common/errors/error_handler.py +166 -0
- roadmap/common/errors/error_network.py +90 -0
- roadmap/common/errors/error_security.py +78 -0
- roadmap/common/errors/error_standards.py +561 -0
- roadmap/common/errors/error_validation.py +111 -0
- roadmap/common/errors/exceptions.py +432 -0
- roadmap/common/formatters/__init__.py +29 -0
- roadmap/common/formatters/base_table_formatter.py +96 -0
- roadmap/common/formatters/export/__init__.py +7 -0
- roadmap/common/formatters/export/issue_exporter.py +97 -0
- roadmap/common/formatters/helpers.py +28 -0
- roadmap/common/formatters/kanban/__init__.py +9 -0
- roadmap/common/formatters/kanban/layout.py +48 -0
- roadmap/common/formatters/kanban/organizer.py +81 -0
- roadmap/common/formatters/output/__init__.py +15 -0
- roadmap/common/formatters/tables/__init__.py +11 -0
- roadmap/common/formatters/tables/column_factory.py +312 -0
- roadmap/common/formatters/tables/issue_table.py +276 -0
- roadmap/common/formatters/tables/milestone_table.py +247 -0
- roadmap/common/formatters/tables/project_table.py +195 -0
- roadmap/common/formatters/text/__init__.py +57 -0
- roadmap/common/formatters/text/basic.py +217 -0
- roadmap/common/formatters/text/display.py +42 -0
- roadmap/common/formatters/text/duration.py +36 -0
- roadmap/common/formatters/text/operations.py +325 -0
- roadmap/common/formatters/text/status_badges.py +57 -0
- roadmap/common/initialization/github/__init__.py +11 -0
- roadmap/common/initialization/github/setup_service.py +238 -0
- roadmap/common/initialization/github/setup_validator.py +78 -0
- roadmap/common/logging/__init__.py +113 -0
- roadmap/common/logging/audit_logging.py +5 -0
- roadmap/common/logging/correlation.py +45 -0
- roadmap/common/logging/decorators.py +267 -0
- roadmap/common/logging/error_logging.py +217 -0
- roadmap/common/logging/formatters.py +124 -0
- roadmap/common/logging/loggers.py +68 -0
- roadmap/common/logging/performance_tracking.py +267 -0
- roadmap/common/logging/utils.py +335 -0
- roadmap/common/models/__init__.py +44 -0
- roadmap/common/models/cli_models.py +118 -0
- roadmap/common/models/config_models.py +199 -0
- roadmap/common/models/output_models.py +381 -0
- roadmap/common/observability/__init__.py +28 -0
- roadmap/common/observability/instrumentation.py +103 -0
- roadmap/common/observability/observability.py +76 -0
- roadmap/common/observability/otel_init.py +109 -0
- roadmap/common/output_formatter.py +393 -0
- roadmap/common/progress.py +379 -0
- roadmap/common/result.py +396 -0
- roadmap/common/security/__init__.py +48 -0
- roadmap/common/security/exceptions.py +13 -0
- roadmap/common/security/export_cleanup.py +79 -0
- roadmap/common/security/file_operations.py +104 -0
- roadmap/common/security/filename_sanitization.py +54 -0
- roadmap/common/security/logging.py +27 -0
- roadmap/common/security/path_validation.py +137 -0
- roadmap/common/security/temp_files.py +41 -0
- roadmap/common/services/__init__.py +65 -0
- roadmap/common/services/decorators.py +183 -0
- roadmap/common/services/logging_utils.py +208 -0
- roadmap/common/services/metrics.py +161 -0
- roadmap/common/services/performance.py +219 -0
- roadmap/common/services/profiling.py +288 -0
- roadmap/common/services/retry.py +288 -0
- roadmap/common/status_style_manager.py +114 -0
- roadmap/common/union_find.py +88 -0
- roadmap/common/update_constants.py +13 -0
- roadmap/common/utils/__init__.py +65 -0
- roadmap/common/utils/cli_helpers.py +381 -0
- roadmap/common/utils/file_utils.py +440 -0
- roadmap/common/utils/path_utils.py +38 -0
- roadmap/common/utils/status_utils.py +130 -0
- roadmap/common/utils/timezone_utils.py +480 -0
- roadmap/common/validation/__init__.py +37 -0
- roadmap/common/validation/field_validator.py +104 -0
- roadmap/common/validation/result.py +37 -0
- roadmap/common/validation/roadmap_validator.py +310 -0
- roadmap/common/validation/schema_validator.py +36 -0
- roadmap/common/validation/validators.py +50 -0
- roadmap/core/__init__.py +1 -0
- roadmap/core/domain/__init__.py +30 -0
- roadmap/core/domain/comment.py +24 -0
- roadmap/core/domain/health.py +15 -0
- roadmap/core/domain/issue.py +274 -0
- roadmap/core/domain/milestone.py +173 -0
- roadmap/core/domain/ports/__init__.py +1 -0
- roadmap/core/domain/ports/baseline_repository.py +24 -0
- roadmap/core/domain/ports/issue_repository.py +34 -0
- roadmap/core/domain/ports/remote_backend_port.py +31 -0
- roadmap/core/domain/project.py +130 -0
- roadmap/core/interfaces/__init__.py +109 -0
- roadmap/core/interfaces/assignee_validator.py +40 -0
- roadmap/core/interfaces/backend_factory.py +175 -0
- roadmap/core/interfaces/github.py +102 -0
- roadmap/core/interfaces/parsers.py +140 -0
- roadmap/core/interfaces/persistence.py +45 -0
- roadmap/core/interfaces/repositories.py +174 -0
- roadmap/core/interfaces/state_managers.py +111 -0
- roadmap/core/interfaces/state_storage.py +64 -0
- roadmap/core/interfaces/sync_backend.py +275 -0
- roadmap/core/interfaces/sync_services.py +119 -0
- roadmap/core/interfaces/sync_validators.py +180 -0
- roadmap/core/models/__init__.py +31 -0
- roadmap/core/models/sync_models.py +162 -0
- roadmap/core/models.py +115 -0
- roadmap/core/observability/__init__.py +17 -0
- roadmap/core/observability/sync_metrics.py +699 -0
- roadmap/core/repositories.py +200 -0
- roadmap/core/services/__init__.py +313 -0
- roadmap/core/services/baseline/__init__.py +1 -0
- roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
- roadmap/core/services/baseline/baseline_retriever.py +63 -0
- roadmap/core/services/baseline/baseline_selector.py +248 -0
- roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
- roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
- roadmap/core/services/comment/__init__.py +1 -0
- roadmap/core/services/comment/comment_service.py +277 -0
- roadmap/core/services/git/__init__.py +1 -0
- roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
- roadmap/core/services/github/__init__.py +1 -0
- roadmap/core/services/github/github_change_detector.py +162 -0
- roadmap/core/services/github/github_config_validator.py +148 -0
- roadmap/core/services/github/github_conflict_detector.py +179 -0
- roadmap/core/services/github/github_entity_classifier.py +51 -0
- roadmap/core/services/github/github_integration_service.py +439 -0
- roadmap/core/services/github/github_issue_client.py +346 -0
- roadmap/core/services/health/__init__.py +1 -0
- roadmap/core/services/health/backup_cleanup_service.py +202 -0
- roadmap/core/services/health/data_integrity_validator_service.py +90 -0
- roadmap/core/services/health/entity_health_scanner.py +460 -0
- roadmap/core/services/health/file_repair_service.py +193 -0
- roadmap/core/services/health/health_check_service.py +166 -0
- roadmap/core/services/health/health_models.py +80 -0
- roadmap/core/services/health/infrastructure_validator_service.py +328 -0
- roadmap/core/services/health/issue_health_scanner.py +263 -0
- roadmap/core/services/helpers/__init__.py +18 -0
- roadmap/core/services/helpers/status_change_helpers.py +88 -0
- roadmap/core/services/initialization/__init__.py +19 -0
- roadmap/core/services/initialization/utils.py +104 -0
- roadmap/core/services/initialization/validator.py +78 -0
- roadmap/core/services/initialization/workflow.py +182 -0
- roadmap/core/services/initialization_service.py +111 -0
- roadmap/core/services/issue/__init__.py +1 -0
- roadmap/core/services/issue/assignee_validation_service.py +312 -0
- roadmap/core/services/issue/issue_creation_service.py +309 -0
- roadmap/core/services/issue/issue_filter_service.py +302 -0
- roadmap/core/services/issue/issue_matching_service.py +127 -0
- roadmap/core/services/issue/issue_service.py +765 -0
- roadmap/core/services/issue/issue_update_service.py +144 -0
- roadmap/core/services/issue/start_issue_service.py +174 -0
- roadmap/core/services/issue_helpers/__init__.py +18 -0
- roadmap/core/services/issue_helpers/issue_filters.py +296 -0
- roadmap/core/services/milestone_service.py +438 -0
- roadmap/core/services/project/__init__.py +1 -0
- roadmap/core/services/project/project_service.py +434 -0
- roadmap/core/services/project/project_status_service.py +125 -0
- roadmap/core/services/project_init/__init__.py +22 -0
- roadmap/core/services/project_init/context_detection.py +146 -0
- roadmap/core/services/project_init/creation.py +76 -0
- roadmap/core/services/project_init/detection.py +51 -0
- roadmap/core/services/project_init/template.py +228 -0
- roadmap/core/services/status_change_service.py +88 -0
- roadmap/core/services/sync/__init__.py +1 -0
- roadmap/core/services/sync/batch_processor.py +290 -0
- roadmap/core/services/sync/conflict_resolver.py +154 -0
- roadmap/core/services/sync/dependency_resolver.py +367 -0
- roadmap/core/services/sync/duplicate_detector.py +771 -0
- roadmap/core/services/sync/duplicate_resolver.py +315 -0
- roadmap/core/services/sync/error_classification.py +470 -0
- roadmap/core/services/sync/performance.py +263 -0
- roadmap/core/services/sync/sync_change_computer.py +211 -0
- roadmap/core/services/sync/sync_checkpoint.py +348 -0
- roadmap/core/services/sync/sync_conflict_detector.py +147 -0
- roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
- roadmap/core/services/sync/sync_errors.py +383 -0
- roadmap/core/services/sync/sync_key_normalizer.py +222 -0
- roadmap/core/services/sync/sync_metadata_service.py +361 -0
- roadmap/core/services/sync/sync_plan.py +171 -0
- roadmap/core/services/sync/sync_plan_executor.py +447 -0
- roadmap/core/services/sync/sync_report.py +296 -0
- roadmap/core/services/sync/sync_state.py +173 -0
- roadmap/core/services/sync/sync_state_comparator.py +690 -0
- roadmap/core/services/sync/sync_state_manager.py +431 -0
- roadmap/core/services/sync/sync_state_normalizer.py +98 -0
- roadmap/core/services/sync/sync_three_way.py +78 -0
- roadmap/core/services/sync/three_way_merger.py +178 -0
- roadmap/core/services/utils/__init__.py +1 -0
- roadmap/core/services/utils/configuration_service.py +113 -0
- roadmap/core/services/utils/critical_path_calculator.py +418 -0
- roadmap/core/services/utils/dependency_analyzer.py +360 -0
- roadmap/core/services/utils/field_conflict_detector.py +126 -0
- roadmap/core/services/utils/remote_fetcher.py +202 -0
- roadmap/core/services/utils/remote_state_normalizer.py +110 -0
- roadmap/core/services/utils/retry_policy.py +51 -0
- roadmap/core/services/validator_base.py +130 -0
- roadmap/core/services/validators/__init__.py +28 -0
- roadmap/core/services/validators/_utils.py +20 -0
- roadmap/core/services/validators/archivable_issues_validator.py +71 -0
- roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
- roadmap/core/services/validators/backup_validator.py +89 -0
- roadmap/core/services/validators/data_integrity_validator.py +84 -0
- roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
- roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
- roadmap/core/services/validators/folder_structure_validator.py +165 -0
- roadmap/core/services/validators/health_status_utils.py +28 -0
- roadmap/core/services/validators/milestone_naming_validator.py +146 -0
- roadmap/core/services/validators/missing_headlines_validator.py +97 -0
- roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
- roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
- roadmap/core/utils/git_remote_parser.py +83 -0
- roadmap/core/utils/github_urls.py +66 -0
- roadmap/domain/validation.py +30 -0
- roadmap/infrastructure/__init__.py +55 -0
- roadmap/infrastructure/coordination/__init__.py +8 -0
- roadmap/infrastructure/coordination/coordinator_params.py +82 -0
- roadmap/infrastructure/coordination/core.py +446 -0
- roadmap/infrastructure/coordination/git_coordinator.py +160 -0
- roadmap/infrastructure/coordination/initialization.py +348 -0
- roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
- roadmap/infrastructure/coordination/issue_operations.py +456 -0
- roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
- roadmap/infrastructure/coordination/milestone_operations.py +201 -0
- roadmap/infrastructure/coordination/project_coordinator.py +113 -0
- roadmap/infrastructure/coordination/project_operations.py +144 -0
- roadmap/infrastructure/coordination/team_coordinator.py +107 -0
- roadmap/infrastructure/coordination/user_operations.py +150 -0
- roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
- roadmap/infrastructure/coordination_gateway.py +152 -0
- roadmap/infrastructure/git/__init__.py +6 -0
- roadmap/infrastructure/git/git_integration_ops.py +234 -0
- roadmap/infrastructure/git_gateway.py +35 -0
- roadmap/infrastructure/github_gateway.py +52 -0
- roadmap/infrastructure/maintenance/__init__.py +5 -0
- roadmap/infrastructure/maintenance/cleanup.py +483 -0
- roadmap/infrastructure/observability/__init__.py +7 -0
- roadmap/infrastructure/observability/health.py +196 -0
- roadmap/infrastructure/observability/health_formatter.py +73 -0
- roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
- roadmap/infrastructure/persistence_gateway.py +164 -0
- roadmap/infrastructure/security/__init__.py +1 -0
- roadmap/infrastructure/security/credentials.py +489 -0
- roadmap/infrastructure/sync_gateway.py +94 -0
- roadmap/infrastructure/validation/__init__.py +7 -0
- roadmap/infrastructure/validation/file_enumeration.py +219 -0
- roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
- roadmap/infrastructure/validation/github_validator.py +101 -0
- roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
- roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
- roadmap/infrastructure/validation_gateway.py +96 -0
- roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
- roadmap/settings.py +345 -0
- roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
- roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
- roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
- roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
- roadmap/version.py +381 -0
- roadmap_cli-0.1.1.dist-info/METADATA +448 -0
- roadmap_cli-0.1.1.dist-info/RECORD +521 -0
- roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
- roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
- 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"
|