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,112 @@
|
|
|
1
|
+
"""Service for creating local issues from remote data."""
|
|
2
|
+
|
|
3
|
+
from structlog import get_logger
|
|
4
|
+
|
|
5
|
+
from roadmap.core.domain.issue import Issue, Status
|
|
6
|
+
from roadmap.core.models.sync_models import SyncIssue
|
|
7
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteIssueCreationService:
|
|
13
|
+
"""Handles creating local issues from remote issue data."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, core: RoadmapCore):
|
|
16
|
+
"""Initialize creation service.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
core: RoadmapCore instance
|
|
20
|
+
"""
|
|
21
|
+
self.core = core
|
|
22
|
+
|
|
23
|
+
def create_issue_from_remote(
|
|
24
|
+
self, remote_id: str | int, remote_issue: SyncIssue
|
|
25
|
+
) -> Issue:
|
|
26
|
+
"""Create a local Issue from remote SyncIssue data.
|
|
27
|
+
|
|
28
|
+
Extracts relevant fields from remote issue and creates a local Issue object.
|
|
29
|
+
Adds "synced:from-github" label to mark as synced from remote.
|
|
30
|
+
Uses remote milestone if available, otherwise defaults to backlog.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
remote_id: Remote issue ID (number)
|
|
34
|
+
remote_issue: SyncIssue object with remote data including:
|
|
35
|
+
- title: Issue title
|
|
36
|
+
- headline: Short description
|
|
37
|
+
- status: 'open', 'closed', etc.
|
|
38
|
+
- labels: List of label names
|
|
39
|
+
- assignee: Assignee login
|
|
40
|
+
- milestone: Milestone title or None
|
|
41
|
+
- backend_id: GitHub issue number
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
New Issue object ready to be created
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Normalize status
|
|
48
|
+
issue_status = self._normalize_status(remote_issue.status)
|
|
49
|
+
|
|
50
|
+
# Prepare labels with synced marker
|
|
51
|
+
labels = list(remote_issue.labels or [])
|
|
52
|
+
if "synced:from-github" not in labels:
|
|
53
|
+
labels.append("synced:from-github")
|
|
54
|
+
|
|
55
|
+
# Create the issue
|
|
56
|
+
new_issue = Issue(
|
|
57
|
+
title=remote_issue.title or f"Remote Issue {remote_id}",
|
|
58
|
+
content=remote_issue.headline or "",
|
|
59
|
+
status=issue_status,
|
|
60
|
+
labels=labels,
|
|
61
|
+
assignee=remote_issue.assignee,
|
|
62
|
+
milestone=remote_issue.milestone or "backlog",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Link to remote
|
|
66
|
+
if remote_issue.backend_id:
|
|
67
|
+
new_issue.remote_ids["github"] = remote_issue.backend_id
|
|
68
|
+
|
|
69
|
+
logger.info(
|
|
70
|
+
"remote_issue_created_locally",
|
|
71
|
+
remote_id=remote_id,
|
|
72
|
+
title=remote_issue.title,
|
|
73
|
+
local_status=issue_status,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return new_issue
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
"remote_issue_creation_failed",
|
|
81
|
+
remote_id=remote_id,
|
|
82
|
+
error=str(e),
|
|
83
|
+
error_type=type(e).__name__,
|
|
84
|
+
)
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _normalize_status(remote_status: str | None) -> Status:
|
|
89
|
+
"""Normalize remote status to local Status enum.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
remote_status: Remote status string (e.g., 'open', 'closed')
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Local Status enum value
|
|
96
|
+
"""
|
|
97
|
+
if not remote_status:
|
|
98
|
+
return Status.TODO
|
|
99
|
+
|
|
100
|
+
remote_lower = remote_status.lower()
|
|
101
|
+
|
|
102
|
+
# Map common remote statuses to local statuses
|
|
103
|
+
status_map = {
|
|
104
|
+
"open": Status.TODO,
|
|
105
|
+
"in_progress": Status.IN_PROGRESS,
|
|
106
|
+
"in progress": Status.IN_PROGRESS,
|
|
107
|
+
"done": Status.CLOSED,
|
|
108
|
+
"closed": Status.CLOSED,
|
|
109
|
+
"blocked": Status.BLOCKED,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return status_map.get(remote_lower, Status.TODO)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Service for analyzing and classifying sync changes."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from structlog import get_logger
|
|
6
|
+
|
|
7
|
+
from roadmap.core.services.sync.sync_key_normalizer import normalize_remote_keys
|
|
8
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState, SyncState
|
|
9
|
+
from roadmap.core.services.sync.sync_state_comparator import SyncStateComparator
|
|
10
|
+
from roadmap.core.services.sync.sync_state_manager import SyncStateManager
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SyncAnalysisService:
|
|
16
|
+
"""Handles change analysis and classification for sync operations."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
state_comparator: SyncStateComparator,
|
|
21
|
+
state_manager: SyncStateManager,
|
|
22
|
+
core=None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize analysis service.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
state_comparator: Service for detecting changes
|
|
28
|
+
state_manager: Service for managing baseline state
|
|
29
|
+
core: Optional RoadmapCore for database baseline fallback
|
|
30
|
+
"""
|
|
31
|
+
self.state_comparator = state_comparator
|
|
32
|
+
self.state_manager = state_manager
|
|
33
|
+
self.core = core
|
|
34
|
+
|
|
35
|
+
def load_baseline_safe(self):
|
|
36
|
+
"""Load baseline state but swallow errors and return None on failure.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
SyncState or None if not found
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
logger.debug("loading_sync_state")
|
|
43
|
+
base_state = self.state_manager.load_sync_state_from_db()
|
|
44
|
+
if base_state:
|
|
45
|
+
logger.info(
|
|
46
|
+
"previous_sync_state_loaded",
|
|
47
|
+
base_issues_count=len(base_state.base_issues),
|
|
48
|
+
last_sync=base_state.last_sync_time.isoformat()
|
|
49
|
+
if base_state.last_sync_time
|
|
50
|
+
else None,
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
base_state = self._load_baseline_from_core()
|
|
54
|
+
if base_state:
|
|
55
|
+
logger.info(
|
|
56
|
+
"previous_sync_state_loaded",
|
|
57
|
+
base_issues_count=len(base_state.base_issues),
|
|
58
|
+
last_sync=base_state.last_sync_time.isoformat()
|
|
59
|
+
if base_state.last_sync_time
|
|
60
|
+
else None,
|
|
61
|
+
source="core_db",
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
logger.info(
|
|
65
|
+
"no_previous_sync_state_found",
|
|
66
|
+
reason="first_sync_or_state_cleared",
|
|
67
|
+
)
|
|
68
|
+
except Exception:
|
|
69
|
+
logger.warning("sync_state_load_warning", reason="will_treat_as_first_sync")
|
|
70
|
+
base_state = None
|
|
71
|
+
|
|
72
|
+
return base_state
|
|
73
|
+
|
|
74
|
+
def _load_baseline_from_core(self) -> SyncState | None:
|
|
75
|
+
if not self.core or not getattr(self.core, "db", None):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
db_baseline = self.core.db.get_sync_baseline()
|
|
79
|
+
if not db_baseline:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
base_issues: dict[str, IssueBaseState] = {}
|
|
83
|
+
for issue_id, data in db_baseline.items():
|
|
84
|
+
base_issues[issue_id] = IssueBaseState(
|
|
85
|
+
id=issue_id,
|
|
86
|
+
status=data.get("status", "todo"),
|
|
87
|
+
title=data.get("title", ""),
|
|
88
|
+
assignee=data.get("assignee"),
|
|
89
|
+
headline=data.get("headline", ""),
|
|
90
|
+
content=data.get("content", ""),
|
|
91
|
+
labels=data.get("labels", []),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return SyncState(last_sync_time=datetime.now(UTC), base_issues=base_issues)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _is_empty_remote_value(value) -> bool:
|
|
98
|
+
if value is None:
|
|
99
|
+
return True
|
|
100
|
+
if isinstance(value, str):
|
|
101
|
+
return value.strip() == ""
|
|
102
|
+
if isinstance(value, (list, tuple, set, dict)):
|
|
103
|
+
return len(value) == 0
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _local_value_for_field(issue, field: str):
|
|
108
|
+
if issue is None:
|
|
109
|
+
return None
|
|
110
|
+
if field == "labels":
|
|
111
|
+
return issue.labels or []
|
|
112
|
+
if field == "assignee":
|
|
113
|
+
return issue.assignee
|
|
114
|
+
if field == "content":
|
|
115
|
+
return issue.content
|
|
116
|
+
return getattr(issue, field, None)
|
|
117
|
+
|
|
118
|
+
def _should_reclassify_as_push(self, change) -> bool:
|
|
119
|
+
if not change.remote_changes or not change.local_state:
|
|
120
|
+
return False
|
|
121
|
+
allowed_fields = {"labels", "assignee"}
|
|
122
|
+
for field, diff in change.remote_changes.items():
|
|
123
|
+
if field not in allowed_fields:
|
|
124
|
+
return False
|
|
125
|
+
remote_value = diff.get("to") if isinstance(diff, dict) else None
|
|
126
|
+
if not self._is_empty_remote_value(remote_value):
|
|
127
|
+
return False
|
|
128
|
+
local_value = self._local_value_for_field(change.local_state, field)
|
|
129
|
+
if self._is_empty_remote_value(local_value):
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def _reclassify_remote_only_changes(self, remote_only_changes):
|
|
134
|
+
reclassified = [
|
|
135
|
+
c for c in remote_only_changes if self._should_reclassify_as_push(c)
|
|
136
|
+
]
|
|
137
|
+
remaining = [c for c in remote_only_changes if c not in reclassified]
|
|
138
|
+
return reclassified, remaining
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _extract_remote_id(remote_obj):
|
|
142
|
+
if isinstance(remote_obj, dict):
|
|
143
|
+
backend_id = remote_obj.get("backend_id") or remote_obj.get("number")
|
|
144
|
+
if backend_id is not None:
|
|
145
|
+
return str(backend_id)
|
|
146
|
+
remote_ids = remote_obj.get("remote_ids") or {}
|
|
147
|
+
backend_name = remote_obj.get("backend_name")
|
|
148
|
+
if backend_name and backend_name in remote_ids:
|
|
149
|
+
return str(remote_ids.get(backend_name))
|
|
150
|
+
if "github" in remote_ids:
|
|
151
|
+
return str(remote_ids.get("github"))
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
backend_id = getattr(remote_obj, "backend_id", None)
|
|
155
|
+
if backend_id is not None:
|
|
156
|
+
return str(backend_id)
|
|
157
|
+
backend_name = getattr(remote_obj, "backend_name", None)
|
|
158
|
+
remote_ids = getattr(remote_obj, "remote_ids", None) or {}
|
|
159
|
+
if backend_name and backend_name in remote_ids:
|
|
160
|
+
return str(remote_ids.get(backend_name))
|
|
161
|
+
if "github" in remote_ids:
|
|
162
|
+
return str(remote_ids.get("github"))
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _build_pull_ids(self, remote_only_changes, normalized_remote) -> list[str]:
|
|
166
|
+
pulls: list[str] = []
|
|
167
|
+
for change in remote_only_changes:
|
|
168
|
+
if change.issue_id in normalized_remote:
|
|
169
|
+
remote_obj = normalized_remote[change.issue_id]
|
|
170
|
+
remote_id = self._extract_remote_id(remote_obj)
|
|
171
|
+
if remote_id:
|
|
172
|
+
pulls.append(remote_id)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
issue_id = str(change.issue_id)
|
|
176
|
+
if issue_id.isdigit() or issue_id.startswith("_remote_"):
|
|
177
|
+
pulls.append(issue_id)
|
|
178
|
+
else:
|
|
179
|
+
logger.debug(
|
|
180
|
+
"pull_id_unresolved",
|
|
181
|
+
issue_id=change.issue_id,
|
|
182
|
+
reason="no_remote_id",
|
|
183
|
+
)
|
|
184
|
+
return pulls
|
|
185
|
+
|
|
186
|
+
def analyze_and_classify(self, local_issues_dict, remote_issues_data, base_state):
|
|
187
|
+
"""Run comparator and classify changes into categories.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
local_issues_dict: Dict of local Issue objects (id -> Issue)
|
|
191
|
+
remote_issues_data: Dict of remote issue data (keyed by remote ID)
|
|
192
|
+
base_state: Previous sync state or None
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Tuple of (changes, conflicts, local_only_changes, remote_only_changes,
|
|
196
|
+
no_changes, updates, pulls, up_to_date)
|
|
197
|
+
"""
|
|
198
|
+
changes = self.state_comparator.analyze_three_way(
|
|
199
|
+
local_issues_dict,
|
|
200
|
+
remote_issues_data,
|
|
201
|
+
base_state.base_issues if base_state else None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
conflicts = [c for c in changes if c.has_conflict]
|
|
205
|
+
local_only_changes = [c for c in changes if c.is_local_only_change()]
|
|
206
|
+
remote_only_changes = [c for c in changes if c.is_remote_only_change()]
|
|
207
|
+
no_changes = [c for c in changes if c.conflict_type == "no_change"]
|
|
208
|
+
|
|
209
|
+
reclassified, remote_only_changes = self._reclassify_remote_only_changes(
|
|
210
|
+
remote_only_changes
|
|
211
|
+
)
|
|
212
|
+
if reclassified:
|
|
213
|
+
local_only_changes = local_only_changes + reclassified
|
|
214
|
+
|
|
215
|
+
updates = [c.local_state for c in local_only_changes if c.local_state]
|
|
216
|
+
|
|
217
|
+
normalized_remote = remote_issues_data
|
|
218
|
+
if self.state_comparator.backend is not None:
|
|
219
|
+
_, normalized_remote = normalize_remote_keys(
|
|
220
|
+
local_issues_dict,
|
|
221
|
+
remote_issues_data,
|
|
222
|
+
self.state_comparator.backend,
|
|
223
|
+
logger=logger,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# For pulls, we need to pass the remote ID (GitHub issue number) to pull_issues()
|
|
227
|
+
# Use normalized_remote so issue_id lookups match analysis keys.
|
|
228
|
+
pulls = self._build_pull_ids(remote_only_changes, normalized_remote)
|
|
229
|
+
|
|
230
|
+
up_to_date = [c.issue_id for c in no_changes]
|
|
231
|
+
|
|
232
|
+
logger.debug(
|
|
233
|
+
"three_way_analysis_complete",
|
|
234
|
+
total_changes=len(changes),
|
|
235
|
+
conflicts=len(conflicts),
|
|
236
|
+
local_only=len(local_only_changes),
|
|
237
|
+
remote_only=len(remote_only_changes),
|
|
238
|
+
no_change=len(no_changes),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
changes,
|
|
243
|
+
conflicts,
|
|
244
|
+
local_only_changes,
|
|
245
|
+
remote_only_changes,
|
|
246
|
+
no_changes,
|
|
247
|
+
updates,
|
|
248
|
+
pulls,
|
|
249
|
+
up_to_date,
|
|
250
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Service for handling sync authentication with pluggable backends.
|
|
2
|
+
|
|
3
|
+
Updated to use Result<T, SyncError> pattern for explicit error handling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
from roadmap.core.interfaces.sync_backend import SyncBackendInterface
|
|
9
|
+
from roadmap.core.services.sync.sync_report import SyncReport
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SyncAuthenticationService:
|
|
15
|
+
"""Handles backend authentication for sync operations."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, backend: SyncBackendInterface):
|
|
18
|
+
"""Initialize authentication service.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
backend: SyncBackendInterface implementation
|
|
22
|
+
"""
|
|
23
|
+
self.backend = backend
|
|
24
|
+
|
|
25
|
+
def ensure_authenticated(self, report: SyncReport) -> bool:
|
|
26
|
+
"""Ensure backend is authenticated.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
report: SyncReport to record any authentication errors
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if authenticated, False otherwise
|
|
33
|
+
|
|
34
|
+
Notes:
|
|
35
|
+
Handles Result<bool, SyncError> from backend.authenticate()
|
|
36
|
+
"""
|
|
37
|
+
result = self.backend.authenticate()
|
|
38
|
+
|
|
39
|
+
if result.is_ok():
|
|
40
|
+
logger.info("backend_authenticated_successfully")
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# Handle authentication error
|
|
44
|
+
error = result.unwrap_err()
|
|
45
|
+
report.error = str(error)
|
|
46
|
+
|
|
47
|
+
logger.error(
|
|
48
|
+
"backend_authentication_failed",
|
|
49
|
+
operation="authenticate",
|
|
50
|
+
backend_type=type(self.backend).__name__,
|
|
51
|
+
error_type=error.error_type.value,
|
|
52
|
+
error_message=error.message,
|
|
53
|
+
is_recoverable=error.is_recoverable,
|
|
54
|
+
suggested_action=error.suggested_fix or "check_credentials",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return False
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Service for fetching and counting sync data from local and remote sources.
|
|
2
|
+
|
|
3
|
+
Updated to use Result<T, SyncError> pattern for explicit error handling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
from roadmap.core.interfaces.sync_backend import SyncBackendInterface
|
|
9
|
+
from roadmap.core.services.sync.sync_report import SyncReport
|
|
10
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SyncDataFetchService:
|
|
16
|
+
"""Handles fetching issues and metadata from local and remote sources."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, core: RoadmapCore, backend: SyncBackendInterface):
|
|
19
|
+
"""Initialize data fetch service.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
core: RoadmapCore instance
|
|
23
|
+
backend: SyncBackendInterface implementation
|
|
24
|
+
"""
|
|
25
|
+
self.core = core
|
|
26
|
+
self.backend = backend
|
|
27
|
+
|
|
28
|
+
def fetch_remote_issues(self, report: SyncReport):
|
|
29
|
+
"""Fetch remote issues from backend.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
report: SyncReport to record any fetch errors
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict of remote issues or None if fetch failed
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
Handles Result<dict, SyncError> from backend.get_issues()
|
|
39
|
+
"""
|
|
40
|
+
logger.info("fetching_remote_issues_starting")
|
|
41
|
+
|
|
42
|
+
result = self.backend.get_issues()
|
|
43
|
+
|
|
44
|
+
if result.is_ok():
|
|
45
|
+
remote_issues_data = result.unwrap()
|
|
46
|
+
|
|
47
|
+
# Count by status
|
|
48
|
+
status_counts = {}
|
|
49
|
+
for issue in remote_issues_data.values():
|
|
50
|
+
status = getattr(issue, "state", None) or getattr(
|
|
51
|
+
issue, "status", "unknown"
|
|
52
|
+
)
|
|
53
|
+
status = str(status)
|
|
54
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
55
|
+
|
|
56
|
+
logger.info(
|
|
57
|
+
"remote_issues_enumerated",
|
|
58
|
+
total_count=len(remote_issues_data),
|
|
59
|
+
status_breakdown=status_counts,
|
|
60
|
+
)
|
|
61
|
+
return remote_issues_data
|
|
62
|
+
|
|
63
|
+
# Handle fetch error
|
|
64
|
+
error = result.unwrap_err()
|
|
65
|
+
report.error = str(error)
|
|
66
|
+
|
|
67
|
+
logger.error(
|
|
68
|
+
"remote_issues_fetch_error",
|
|
69
|
+
operation="fetch_remote_issues",
|
|
70
|
+
error_type=error.error_type.value,
|
|
71
|
+
error_message=error.message,
|
|
72
|
+
is_recoverable=error.is_recoverable,
|
|
73
|
+
suggested_action=error.suggested_fix or "check_backend_connectivity",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def fetch_local_issues(self, report: SyncReport):
|
|
79
|
+
"""Fetch local issues from roadmap.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
report: SyncReport to record any fetch errors
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of local Issue objects or None if fetch failed
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
logger.info(
|
|
89
|
+
"fetching_local_issues_starting",
|
|
90
|
+
)
|
|
91
|
+
local_issues = self.core.issues.list_all_including_archived() or []
|
|
92
|
+
|
|
93
|
+
# Count by status
|
|
94
|
+
status_counts = {}
|
|
95
|
+
for issue in local_issues:
|
|
96
|
+
status = str(issue.status)
|
|
97
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
98
|
+
|
|
99
|
+
logger.info(
|
|
100
|
+
"local_issues_enumerated",
|
|
101
|
+
total_count=len(local_issues),
|
|
102
|
+
status_breakdown=status_counts,
|
|
103
|
+
)
|
|
104
|
+
return local_issues
|
|
105
|
+
except OSError as e:
|
|
106
|
+
report.error = f"Failed to fetch local issues: {str(e)}"
|
|
107
|
+
logger.error(
|
|
108
|
+
"local_issues_fetch_error",
|
|
109
|
+
operation="fetch_local_issues",
|
|
110
|
+
error_type=type(e).__name__,
|
|
111
|
+
error=str(e),
|
|
112
|
+
is_recoverable=False,
|
|
113
|
+
suggested_action="check_file_permissions",
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
except Exception as e:
|
|
117
|
+
report.error = f"Failed to fetch local issues: {str(e)}"
|
|
118
|
+
logger.error(
|
|
119
|
+
"local_issues_fetch_error",
|
|
120
|
+
operation="fetch_local_issues",
|
|
121
|
+
error_type=type(e).__name__,
|
|
122
|
+
error=str(e),
|
|
123
|
+
error_classification="sync_error",
|
|
124
|
+
)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def count_active_archived(self, local_issues):
|
|
128
|
+
"""Count active and archived issues.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
local_issues: List of Issue objects
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple of (active_count, archived_count)
|
|
135
|
+
"""
|
|
136
|
+
active_issues_count = 0
|
|
137
|
+
archived_issues_count = 0
|
|
138
|
+
for issue in local_issues:
|
|
139
|
+
if issue.file_path and "archive" in issue.file_path:
|
|
140
|
+
archived_issues_count += 1
|
|
141
|
+
else:
|
|
142
|
+
active_issues_count += 1
|
|
143
|
+
return active_issues_count, archived_issues_count
|
|
144
|
+
|
|
145
|
+
def count_milestones(self):
|
|
146
|
+
"""Count active and archived milestones.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (all_milestones, active_count, archived_count)
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
all_milestones = self.core.milestones.list()
|
|
153
|
+
active_milestones_count = 0
|
|
154
|
+
archived_milestones_count = 0
|
|
155
|
+
for milestone in all_milestones:
|
|
156
|
+
if (
|
|
157
|
+
hasattr(milestone, "file_path")
|
|
158
|
+
and milestone.file_path
|
|
159
|
+
and "archive" in milestone.file_path
|
|
160
|
+
):
|
|
161
|
+
archived_milestones_count += 1
|
|
162
|
+
else:
|
|
163
|
+
active_milestones_count += 1
|
|
164
|
+
return all_milestones, active_milestones_count, archived_milestones_count
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.debug("milestone_count_failed", error=str(e))
|
|
167
|
+
return [], 0, 0
|
|
168
|
+
|
|
169
|
+
def count_remote_stats(self, remote_issues_data):
|
|
170
|
+
"""Count remote issues and milestones statistics.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
remote_issues_data: Dict of remote issues
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Tuple of (total_count, open_count, closed_count, milestones_count)
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
remote_issues_count = len(remote_issues_data) if remote_issues_data else 0
|
|
180
|
+
remote_open_count = 0
|
|
181
|
+
remote_closed_count = 0
|
|
182
|
+
for issue_data in remote_issues_data.values() if remote_issues_data else []:
|
|
183
|
+
state = getattr(issue_data, "state", None) or getattr(
|
|
184
|
+
issue_data, "status", "open"
|
|
185
|
+
)
|
|
186
|
+
if state and state.lower() == "closed":
|
|
187
|
+
remote_closed_count += 1
|
|
188
|
+
else:
|
|
189
|
+
remote_open_count += 1
|
|
190
|
+
|
|
191
|
+
milestones_result = self.backend.get_milestones()
|
|
192
|
+
remote_milestones_count = (
|
|
193
|
+
len(milestones_result.unwrap()) if milestones_result.is_ok() else 0
|
|
194
|
+
)
|
|
195
|
+
logger.debug(
|
|
196
|
+
"remote_items_counted",
|
|
197
|
+
remote_issues=remote_issues_count,
|
|
198
|
+
remote_open=remote_open_count,
|
|
199
|
+
remote_closed=remote_closed_count,
|
|
200
|
+
remote_milestones=remote_milestones_count,
|
|
201
|
+
)
|
|
202
|
+
return (
|
|
203
|
+
remote_issues_count,
|
|
204
|
+
remote_open_count,
|
|
205
|
+
remote_closed_count,
|
|
206
|
+
remote_milestones_count,
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(
|
|
210
|
+
"count_remote_stats_error", error=str(e), severity="operational"
|
|
211
|
+
)
|
|
212
|
+
return 0, 0, 0, 0
|