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,765 @@
|
|
|
1
|
+
"""Issue service - handles all issue-related operations.
|
|
2
|
+
|
|
3
|
+
The IssueService manages:
|
|
4
|
+
- Issue creation with metadata
|
|
5
|
+
- Issue listing with filtering (milestone, status, assignee)
|
|
6
|
+
- Getting specific issues by ID
|
|
7
|
+
- Updating issue fields
|
|
8
|
+
- Deleting issues
|
|
9
|
+
- Status transitions
|
|
10
|
+
|
|
11
|
+
Extracted from core.py to separate business logic.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from roadmap.common.cache import SessionCache
|
|
15
|
+
from roadmap.common.constants import IssueType, Priority, Status
|
|
16
|
+
from roadmap.common.errors import OperationType, safe_operation
|
|
17
|
+
from roadmap.common.logging import get_logger
|
|
18
|
+
from roadmap.common.logging.error_logging import (
|
|
19
|
+
log_database_error,
|
|
20
|
+
log_error_with_context,
|
|
21
|
+
)
|
|
22
|
+
from roadmap.common.observability.instrumentation import traced
|
|
23
|
+
from roadmap.common.result import Err, Ok
|
|
24
|
+
from roadmap.common.services import log_entry, log_event, log_exit, log_metric
|
|
25
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
26
|
+
from roadmap.core.domain.issue import Issue
|
|
27
|
+
from roadmap.core.models import (
|
|
28
|
+
NOT_PROVIDED,
|
|
29
|
+
IssueCreateServiceParams,
|
|
30
|
+
IssueUpdateServiceParams,
|
|
31
|
+
)
|
|
32
|
+
from roadmap.core.repositories import IssueRepository
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IssueService:
|
|
38
|
+
"""Service for managing issues."""
|
|
39
|
+
|
|
40
|
+
# Cache TTL in seconds
|
|
41
|
+
_cache_ttl = 60
|
|
42
|
+
|
|
43
|
+
def __init__(self, repository: IssueRepository):
|
|
44
|
+
"""Initialize issue service.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
repository: IssueRepository implementation for data persistence
|
|
48
|
+
"""
|
|
49
|
+
self.repository = repository
|
|
50
|
+
# Cache for list_issues results (instance-level to avoid test pollution)
|
|
51
|
+
self._list_issues_cache = SessionCache()
|
|
52
|
+
|
|
53
|
+
@traced("create_issue")
|
|
54
|
+
@safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
|
|
55
|
+
def create_issue(self, params: IssueCreateServiceParams) -> Issue:
|
|
56
|
+
"""Create a new issue with provided metadata.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
params: IssueCreateServiceParams containing all creation parameters
|
|
60
|
+
- title: Issue title/summary
|
|
61
|
+
- priority: Priority level (CRITICAL, HIGH, MEDIUM, LOW)
|
|
62
|
+
- issue_type: Type of issue (BUG, FEATURE, TASK, etc.)
|
|
63
|
+
- milestone: Associated milestone name
|
|
64
|
+
- labels: List of labels/tags
|
|
65
|
+
- assignee: Assigned user or team member
|
|
66
|
+
- estimate: Estimated effort in hours
|
|
67
|
+
- depends_on: List of issue IDs this depends on
|
|
68
|
+
- blocks: List of issue IDs this blocks
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Newly created Issue object
|
|
72
|
+
"""
|
|
73
|
+
log_entry("create_issue", title=params.title, priority=params.priority)
|
|
74
|
+
logger.info(
|
|
75
|
+
"creating_issue",
|
|
76
|
+
title=params.title,
|
|
77
|
+
priority=params.priority,
|
|
78
|
+
issue_type=params.issue_type,
|
|
79
|
+
)
|
|
80
|
+
log_event("issue_creation_started", issue_title=params.title)
|
|
81
|
+
|
|
82
|
+
# Convert string values to enums (if they're not already)
|
|
83
|
+
try:
|
|
84
|
+
priority = (
|
|
85
|
+
Priority[params.priority.upper()]
|
|
86
|
+
if isinstance(params.priority, str)
|
|
87
|
+
else params.priority or Priority.MEDIUM
|
|
88
|
+
)
|
|
89
|
+
except (AttributeError, KeyError):
|
|
90
|
+
priority = Priority.MEDIUM
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
issue_type = (
|
|
94
|
+
IssueType[params.issue_type.upper()]
|
|
95
|
+
if isinstance(params.issue_type, str)
|
|
96
|
+
else params.issue_type or IssueType.OTHER
|
|
97
|
+
)
|
|
98
|
+
except (AttributeError, KeyError):
|
|
99
|
+
issue_type = IssueType.OTHER
|
|
100
|
+
|
|
101
|
+
issue = Issue(
|
|
102
|
+
title=params.title,
|
|
103
|
+
priority=priority,
|
|
104
|
+
issue_type=issue_type,
|
|
105
|
+
milestone=params.milestone,
|
|
106
|
+
labels=params.labels or [],
|
|
107
|
+
assignee=params.assignee,
|
|
108
|
+
estimated_hours=params.estimate,
|
|
109
|
+
depends_on=params.depends_on or [],
|
|
110
|
+
blocks=params.blocks or [],
|
|
111
|
+
content=params.content
|
|
112
|
+
or f"# {params.title}\n\n## Description\n\nBrief description of the issue or feature request.\n\n## Acceptance Criteria\n\n- [ ] Criterion 1\n- [ ] Criterion 2\n- [ ] Criterion 3",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Persist using repository abstraction
|
|
116
|
+
try:
|
|
117
|
+
self.repository.save(issue)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
log_database_error(
|
|
120
|
+
e,
|
|
121
|
+
operation="create",
|
|
122
|
+
entity_type="Issue",
|
|
123
|
+
entity_id=issue.id,
|
|
124
|
+
)
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
# Invalidate cache after successful creation
|
|
128
|
+
self._list_issues_cache.clear()
|
|
129
|
+
|
|
130
|
+
log_event("issue_created", issue_id=issue.id)
|
|
131
|
+
log_exit("create_issue", issue_id=issue.id)
|
|
132
|
+
return issue
|
|
133
|
+
|
|
134
|
+
def _check_milestone_filter(self, issue: Issue, milestone: str | None) -> bool:
|
|
135
|
+
"""Check milestone filter.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
issue: Issue to check
|
|
139
|
+
milestone: Required milestone or None
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if milestone matches or no filter
|
|
143
|
+
"""
|
|
144
|
+
return not milestone or issue.milestone == milestone
|
|
145
|
+
|
|
146
|
+
def _check_status_filter(self, issue: Issue, status: Status | None) -> bool:
|
|
147
|
+
"""Check status filter.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
issue: Issue to check
|
|
151
|
+
status: Required status or None
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if status matches or no filter
|
|
155
|
+
"""
|
|
156
|
+
return not status or issue.status == status
|
|
157
|
+
|
|
158
|
+
def _check_priority_filter(self, issue: Issue, priority: Priority | None) -> bool:
|
|
159
|
+
"""Check priority filter.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
issue: Issue to check
|
|
163
|
+
priority: Required priority or None
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if priority matches or no filter
|
|
167
|
+
"""
|
|
168
|
+
return not priority or issue.priority == priority
|
|
169
|
+
|
|
170
|
+
def _check_type_filter(self, issue: Issue, issue_type: IssueType | None) -> bool:
|
|
171
|
+
"""Check issue type filter.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
issue: Issue to check
|
|
175
|
+
issue_type: Required issue type or None
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if type matches or no filter
|
|
179
|
+
"""
|
|
180
|
+
return not issue_type or issue.issue_type == issue_type
|
|
181
|
+
|
|
182
|
+
def _check_assignee_filter(self, issue: Issue, assignee: str | None) -> bool:
|
|
183
|
+
"""Check assignee filter.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
issue: Issue to check
|
|
187
|
+
assignee: Required assignee or None
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if assignee matches or no filter
|
|
191
|
+
"""
|
|
192
|
+
return not assignee or issue.assignee == assignee
|
|
193
|
+
|
|
194
|
+
def _matches_all_filters(
|
|
195
|
+
self,
|
|
196
|
+
issue: Issue,
|
|
197
|
+
milestone: str | None,
|
|
198
|
+
status: Status | None,
|
|
199
|
+
priority: Priority | None,
|
|
200
|
+
issue_type: IssueType | None,
|
|
201
|
+
assignee: str | None,
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""Check if issue matches all provided filter criteria."""
|
|
204
|
+
return (
|
|
205
|
+
self._check_milestone_filter(issue, milestone)
|
|
206
|
+
and self._check_status_filter(issue, status)
|
|
207
|
+
and self._check_priority_filter(issue, priority)
|
|
208
|
+
and self._check_type_filter(issue, issue_type)
|
|
209
|
+
and self._check_assignee_filter(issue, assignee)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _get_priority_order(self) -> dict:
|
|
213
|
+
"""Get priority ordering for sorting."""
|
|
214
|
+
return {
|
|
215
|
+
Priority.CRITICAL: 0,
|
|
216
|
+
Priority.HIGH: 1,
|
|
217
|
+
Priority.MEDIUM: 2,
|
|
218
|
+
Priority.LOW: 3,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def _sort_issues_by_priority_and_date(self, issues: list[Issue]) -> list[Issue]:
|
|
222
|
+
"""Sort issues by priority then creation date."""
|
|
223
|
+
priority_order = self._get_priority_order()
|
|
224
|
+
issues.sort(key=lambda x: (priority_order.get(x.priority, 999), x.created))
|
|
225
|
+
return issues
|
|
226
|
+
|
|
227
|
+
@traced("list_issues")
|
|
228
|
+
def list_issues(
|
|
229
|
+
self,
|
|
230
|
+
milestone: str | None = None,
|
|
231
|
+
status: Status | None = None,
|
|
232
|
+
priority: Priority | None = None,
|
|
233
|
+
issue_type: IssueType | None = None,
|
|
234
|
+
assignee: str | None = None,
|
|
235
|
+
) -> list[Issue]:
|
|
236
|
+
"""List issues with optional filtering.
|
|
237
|
+
|
|
238
|
+
All filters are combined with AND logic (all must match).
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
milestone: Filter by milestone name
|
|
242
|
+
status: Filter by status (OPEN, IN_PROGRESS, BLOCKED, REVIEW, DONE)
|
|
243
|
+
priority: Filter by priority level
|
|
244
|
+
issue_type: Filter by issue type
|
|
245
|
+
assignee: Filter by assignee
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of Issue objects matching all filters, sorted by priority then date
|
|
249
|
+
"""
|
|
250
|
+
log_entry("list_issues", milestone=milestone, status=status)
|
|
251
|
+
|
|
252
|
+
# Create cache key from filter parameters
|
|
253
|
+
cache_key = (milestone, status, priority, issue_type, assignee)
|
|
254
|
+
# Convert to string for cache key
|
|
255
|
+
cache_key_str = str(cache_key)
|
|
256
|
+
|
|
257
|
+
# Check cache first
|
|
258
|
+
cached = self._list_issues_cache.get(cache_key_str)
|
|
259
|
+
if cached is not None:
|
|
260
|
+
log_metric("cache_hit", 1, operation="list_issues")
|
|
261
|
+
log_exit("list_issues", issue_count=len(cached), from_cache=True)
|
|
262
|
+
return cached
|
|
263
|
+
|
|
264
|
+
# Get issues from repository
|
|
265
|
+
try:
|
|
266
|
+
issues = self.repository.list()
|
|
267
|
+
except Exception as e:
|
|
268
|
+
log_database_error(
|
|
269
|
+
e,
|
|
270
|
+
operation="list",
|
|
271
|
+
entity_type="Issue",
|
|
272
|
+
)
|
|
273
|
+
logger.warning(
|
|
274
|
+
"returning_empty_issue_list_due_to_error",
|
|
275
|
+
severity="operational",
|
|
276
|
+
)
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
log_metric("issues_enumerated", len(issues))
|
|
280
|
+
|
|
281
|
+
# Apply filters
|
|
282
|
+
try:
|
|
283
|
+
filtered_issues = [
|
|
284
|
+
issue
|
|
285
|
+
for issue in issues
|
|
286
|
+
if self._matches_all_filters(
|
|
287
|
+
issue, milestone, status, priority, issue_type, assignee
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
except Exception as e:
|
|
291
|
+
log_error_with_context(
|
|
292
|
+
e,
|
|
293
|
+
operation="filter_issues",
|
|
294
|
+
entity_type="Issue",
|
|
295
|
+
additional_context={
|
|
296
|
+
"milestone": milestone,
|
|
297
|
+
"status": status,
|
|
298
|
+
"priority": priority,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
return issues # Return unfiltered list as fallback
|
|
302
|
+
|
|
303
|
+
# Sort by priority then by creation date
|
|
304
|
+
sorted_issues = self._sort_issues_by_priority_and_date(filtered_issues)
|
|
305
|
+
log_metric("issues_filtered", len(issues), filtered=len(sorted_issues))
|
|
306
|
+
|
|
307
|
+
# Cache the result with TTL
|
|
308
|
+
self._list_issues_cache.set(cache_key_str, sorted_issues, ttl=self._cache_ttl)
|
|
309
|
+
|
|
310
|
+
log_exit("list_issues", issue_count=len(sorted_issues))
|
|
311
|
+
return sorted_issues
|
|
312
|
+
|
|
313
|
+
@traced("list_all_including_archived")
|
|
314
|
+
def list_all_including_archived(self) -> list[Issue]:
|
|
315
|
+
"""List all issues including archived ones.
|
|
316
|
+
|
|
317
|
+
Used by sync operations that need to compare both active and archived issues.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of all Issue objects (both active and archived)
|
|
321
|
+
"""
|
|
322
|
+
log_entry("list_all_including_archived")
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
issues = self.repository.list_all_including_archived()
|
|
326
|
+
except Exception as e:
|
|
327
|
+
log_database_error(
|
|
328
|
+
e,
|
|
329
|
+
operation="list_all_including_archived",
|
|
330
|
+
entity_type="Issue",
|
|
331
|
+
)
|
|
332
|
+
logger.warning(
|
|
333
|
+
"returning_empty_issue_list_due_to_error",
|
|
334
|
+
severity="operational",
|
|
335
|
+
)
|
|
336
|
+
return []
|
|
337
|
+
|
|
338
|
+
log_metric("all_issues_enumerated", len(issues))
|
|
339
|
+
log_exit("list_all_including_archived", issue_count=len(issues))
|
|
340
|
+
return issues
|
|
341
|
+
|
|
342
|
+
@traced("get_issue")
|
|
343
|
+
def get_issue(self, issue_id: str) -> Issue | None:
|
|
344
|
+
"""Get a specific issue by ID.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
issue_id: Issue identifier (ID prefix used in filename)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Issue object if found, None otherwise.
|
|
351
|
+
|
|
352
|
+
Note:
|
|
353
|
+
If multiple copies exist (due to migration), prefers milestone-specific
|
|
354
|
+
subdirectories (v.X.X.X) over root directory.
|
|
355
|
+
"""
|
|
356
|
+
log_entry("get_issue", issue_id=issue_id)
|
|
357
|
+
# Get issue from repository
|
|
358
|
+
try:
|
|
359
|
+
issue = self.repository.get(issue_id)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
log_database_error(
|
|
362
|
+
e,
|
|
363
|
+
operation="read",
|
|
364
|
+
entity_type="Issue",
|
|
365
|
+
entity_id=issue_id,
|
|
366
|
+
)
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
if issue and hasattr(issue, "file_path"):
|
|
370
|
+
# Store the original file path so updates preserve the location
|
|
371
|
+
issue.file_path = str(issue.file_path)
|
|
372
|
+
log_event("issue_found", issue_id=issue_id, title=issue.title)
|
|
373
|
+
else:
|
|
374
|
+
log_event("issue_not_found", issue_id=issue_id)
|
|
375
|
+
|
|
376
|
+
log_exit("get_issue", found=issue is not None)
|
|
377
|
+
return issue
|
|
378
|
+
|
|
379
|
+
@safe_operation(OperationType.UPDATE, "Issue")
|
|
380
|
+
@traced("update_issue")
|
|
381
|
+
def update_issue(self, params: IssueUpdateServiceParams) -> Issue | None:
|
|
382
|
+
"""Update an existing issue with new field values.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
params: IssueUpdateServiceParams with fields to update
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Updated Issue object if found, None otherwise
|
|
389
|
+
"""
|
|
390
|
+
log_entry(
|
|
391
|
+
"update_issue",
|
|
392
|
+
issue_id=params.issue_id,
|
|
393
|
+
fields=[
|
|
394
|
+
"title",
|
|
395
|
+
"status",
|
|
396
|
+
"priority",
|
|
397
|
+
"assignee",
|
|
398
|
+
"milestone",
|
|
399
|
+
"content",
|
|
400
|
+
"estimate",
|
|
401
|
+
"reason",
|
|
402
|
+
],
|
|
403
|
+
)
|
|
404
|
+
logger.info(
|
|
405
|
+
"updating_issue",
|
|
406
|
+
issue_id=params.issue_id,
|
|
407
|
+
update_fields=[
|
|
408
|
+
"title",
|
|
409
|
+
"status",
|
|
410
|
+
"priority",
|
|
411
|
+
"assignee",
|
|
412
|
+
"milestone",
|
|
413
|
+
"content",
|
|
414
|
+
"estimate",
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
issue = self.get_issue(params.issue_id)
|
|
419
|
+
if not issue:
|
|
420
|
+
log_event("issue_not_found", issue_id=params.issue_id)
|
|
421
|
+
log_exit("update_issue", success=False)
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
# Update fields if provided (not NOT_PROVIDED)
|
|
425
|
+
if params.title is not NOT_PROVIDED:
|
|
426
|
+
issue.title = params.title
|
|
427
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="title")
|
|
428
|
+
if params.status is not NOT_PROVIDED:
|
|
429
|
+
issue.status = Status(params.status)
|
|
430
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="status")
|
|
431
|
+
if params.priority is not NOT_PROVIDED:
|
|
432
|
+
try:
|
|
433
|
+
priority = (
|
|
434
|
+
Priority[params.priority.upper()]
|
|
435
|
+
if isinstance(params.priority, str)
|
|
436
|
+
else params.priority
|
|
437
|
+
)
|
|
438
|
+
except (AttributeError, KeyError):
|
|
439
|
+
# Invalid priority - keep current priority
|
|
440
|
+
logger.warning(
|
|
441
|
+
"invalid_priority_in_update",
|
|
442
|
+
issue_id=params.issue_id,
|
|
443
|
+
priority=params.priority,
|
|
444
|
+
severity="data_error",
|
|
445
|
+
)
|
|
446
|
+
priority = issue.priority
|
|
447
|
+
issue.priority = priority
|
|
448
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="priority")
|
|
449
|
+
if params.assignee is not NOT_PROVIDED:
|
|
450
|
+
issue.assignee = params.assignee
|
|
451
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="assignee")
|
|
452
|
+
if params.milestone is not NOT_PROVIDED:
|
|
453
|
+
issue.milestone = params.milestone
|
|
454
|
+
log_event(
|
|
455
|
+
"issue_field_updated", issue_id=params.issue_id, field="milestone"
|
|
456
|
+
)
|
|
457
|
+
if params.content is not NOT_PROVIDED:
|
|
458
|
+
issue.content = params.content
|
|
459
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="content")
|
|
460
|
+
if params.estimate is not NOT_PROVIDED:
|
|
461
|
+
issue.estimated_hours = params.estimate
|
|
462
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="estimate")
|
|
463
|
+
if params.archived is not NOT_PROVIDED:
|
|
464
|
+
issue.archived = params.archived
|
|
465
|
+
log_event("issue_field_updated", issue_id=params.issue_id, field="archived")
|
|
466
|
+
|
|
467
|
+
# Update timestamp
|
|
468
|
+
issue.updated = now_utc()
|
|
469
|
+
|
|
470
|
+
# Save updated issue through repository
|
|
471
|
+
try:
|
|
472
|
+
self.repository.save(issue)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
log_database_error(
|
|
475
|
+
e,
|
|
476
|
+
operation="update",
|
|
477
|
+
entity_type="Issue",
|
|
478
|
+
entity_id=params.issue_id,
|
|
479
|
+
)
|
|
480
|
+
raise
|
|
481
|
+
|
|
482
|
+
log_event("issue_saved", issue_id=params.issue_id)
|
|
483
|
+
|
|
484
|
+
# Invalidate cache after successful update
|
|
485
|
+
self._list_issues_cache.clear()
|
|
486
|
+
|
|
487
|
+
log_exit("update_issue", issue_id=params.issue_id, success=True)
|
|
488
|
+
|
|
489
|
+
return issue
|
|
490
|
+
|
|
491
|
+
@safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
|
|
492
|
+
@traced("delete_issue")
|
|
493
|
+
def delete_issue(self, issue_id: str) -> bool:
|
|
494
|
+
"""Delete an issue.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
issue_id: Issue identifier
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
True if deleted successfully, False if not found
|
|
501
|
+
"""
|
|
502
|
+
log_entry("delete_issue", issue_id=issue_id)
|
|
503
|
+
logger.info("deleting_issue", issue_id=issue_id)
|
|
504
|
+
# Delete issue through repository
|
|
505
|
+
try:
|
|
506
|
+
deleted = self.repository.delete(issue_id)
|
|
507
|
+
except Exception as e:
|
|
508
|
+
log_database_error(
|
|
509
|
+
e,
|
|
510
|
+
operation="delete",
|
|
511
|
+
entity_type="Issue",
|
|
512
|
+
entity_id=issue_id,
|
|
513
|
+
)
|
|
514
|
+
raise
|
|
515
|
+
|
|
516
|
+
if deleted:
|
|
517
|
+
log_event("issue_deleted", issue_id=issue_id)
|
|
518
|
+
# Invalidate cache after successful deletion
|
|
519
|
+
self._list_issues_cache.clear()
|
|
520
|
+
log_exit("delete_issue", success=True)
|
|
521
|
+
else:
|
|
522
|
+
log_event("issue_not_found_delete", issue_id=issue_id)
|
|
523
|
+
log_exit("delete_issue", success=False)
|
|
524
|
+
return deleted
|
|
525
|
+
|
|
526
|
+
@safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
|
|
527
|
+
@traced("delete_many_issues")
|
|
528
|
+
def delete_many_issues(self, issue_ids: list[str]) -> int:
|
|
529
|
+
"""Delete multiple issues in a single batch operation.
|
|
530
|
+
|
|
531
|
+
Batch deletion is much more efficient than individual deletes
|
|
532
|
+
as it performs all deletes in one transaction and clears the
|
|
533
|
+
cache only once, avoiding repeated enumeration overhead.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
issue_ids: List of issue identifiers to delete
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Number of issues successfully deleted
|
|
540
|
+
"""
|
|
541
|
+
if not issue_ids:
|
|
542
|
+
return 0
|
|
543
|
+
|
|
544
|
+
log_entry("delete_many_issues", count=len(issue_ids))
|
|
545
|
+
logger.info("deleting_many_issues", count=len(issue_ids))
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
deleted_count = self.repository.delete_many(issue_ids)
|
|
549
|
+
except Exception as e:
|
|
550
|
+
log_database_error(
|
|
551
|
+
e,
|
|
552
|
+
operation="delete_many",
|
|
553
|
+
entity_type="Issue",
|
|
554
|
+
)
|
|
555
|
+
raise
|
|
556
|
+
|
|
557
|
+
if deleted_count > 0:
|
|
558
|
+
log_event("issues_deleted", count=deleted_count, requested=len(issue_ids))
|
|
559
|
+
# Invalidate cache after successful bulk deletion
|
|
560
|
+
self._list_issues_cache.clear()
|
|
561
|
+
log_exit("delete_many_issues", success=True)
|
|
562
|
+
else:
|
|
563
|
+
log_event("no_issues_deleted", requested=len(issue_ids))
|
|
564
|
+
log_exit("delete_many_issues", success=False)
|
|
565
|
+
|
|
566
|
+
return deleted_count
|
|
567
|
+
|
|
568
|
+
def close_issue(self, issue_id: str) -> Issue | None:
|
|
569
|
+
"""Close/mark issue as complete.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
issue_id: Issue identifier
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Closed Issue object if found, None otherwise
|
|
576
|
+
"""
|
|
577
|
+
params = IssueUpdateServiceParams(issue_id=issue_id, status="closed")
|
|
578
|
+
return self.update_issue(params)
|
|
579
|
+
|
|
580
|
+
@traced("merge_issues")
|
|
581
|
+
def merge_issues(
|
|
582
|
+
self, canonical_id: str, duplicate_id: str
|
|
583
|
+
) -> Ok[Issue] | Err[str]:
|
|
584
|
+
"""Merge duplicate issue into canonical issue.
|
|
585
|
+
|
|
586
|
+
Transfers all data from duplicate to canonical:
|
|
587
|
+
- Comments and discussion threads
|
|
588
|
+
- Labels (union of both)
|
|
589
|
+
- Remote IDs (merge both backends)
|
|
590
|
+
- Updated timestamp (take latest)
|
|
591
|
+
- Keep canonical: created date, ID, content (local priority)
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
canonical_id: ID of issue to keep
|
|
595
|
+
duplicate_id: ID of issue to merge away
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Ok(merged_issue) on success, Err(message) on failure
|
|
599
|
+
"""
|
|
600
|
+
log_entry("merge_issues", canonical_id=canonical_id, duplicate_id=duplicate_id)
|
|
601
|
+
|
|
602
|
+
# Get both issues
|
|
603
|
+
canonical = self.get_issue(canonical_id)
|
|
604
|
+
if not canonical:
|
|
605
|
+
msg = f"Canonical issue {canonical_id} not found"
|
|
606
|
+
logger.warning("merge_issues_canonical_not_found", issue_id=canonical_id)
|
|
607
|
+
log_exit("merge_issues", success=False)
|
|
608
|
+
return Err(msg)
|
|
609
|
+
|
|
610
|
+
duplicate = self.get_issue(duplicate_id)
|
|
611
|
+
if not duplicate:
|
|
612
|
+
msg = f"Duplicate issue {duplicate_id} not found"
|
|
613
|
+
logger.warning("merge_issues_duplicate_not_found", issue_id=duplicate_id)
|
|
614
|
+
log_exit("merge_issues", success=False)
|
|
615
|
+
return Err(msg)
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
# Merge data: union labels, merge remote_ids, combine comments
|
|
619
|
+
merged = Issue(
|
|
620
|
+
id=canonical.id,
|
|
621
|
+
title=canonical.title, # Keep canonical title
|
|
622
|
+
headline=canonical.headline,
|
|
623
|
+
content=canonical.content, # Keep canonical content (local priority)
|
|
624
|
+
status=canonical.status, # Keep canonical status
|
|
625
|
+
priority=canonical.priority,
|
|
626
|
+
issue_type=canonical.issue_type,
|
|
627
|
+
milestone=canonical.milestone,
|
|
628
|
+
labels=list(set(canonical.labels + duplicate.labels)), # Union labels
|
|
629
|
+
assignee=canonical.assignee, # Keep canonical assignee
|
|
630
|
+
estimated_hours=canonical.estimated_hours,
|
|
631
|
+
due_date=canonical.due_date,
|
|
632
|
+
depends_on=list(set(canonical.depends_on + duplicate.depends_on)),
|
|
633
|
+
blocks=list(set(canonical.blocks + duplicate.blocks)),
|
|
634
|
+
actual_start_date=canonical.actual_start_date,
|
|
635
|
+
actual_end_date=canonical.actual_end_date,
|
|
636
|
+
progress_percentage=canonical.progress_percentage,
|
|
637
|
+
handoff_notes=canonical.handoff_notes,
|
|
638
|
+
previous_assignee=canonical.previous_assignee,
|
|
639
|
+
handoff_date=canonical.handoff_date,
|
|
640
|
+
git_branches=list(set(canonical.git_branches + duplicate.git_branches)),
|
|
641
|
+
git_commits=canonical.git_commits
|
|
642
|
+
+ duplicate.git_commits, # Append commits
|
|
643
|
+
completed_date=canonical.completed_date or duplicate.completed_date,
|
|
644
|
+
comments=canonical.comments + duplicate.comments, # Append comments
|
|
645
|
+
created=canonical.created, # Keep canonical's earliest created
|
|
646
|
+
updated=max(
|
|
647
|
+
canonical.updated, duplicate.updated
|
|
648
|
+
), # Take latest updated
|
|
649
|
+
remote_ids={
|
|
650
|
+
**canonical.remote_ids,
|
|
651
|
+
**duplicate.remote_ids, # Merge remote IDs
|
|
652
|
+
},
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Persist merged issue
|
|
656
|
+
self.repository.save(merged)
|
|
657
|
+
self._list_issues_cache.clear()
|
|
658
|
+
|
|
659
|
+
logger.info(
|
|
660
|
+
"issues_merged",
|
|
661
|
+
canonical_id=canonical_id,
|
|
662
|
+
duplicate_id=duplicate_id,
|
|
663
|
+
merged_title=merged.title,
|
|
664
|
+
)
|
|
665
|
+
log_event(
|
|
666
|
+
"issues_merged", canonical_id=canonical_id, duplicate_id=duplicate_id
|
|
667
|
+
)
|
|
668
|
+
log_exit("merge_issues", success=True)
|
|
669
|
+
return Ok(merged)
|
|
670
|
+
|
|
671
|
+
except Exception as e:
|
|
672
|
+
log_database_error(
|
|
673
|
+
e,
|
|
674
|
+
operation="merge",
|
|
675
|
+
entity_type="Issue",
|
|
676
|
+
entity_id=canonical_id,
|
|
677
|
+
)
|
|
678
|
+
logger.error(
|
|
679
|
+
"merge_issues_failed",
|
|
680
|
+
canonical_id=canonical_id,
|
|
681
|
+
duplicate_id=duplicate_id,
|
|
682
|
+
error=str(e),
|
|
683
|
+
)
|
|
684
|
+
log_exit("merge_issues", success=False)
|
|
685
|
+
return Err(f"Failed to merge issues: {str(e)}")
|
|
686
|
+
|
|
687
|
+
@traced("archive_issue")
|
|
688
|
+
def archive_issue(
|
|
689
|
+
self,
|
|
690
|
+
issue_id: str,
|
|
691
|
+
duplicate_of_id: str | None = None,
|
|
692
|
+
resolution_type: str = "duplicate",
|
|
693
|
+
) -> Ok[Issue] | Err[str]:
|
|
694
|
+
"""Archive an issue (soft delete) with metadata.
|
|
695
|
+
|
|
696
|
+
Sets status to ARCHIVED and adds metadata about why it was archived.
|
|
697
|
+
Uses repository.mark_archived() for proper database handling.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
issue_id: ID of issue to archive
|
|
701
|
+
duplicate_of_id: ID of the canonical issue this is a duplicate of
|
|
702
|
+
resolution_type: Type of resolution (default: "duplicate")
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
Ok(archived_issue) on success, Err(message) on failure
|
|
706
|
+
"""
|
|
707
|
+
log_entry(
|
|
708
|
+
"archive_issue",
|
|
709
|
+
issue_id=issue_id,
|
|
710
|
+
duplicate_of_id=duplicate_of_id,
|
|
711
|
+
resolution_type=resolution_type,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
issue = self.get_issue(issue_id)
|
|
715
|
+
if not issue:
|
|
716
|
+
msg = f"Issue {issue_id} not found"
|
|
717
|
+
logger.warning("archive_issue_not_found", issue_id=issue_id)
|
|
718
|
+
log_exit("archive_issue", success=False)
|
|
719
|
+
return Err(msg)
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
# Update domain model status
|
|
723
|
+
issue.status = Status.ARCHIVED
|
|
724
|
+
|
|
725
|
+
# Store resolution metadata
|
|
726
|
+
if not issue.github_sync_metadata:
|
|
727
|
+
issue.github_sync_metadata = {}
|
|
728
|
+
issue.github_sync_metadata["archived_at"] = now_utc().isoformat()
|
|
729
|
+
issue.github_sync_metadata["resolution_type"] = resolution_type
|
|
730
|
+
if duplicate_of_id:
|
|
731
|
+
issue.github_sync_metadata["duplicate_of_id"] = duplicate_of_id
|
|
732
|
+
|
|
733
|
+
issue.updated = now_utc()
|
|
734
|
+
|
|
735
|
+
# Persist updated metadata and status
|
|
736
|
+
self.repository.save(issue)
|
|
737
|
+
self._list_issues_cache.clear()
|
|
738
|
+
|
|
739
|
+
logger.info(
|
|
740
|
+
"issue_archived",
|
|
741
|
+
issue_id=issue_id,
|
|
742
|
+
duplicate_of_id=duplicate_of_id,
|
|
743
|
+
resolution_type=resolution_type,
|
|
744
|
+
)
|
|
745
|
+
log_event(
|
|
746
|
+
"issue_archived", issue_id=issue_id, duplicate_of_id=duplicate_of_id
|
|
747
|
+
)
|
|
748
|
+
log_exit("archive_issue", success=True)
|
|
749
|
+
return Ok(issue)
|
|
750
|
+
|
|
751
|
+
except Exception as e:
|
|
752
|
+
log_database_error(
|
|
753
|
+
e,
|
|
754
|
+
operation="archive",
|
|
755
|
+
entity_type="Issue",
|
|
756
|
+
entity_id=issue_id,
|
|
757
|
+
)
|
|
758
|
+
logger.error(
|
|
759
|
+
"archive_issue_failed",
|
|
760
|
+
issue_id=issue_id,
|
|
761
|
+
duplicate_of_id=duplicate_of_id,
|
|
762
|
+
error=str(e),
|
|
763
|
+
)
|
|
764
|
+
log_exit("archive_issue", success=False)
|
|
765
|
+
return Err(f"Failed to archive issue: {str(e)}")
|