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,456 @@
|
|
|
1
|
+
"""Issue Operations Module - Handles all issue-related CRUD operations.
|
|
2
|
+
|
|
3
|
+
This module encapsulates issue management responsibilities extracted from RoadmapCore,
|
|
4
|
+
including creating, reading, updating, and deleting issues, as well as issue assignment
|
|
5
|
+
and milestone-related queries.
|
|
6
|
+
|
|
7
|
+
Responsibilities:
|
|
8
|
+
- Issue CRUD operations (create, list, get, update, delete)
|
|
9
|
+
- Issue assignment to milestones
|
|
10
|
+
- Issue queries (backlog, by milestone, grouped by milestone)
|
|
11
|
+
- Issue movement between milestones
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from difflib import get_close_matches
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from roadmap.adapters.persistence.parser import IssueParser
|
|
18
|
+
from roadmap.common.cache import SessionCache
|
|
19
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
20
|
+
from roadmap.core.domain import (
|
|
21
|
+
Issue,
|
|
22
|
+
IssueType,
|
|
23
|
+
Priority,
|
|
24
|
+
Status,
|
|
25
|
+
)
|
|
26
|
+
from roadmap.core.models import IssueCreateServiceParams, IssueUpdateServiceParams
|
|
27
|
+
from roadmap.core.services import IssueService
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IssueOperations:
|
|
31
|
+
"""Manager for issue-related operations."""
|
|
32
|
+
|
|
33
|
+
# Cache TTL in seconds
|
|
34
|
+
_cache_ttl = 60
|
|
35
|
+
|
|
36
|
+
def __init__(self, issue_service: IssueService, issues_dir: Path):
|
|
37
|
+
"""Initialize issue operations manager.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
issue_service: The IssueService instance for database operations
|
|
41
|
+
issues_dir: Path to the issues directory
|
|
42
|
+
"""
|
|
43
|
+
self.issue_service = issue_service
|
|
44
|
+
self.issues_dir = issues_dir
|
|
45
|
+
# Cache for get_issues_by_milestone results (instance-level to avoid test pollution)
|
|
46
|
+
self._milestone_cache = SessionCache()
|
|
47
|
+
|
|
48
|
+
def create_issue(
|
|
49
|
+
self,
|
|
50
|
+
title: str,
|
|
51
|
+
priority: Priority = Priority.MEDIUM,
|
|
52
|
+
issue_type: IssueType = IssueType.OTHER,
|
|
53
|
+
milestone: str | None = None,
|
|
54
|
+
labels: list[str] | None = None,
|
|
55
|
+
assignee: str | None = None,
|
|
56
|
+
estimated_hours: float | None = None,
|
|
57
|
+
depends_on: list[str] | None = None,
|
|
58
|
+
blocks: list[str] | None = None,
|
|
59
|
+
status: str | None = None,
|
|
60
|
+
content: str | None = None,
|
|
61
|
+
) -> Issue:
|
|
62
|
+
"""Create a new issue.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
title: Issue title
|
|
66
|
+
priority: Issue priority (default: MEDIUM)
|
|
67
|
+
issue_type: Type of issue (default: OTHER)
|
|
68
|
+
milestone: Milestone name (optional)
|
|
69
|
+
labels: List of labels (optional)
|
|
70
|
+
assignee: Assigned user (optional)
|
|
71
|
+
estimated_hours: Estimated hours to complete (optional)
|
|
72
|
+
depends_on: List of issue IDs this depends on (optional)
|
|
73
|
+
blocks: List of issue IDs this blocks (optional)
|
|
74
|
+
status: Issue status (optional, defaults to TODO)
|
|
75
|
+
content: Issue content/description (optional)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Created Issue object
|
|
79
|
+
"""
|
|
80
|
+
# Create service params dataclass from individual arguments
|
|
81
|
+
params = IssueCreateServiceParams(
|
|
82
|
+
title=title,
|
|
83
|
+
priority=priority.value
|
|
84
|
+
if isinstance(priority, Priority)
|
|
85
|
+
else str(priority),
|
|
86
|
+
issue_type=issue_type.value
|
|
87
|
+
if isinstance(issue_type, IssueType)
|
|
88
|
+
else str(issue_type),
|
|
89
|
+
milestone=milestone,
|
|
90
|
+
labels=labels or [],
|
|
91
|
+
assignee=assignee,
|
|
92
|
+
content=content,
|
|
93
|
+
estimate=estimated_hours,
|
|
94
|
+
depends_on=depends_on or [],
|
|
95
|
+
blocks=blocks or [],
|
|
96
|
+
status=status or "todo",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
result = self.issue_service.create_issue(params)
|
|
100
|
+
|
|
101
|
+
# Invalidate milestone cache after creation
|
|
102
|
+
self._milestone_cache.clear()
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
def list_issues(
|
|
107
|
+
self,
|
|
108
|
+
milestone: str | None = None,
|
|
109
|
+
status: Status | None = None,
|
|
110
|
+
priority: Priority | None = None,
|
|
111
|
+
issue_type: IssueType | None = None,
|
|
112
|
+
assignee: str | None = None,
|
|
113
|
+
) -> list[Issue]:
|
|
114
|
+
"""List issues with optional filtering.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
milestone: Filter by milestone name (optional)
|
|
118
|
+
status: Filter by issue status (optional)
|
|
119
|
+
priority: Filter by priority (optional)
|
|
120
|
+
issue_type: Filter by issue type (optional)
|
|
121
|
+
assignee: Filter by assignee (optional)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of Issue objects matching the filters
|
|
125
|
+
"""
|
|
126
|
+
return self.issue_service.list_issues(
|
|
127
|
+
milestone=milestone,
|
|
128
|
+
status=status,
|
|
129
|
+
priority=priority,
|
|
130
|
+
issue_type=issue_type,
|
|
131
|
+
assignee=assignee,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def list_all_including_archived(self) -> list[Issue]:
|
|
135
|
+
"""List all issues including archived ones.
|
|
136
|
+
|
|
137
|
+
Used by sync operations that need to compare both active and archived issues.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of all Issue objects (both active and archived)
|
|
141
|
+
"""
|
|
142
|
+
return self.issue_service.list_all_including_archived()
|
|
143
|
+
|
|
144
|
+
def get_issue(self, issue_id: str) -> Issue | None:
|
|
145
|
+
"""Get a specific issue by ID.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
issue_id: The issue ID to retrieve
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Issue object if found, None otherwise
|
|
152
|
+
"""
|
|
153
|
+
return self.issue_service.get_issue(issue_id)
|
|
154
|
+
|
|
155
|
+
def _build_update_params(
|
|
156
|
+
self, issue_id: str, updates: dict
|
|
157
|
+
) -> IssueUpdateServiceParams:
|
|
158
|
+
"""Build IssueUpdateServiceParams from a raw updates dict."""
|
|
159
|
+
params_dict: dict = {"issue_id": issue_id}
|
|
160
|
+
|
|
161
|
+
if "title" in updates:
|
|
162
|
+
params_dict["title"] = updates["title"]
|
|
163
|
+
if "priority" in updates:
|
|
164
|
+
priority = updates["priority"]
|
|
165
|
+
params_dict["priority"] = (
|
|
166
|
+
priority.value if isinstance(priority, Priority) else priority
|
|
167
|
+
)
|
|
168
|
+
if "status" in updates:
|
|
169
|
+
status = updates["status"]
|
|
170
|
+
params_dict["status"] = (
|
|
171
|
+
status.value if isinstance(status, Status) else status
|
|
172
|
+
)
|
|
173
|
+
if "assignee" in updates:
|
|
174
|
+
params_dict["assignee"] = updates["assignee"]
|
|
175
|
+
if "milestone" in updates:
|
|
176
|
+
params_dict["milestone"] = updates["milestone"] # Can be None!
|
|
177
|
+
if "description" in updates:
|
|
178
|
+
params_dict["content"] = updates["description"]
|
|
179
|
+
if "estimate" in updates:
|
|
180
|
+
params_dict["estimate"] = updates["estimate"]
|
|
181
|
+
if "reason" in updates:
|
|
182
|
+
params_dict["reason"] = updates["reason"]
|
|
183
|
+
|
|
184
|
+
return IssueUpdateServiceParams(**params_dict)
|
|
185
|
+
|
|
186
|
+
_HANDLED_UPDATE_FIELDS = frozenset(
|
|
187
|
+
{
|
|
188
|
+
"issue_id",
|
|
189
|
+
"title",
|
|
190
|
+
"priority",
|
|
191
|
+
"status",
|
|
192
|
+
"assignee",
|
|
193
|
+
"milestone",
|
|
194
|
+
"content",
|
|
195
|
+
"estimate",
|
|
196
|
+
"reason",
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _apply_extra_fields_to_result(self, result: Issue, updates: dict) -> None:
|
|
201
|
+
"""Apply update fields not handled by the service layer, then save to file."""
|
|
202
|
+
for field, value in updates.items():
|
|
203
|
+
if field not in self._HANDLED_UPDATE_FIELDS and hasattr(result, field):
|
|
204
|
+
setattr(result, field, value)
|
|
205
|
+
|
|
206
|
+
# Save extra fields to disk when the service layer didn't already do so.
|
|
207
|
+
# Milestone updates are written by the repository's own save(); skip to avoid
|
|
208
|
+
# overwriting a potentially relocated file path.
|
|
209
|
+
if result.file_path and "milestone" not in updates:
|
|
210
|
+
IssueParser.save_issue_file(result, Path(result.file_path))
|
|
211
|
+
|
|
212
|
+
def update_issue(self, issue_id: str, **updates) -> Issue | None:
|
|
213
|
+
"""Update an existing issue.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
issue_id: The issue ID to update
|
|
217
|
+
**updates: Fields to update
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Updated Issue object if found, None otherwise
|
|
221
|
+
"""
|
|
222
|
+
params = self._build_update_params(issue_id, updates)
|
|
223
|
+
result = self.issue_service.update_issue(params)
|
|
224
|
+
|
|
225
|
+
if result is not None:
|
|
226
|
+
self._apply_extra_fields_to_result(result, updates)
|
|
227
|
+
self._milestone_cache.clear()
|
|
228
|
+
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
def delete_issue(self, issue_id: str) -> bool:
|
|
232
|
+
"""Delete an issue.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
issue_id: The issue ID to delete
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
True if issue was deleted, False if not found
|
|
239
|
+
"""
|
|
240
|
+
result = self.issue_service.delete_issue(issue_id)
|
|
241
|
+
|
|
242
|
+
# Invalidate milestone cache after deletion
|
|
243
|
+
if result:
|
|
244
|
+
self._milestone_cache.clear()
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def assign_issue_to_milestone(self, issue_id: str, milestone_name: str) -> bool:
|
|
249
|
+
"""Assign an issue to a milestone.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
issue_id: The issue ID to assign
|
|
253
|
+
milestone_name: The milestone name to assign to
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if assignment successful, False if issue or milestone not found
|
|
257
|
+
"""
|
|
258
|
+
issue = self.get_issue(issue_id)
|
|
259
|
+
if not issue:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
# Validate milestone exists by checking file using same logic as Milestone.filename
|
|
263
|
+
safe_name = "".join(
|
|
264
|
+
c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
|
|
265
|
+
).strip()
|
|
266
|
+
safe_name = safe_name.replace(" ", "-").lower()
|
|
267
|
+
milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
|
|
268
|
+
if not milestone_file.exists():
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
# Use the repository's update method which handles file movement correctly
|
|
272
|
+
return self.update_issue(issue_id, milestone=milestone_name) is not None
|
|
273
|
+
|
|
274
|
+
def get_backlog_issues(self) -> list[Issue]:
|
|
275
|
+
"""Get all issues not assigned to any milestone (backlog).
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of Issue objects in the backlog
|
|
279
|
+
"""
|
|
280
|
+
all_issues = self.list_issues()
|
|
281
|
+
return [issue for issue in all_issues if issue.is_backlog]
|
|
282
|
+
|
|
283
|
+
def get_milestone_issues(self, milestone_name: str) -> list[Issue]:
|
|
284
|
+
"""Get all issues assigned to a specific milestone.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
milestone_name: The milestone name to filter by
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of Issue objects assigned to the milestone
|
|
291
|
+
"""
|
|
292
|
+
all_issues = self.list_issues()
|
|
293
|
+
return [issue for issue in all_issues if issue.milestone == milestone_name]
|
|
294
|
+
|
|
295
|
+
def get_issues_by_milestone(self) -> dict[str, list[Issue]]:
|
|
296
|
+
"""Get all issues grouped by milestone, including backlog.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Dictionary mapping milestone names to lists of Issue objects,
|
|
300
|
+
with a "Backlog" key for unassigned issues
|
|
301
|
+
"""
|
|
302
|
+
# Check cache first
|
|
303
|
+
cached = self._milestone_cache.get("all_milestones")
|
|
304
|
+
if cached is not None:
|
|
305
|
+
return cached
|
|
306
|
+
|
|
307
|
+
all_issues = self.list_issues()
|
|
308
|
+
grouped: dict[str, list[Issue]] = {"Backlog": []}
|
|
309
|
+
|
|
310
|
+
# Add backlog issues
|
|
311
|
+
for issue in all_issues:
|
|
312
|
+
if issue.is_backlog:
|
|
313
|
+
grouped["Backlog"].append(issue)
|
|
314
|
+
else:
|
|
315
|
+
milestone_name = issue.milestone
|
|
316
|
+
if milestone_name is None:
|
|
317
|
+
# Issues without milestone go to Backlog
|
|
318
|
+
grouped["Backlog"].append(issue)
|
|
319
|
+
else:
|
|
320
|
+
if milestone_name not in grouped:
|
|
321
|
+
grouped[milestone_name] = []
|
|
322
|
+
grouped[milestone_name].append(issue)
|
|
323
|
+
|
|
324
|
+
# Cache the result with TTL
|
|
325
|
+
self._milestone_cache.set("all_milestones", grouped, ttl=self._cache_ttl)
|
|
326
|
+
|
|
327
|
+
return grouped
|
|
328
|
+
|
|
329
|
+
def move_issue_to_milestone(
|
|
330
|
+
self, issue_id: str, milestone_name: str | None
|
|
331
|
+
) -> bool:
|
|
332
|
+
"""Move an issue to a milestone or to backlog if milestone_name is None.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
issue_id: The issue ID to move
|
|
336
|
+
milestone_name: The target milestone name, or None for backlog
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
True if move successful, False if issue not found or milestone invalid
|
|
340
|
+
"""
|
|
341
|
+
issue = self.get_issue(issue_id)
|
|
342
|
+
if not issue:
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
# Validate milestone exists if milestone_name is provided (None is valid for backlog)
|
|
346
|
+
if milestone_name is not None:
|
|
347
|
+
# Check if milestone file exists
|
|
348
|
+
safe_name = "".join(
|
|
349
|
+
c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
|
|
350
|
+
).strip()
|
|
351
|
+
safe_name = safe_name.replace(" ", "-").lower()
|
|
352
|
+
milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
|
|
353
|
+
if not milestone_file.exists():
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
# Update issue milestone using the repository method which handles file movement
|
|
357
|
+
updated_issue = self.update_issue(issue_id, milestone=milestone_name)
|
|
358
|
+
|
|
359
|
+
# Invalidate milestone cache after moving
|
|
360
|
+
self._milestone_cache.clear()
|
|
361
|
+
|
|
362
|
+
return updated_issue is not None
|
|
363
|
+
|
|
364
|
+
def batch_assign_to_milestone(
|
|
365
|
+
self,
|
|
366
|
+
issue_ids: list[str],
|
|
367
|
+
milestone_name: str,
|
|
368
|
+
preloaded_issues: list[Issue] | None = None,
|
|
369
|
+
) -> tuple[int, int]:
|
|
370
|
+
"""Batch assign multiple issues to a milestone in a single pass.
|
|
371
|
+
|
|
372
|
+
This method is more efficient than calling assign_issue_to_milestone
|
|
373
|
+
in a loop, as it validates the milestone once. If preloaded_issues
|
|
374
|
+
is provided, it uses those instead of scanning the filesystem.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
issue_ids: List of issue IDs to assign
|
|
378
|
+
milestone_name: The milestone name to assign to
|
|
379
|
+
preloaded_issues: Optional pre-loaded list of issues (avoids filesystem scan)
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Tuple of (successful_count, failed_count)
|
|
383
|
+
"""
|
|
384
|
+
# Validate milestone exists once upfront
|
|
385
|
+
safe_name = "".join(
|
|
386
|
+
c for c in milestone_name if c.isalnum() or c in (" ", "-", "_")
|
|
387
|
+
).strip()
|
|
388
|
+
safe_name = safe_name.replace(" ", "-").lower()
|
|
389
|
+
milestone_file = self.issues_dir.parent / "milestones" / f"{safe_name}.md"
|
|
390
|
+
if not milestone_file.exists():
|
|
391
|
+
return 0, len(issue_ids)
|
|
392
|
+
|
|
393
|
+
# Load all issues once (or use preloaded)
|
|
394
|
+
if preloaded_issues is not None:
|
|
395
|
+
all_issues = preloaded_issues
|
|
396
|
+
else:
|
|
397
|
+
all_issues = self.list_issues()
|
|
398
|
+
|
|
399
|
+
issue_by_id = {issue.id: issue for issue in all_issues}
|
|
400
|
+
|
|
401
|
+
successful = 0
|
|
402
|
+
failed = 0
|
|
403
|
+
|
|
404
|
+
# Update and save only the issues we need to modify
|
|
405
|
+
for issue_id in issue_ids:
|
|
406
|
+
if issue_id not in issue_by_id:
|
|
407
|
+
failed += 1
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
issue = issue_by_id[issue_id]
|
|
411
|
+
issue.milestone = milestone_name
|
|
412
|
+
issue.updated = now_utc()
|
|
413
|
+
|
|
414
|
+
# Save the issue
|
|
415
|
+
if hasattr(issue, "file_path") and issue.file_path:
|
|
416
|
+
issue_path = Path(issue.file_path)
|
|
417
|
+
else:
|
|
418
|
+
issue_path = self.issues_dir / issue.filename
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
IssueParser.save_issue_file(issue, issue_path)
|
|
422
|
+
successful += 1
|
|
423
|
+
except Exception:
|
|
424
|
+
failed += 1
|
|
425
|
+
|
|
426
|
+
# Invalidate caches after batch assignment
|
|
427
|
+
if successful > 0:
|
|
428
|
+
self._milestone_cache.clear()
|
|
429
|
+
|
|
430
|
+
return successful, failed
|
|
431
|
+
|
|
432
|
+
def get_similar_milestone_names(
|
|
433
|
+
self, milestone_name: str, max_results: int = 3
|
|
434
|
+
) -> list[str]:
|
|
435
|
+
"""Find milestone names similar to the given name.
|
|
436
|
+
|
|
437
|
+
Useful for suggesting corrections when a user typos a milestone name.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
milestone_name: The milestone name to find similar matches for
|
|
441
|
+
max_results: Maximum number of suggestions to return
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
List of similar milestone names found
|
|
445
|
+
"""
|
|
446
|
+
# Get all available milestone names
|
|
447
|
+
milestones_dir = self.issues_dir.parent / "milestones"
|
|
448
|
+
if not milestones_dir.exists():
|
|
449
|
+
return []
|
|
450
|
+
|
|
451
|
+
available_milestones = [f.stem for f in milestones_dir.glob("*.md")]
|
|
452
|
+
|
|
453
|
+
# Find close matches
|
|
454
|
+
return get_close_matches(
|
|
455
|
+
milestone_name, available_milestones, n=max_results, cutoff=0.6
|
|
456
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Milestone Coordinator - Coordinates milestone-related operations.
|
|
2
|
+
|
|
3
|
+
Extracted from RoadmapCore to reduce god object complexity.
|
|
4
|
+
Provides a focused API for all milestone-related concerns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from roadmap.core.domain import Milestone
|
|
13
|
+
from roadmap.infrastructure.coordination.milestone_operations import MilestoneOperations
|
|
14
|
+
from roadmap.infrastructure.validation.milestone_consistency_validator import (
|
|
15
|
+
MilestoneConsistencyValidator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MilestoneCoordinator:
|
|
23
|
+
"""Coordinates all milestone-related operations."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
milestone_ops: MilestoneOperations,
|
|
28
|
+
milestones_dir,
|
|
29
|
+
core: RoadmapCore | None = None,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize coordinator with milestone operations manager.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
milestone_ops: MilestoneOperations instance
|
|
35
|
+
milestones_dir: Path to milestones directory
|
|
36
|
+
core: RoadmapCore instance for initialization checks
|
|
37
|
+
"""
|
|
38
|
+
self._ops = milestone_ops
|
|
39
|
+
self._consistency_validator = MilestoneConsistencyValidator(milestones_dir)
|
|
40
|
+
self._core = core
|
|
41
|
+
|
|
42
|
+
# CRUD Operations
|
|
43
|
+
def create(
|
|
44
|
+
self,
|
|
45
|
+
name: str,
|
|
46
|
+
headline: str = "",
|
|
47
|
+
due_date: datetime | None = None,
|
|
48
|
+
status: str | None = None,
|
|
49
|
+
project_id: str | None = None,
|
|
50
|
+
) -> Milestone:
|
|
51
|
+
"""Create a new milestone."""
|
|
52
|
+
milestone = self._ops.create_milestone(
|
|
53
|
+
name=name, headline=headline, due_date=due_date, status=status
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Assign to project if provided
|
|
57
|
+
if project_id:
|
|
58
|
+
milestone.project_id = project_id
|
|
59
|
+
|
|
60
|
+
return milestone
|
|
61
|
+
|
|
62
|
+
def list(self) -> list[Milestone]:
|
|
63
|
+
"""List all milestones."""
|
|
64
|
+
return self._ops.list_milestones()
|
|
65
|
+
|
|
66
|
+
def get(self, name: str) -> Milestone | None:
|
|
67
|
+
"""Get a specific milestone by name (searches by YAML name field, not filename)."""
|
|
68
|
+
return self._ops.get_milestone(name)
|
|
69
|
+
|
|
70
|
+
def update(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
headline: str | None = None,
|
|
74
|
+
due_date: datetime | None = None,
|
|
75
|
+
clear_due_date: bool = False,
|
|
76
|
+
status: str | None = None,
|
|
77
|
+
project_id: str | None = None,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Update a milestone's properties.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: Name of the milestone to update
|
|
83
|
+
headline: New headline (None to keep current)
|
|
84
|
+
due_date: New due date (None to keep current)
|
|
85
|
+
clear_due_date: If True, remove the due date
|
|
86
|
+
status: New status (None to keep current)
|
|
87
|
+
project_id: Project ID to assign milestone to
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if milestone was updated, False if not found
|
|
91
|
+
"""
|
|
92
|
+
return self._ops.update_milestone(
|
|
93
|
+
name=name,
|
|
94
|
+
headline=headline,
|
|
95
|
+
due_date=due_date,
|
|
96
|
+
clear_due_date=clear_due_date,
|
|
97
|
+
status=status,
|
|
98
|
+
project_id=project_id,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def delete(self, name: str) -> bool:
|
|
102
|
+
"""Delete a milestone and unassign all issues from it.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
name: Name of the milestone to delete
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if milestone was deleted, False if not found
|
|
109
|
+
"""
|
|
110
|
+
return self._ops.delete_milestone(name)
|
|
111
|
+
|
|
112
|
+
def get_progress(self, milestone_name: str) -> dict[str, Any]:
|
|
113
|
+
"""Get progress statistics for a milestone."""
|
|
114
|
+
return self._ops.get_milestone_progress(milestone_name)
|
|
115
|
+
|
|
116
|
+
def get_next(self) -> Milestone | None:
|
|
117
|
+
"""Get the next upcoming milestone based on due date."""
|
|
118
|
+
return self._ops.get_next_milestone()
|
|
119
|
+
|
|
120
|
+
# Consistency and Validation
|
|
121
|
+
def validate_naming_consistency(self) -> list[dict[str, str]]:
|
|
122
|
+
"""Check for inconsistencies between milestone filenames and name fields.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of dictionaries with inconsistency details
|
|
126
|
+
"""
|
|
127
|
+
return self._consistency_validator.validate()
|
|
128
|
+
|
|
129
|
+
def fix_naming_consistency(self) -> dict[str, list[str]]:
|
|
130
|
+
"""Fix milestone filename inconsistencies by renaming files to match name fields.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Dictionary with 'renamed' and 'errors' lists
|
|
134
|
+
"""
|
|
135
|
+
return self._consistency_validator.fix()
|