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,690 @@
|
|
|
1
|
+
"""Sync state comparison service.
|
|
2
|
+
|
|
3
|
+
Provides logic to compare local and remote issue states, identifying
|
|
4
|
+
what needs to be pushed, pulled, or resolved. Backend-agnostic.
|
|
5
|
+
Supports both two-way (legacy) and three-way merge analysis.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from structlog import get_logger
|
|
13
|
+
|
|
14
|
+
from roadmap.core.domain.issue import Issue
|
|
15
|
+
from roadmap.core.models.sync_models import SyncIssue
|
|
16
|
+
from roadmap.core.services.sync.sync_change_computer import (
|
|
17
|
+
compute_changes as _compute_changes_helper,
|
|
18
|
+
)
|
|
19
|
+
from roadmap.core.services.sync.sync_change_computer import (
|
|
20
|
+
compute_changes_remote as _compute_changes_remote_helper,
|
|
21
|
+
)
|
|
22
|
+
from roadmap.core.services.sync.sync_conflict_detector import (
|
|
23
|
+
detect_field_conflicts as _detect_field_conflicts_helper,
|
|
24
|
+
)
|
|
25
|
+
from roadmap.core.services.sync.sync_conflict_resolver import Conflict, ConflictField
|
|
26
|
+
from roadmap.core.services.sync.sync_key_normalizer import (
|
|
27
|
+
normalize_remote_keys as _normalize_remote_keys_helper,
|
|
28
|
+
)
|
|
29
|
+
from roadmap.core.services.sync.sync_report import IssueChange
|
|
30
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState
|
|
31
|
+
from roadmap.core.services.sync.sync_state_normalizer import (
|
|
32
|
+
extract_timestamp as _extract_timestamp_helper,
|
|
33
|
+
)
|
|
34
|
+
from roadmap.core.services.sync.sync_state_normalizer import (
|
|
35
|
+
normalize_remote_state as _normalize_remote_state_helper,
|
|
36
|
+
)
|
|
37
|
+
from roadmap.core.services.sync.sync_three_way import (
|
|
38
|
+
build_issue_change as _build_issue_change_helper,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = get_logger()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SyncStateComparator:
|
|
45
|
+
"""Compares local and remote issue states.
|
|
46
|
+
|
|
47
|
+
Identifies conflicts, updates, and items requiring push/pull
|
|
48
|
+
without knowledge of the backend implementation.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
fields_to_sync: list[str] | None = None,
|
|
54
|
+
backend=None, # Optional: SyncBackendInterface for key normalization
|
|
55
|
+
):
|
|
56
|
+
"""Initialize the state comparator.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
fields_to_sync: Fields to check for changes (default: common fields)
|
|
60
|
+
Note: 'title' is intentionally excluded as it's display metadata
|
|
61
|
+
that doesn't affect workflow state. Only sync fields that change
|
|
62
|
+
the work item's state (status, labels, priority, etc).
|
|
63
|
+
backend: Optional backend instance for normalizing remote issue keys.
|
|
64
|
+
When provided, remote issue keys are normalized to match local keys
|
|
65
|
+
using backend-specific ID mapping (e.g., gh-42 → 42).
|
|
66
|
+
"""
|
|
67
|
+
self.logger = get_logger()
|
|
68
|
+
self.backend = backend
|
|
69
|
+
default_fields = [
|
|
70
|
+
"status",
|
|
71
|
+
"priority",
|
|
72
|
+
"content",
|
|
73
|
+
"assignee",
|
|
74
|
+
]
|
|
75
|
+
self.fields_to_sync = fields_to_sync or default_fields
|
|
76
|
+
self.logger.debug(
|
|
77
|
+
"sync_state_comparator_initialized",
|
|
78
|
+
fields_to_sync=self.fields_to_sync,
|
|
79
|
+
backend_aware=backend is not None,
|
|
80
|
+
note="title excluded from sync as display metadata",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _normalize_remote_keys(
|
|
84
|
+
self,
|
|
85
|
+
local: dict[str, Issue],
|
|
86
|
+
remote: Mapping[str, Any],
|
|
87
|
+
) -> tuple[dict[str, Issue], Mapping[str, Any]]:
|
|
88
|
+
"""Normalize remote issue keys to match local issue keys.
|
|
89
|
+
|
|
90
|
+
Local issues are keyed by UUID (e.g., "7e99d67b").
|
|
91
|
+
Remote issues may be keyed by backend-specific IDs (e.g., "gh-42" or "42").
|
|
92
|
+
|
|
93
|
+
This method creates a mapping by matching remote IDs to local remote_ids
|
|
94
|
+
using two sources (in order of preference):
|
|
95
|
+
1. Database cache (fast O(1) lookups via RemoteLinkRepository)
|
|
96
|
+
2. YAML files (fallback if database not available)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
local: Dict of local issues keyed by UUID
|
|
100
|
+
remote: Dict of remote SyncIssue objects keyed by backend-specific ID
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (local, normalized_remote) where normalized_remote is keyed
|
|
104
|
+
by local issue UUIDs for proper matching.
|
|
105
|
+
"""
|
|
106
|
+
return _normalize_remote_keys_helper(
|
|
107
|
+
local, remote, self.backend, logger=self.logger
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def identify_conflicts(
|
|
111
|
+
self,
|
|
112
|
+
local: dict[str, Issue],
|
|
113
|
+
remote: dict[str, dict[str, Any]],
|
|
114
|
+
) -> list[Conflict]:
|
|
115
|
+
"""Identify issues that differ in both local and remote.
|
|
116
|
+
|
|
117
|
+
A conflict exists when:
|
|
118
|
+
1. Same issue exists in both local and remote
|
|
119
|
+
2. They have different values for at least one field
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
local: Dict of local issues keyed by ID
|
|
123
|
+
remote: Dict of remote issues keyed by ID
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of Conflict objects representing detected conflicts
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If issue data is invalid
|
|
130
|
+
"""
|
|
131
|
+
self.logger.info(
|
|
132
|
+
"identify_conflicts_start",
|
|
133
|
+
local_count=len(local),
|
|
134
|
+
remote_count=len(remote),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
conflicts = []
|
|
138
|
+
common_ids = set(local.keys()) & set(remote.keys())
|
|
139
|
+
|
|
140
|
+
self.logger.debug(
|
|
141
|
+
"identifying_conflicts",
|
|
142
|
+
common_issue_count=len(common_ids),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
for issue_id in common_ids:
|
|
146
|
+
try:
|
|
147
|
+
local_issue = local[issue_id]
|
|
148
|
+
remote_issue = remote[issue_id]
|
|
149
|
+
|
|
150
|
+
# Detect field-level conflicts
|
|
151
|
+
conflicting_fields = self._detect_field_conflicts(
|
|
152
|
+
local_issue, remote_issue
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if conflicting_fields:
|
|
156
|
+
# Extract updated timestamps
|
|
157
|
+
local_updated = local_issue.updated
|
|
158
|
+
remote_updated = self._extract_timestamp(remote_issue, "updated_at")
|
|
159
|
+
|
|
160
|
+
conflict = Conflict(
|
|
161
|
+
issue_id=issue_id,
|
|
162
|
+
local_issue=local_issue,
|
|
163
|
+
remote_issue=remote_issue,
|
|
164
|
+
fields=conflicting_fields,
|
|
165
|
+
local_updated=local_updated,
|
|
166
|
+
remote_updated=remote_updated,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
conflicts.append(conflict)
|
|
170
|
+
self.logger.debug(
|
|
171
|
+
"conflict_identified",
|
|
172
|
+
issue_id=issue_id,
|
|
173
|
+
field_count=len(conflicting_fields),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
self.logger.error(
|
|
178
|
+
"conflict_detection_error",
|
|
179
|
+
issue_id=issue_id,
|
|
180
|
+
error=str(e),
|
|
181
|
+
)
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Failed to detect conflicts for {issue_id}: {str(e)}"
|
|
184
|
+
) from e
|
|
185
|
+
|
|
186
|
+
self.logger.info("identify_conflicts_complete", conflict_count=len(conflicts))
|
|
187
|
+
return conflicts
|
|
188
|
+
|
|
189
|
+
def identify_updates(
|
|
190
|
+
self,
|
|
191
|
+
local: dict[str, Issue],
|
|
192
|
+
remote: dict[str, dict[str, Any]],
|
|
193
|
+
) -> list[Issue]:
|
|
194
|
+
"""Identify local issues that need pushing to remote.
|
|
195
|
+
|
|
196
|
+
An update is needed when:
|
|
197
|
+
1. Issue exists only in local (new issue)
|
|
198
|
+
2. Issue exists in both but local is newer
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
local: Dict of local issues keyed by ID
|
|
202
|
+
remote: Dict of remote issues keyed by ID
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of Issues that need to be pushed
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If issue data is invalid
|
|
209
|
+
"""
|
|
210
|
+
self.logger.info(
|
|
211
|
+
"identify_updates_start",
|
|
212
|
+
local_count=len(local),
|
|
213
|
+
remote_count=len(remote),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
updates = []
|
|
217
|
+
|
|
218
|
+
for issue_id, local_issue in local.items():
|
|
219
|
+
try:
|
|
220
|
+
# New issue (doesn't exist in remote)
|
|
221
|
+
if issue_id not in remote:
|
|
222
|
+
self.logger.debug("new_local_issue_identified", issue_id=issue_id)
|
|
223
|
+
updates.append(local_issue)
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
# Existing issue - check if local is newer
|
|
227
|
+
remote_issue = remote[issue_id]
|
|
228
|
+
remote_updated = self._extract_timestamp(remote_issue, "updated_at")
|
|
229
|
+
|
|
230
|
+
if remote_updated is None:
|
|
231
|
+
# Can't compare - assume local should be pushed
|
|
232
|
+
self.logger.debug(
|
|
233
|
+
"no_remote_timestamp_treating_as_update",
|
|
234
|
+
issue_id=issue_id,
|
|
235
|
+
)
|
|
236
|
+
updates.append(local_issue)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if local_issue.updated > remote_updated:
|
|
240
|
+
time_diff = (local_issue.updated - remote_updated).total_seconds()
|
|
241
|
+
self.logger.debug(
|
|
242
|
+
"local_is_newer_needs_update",
|
|
243
|
+
issue_id=issue_id,
|
|
244
|
+
seconds_ahead=time_diff,
|
|
245
|
+
)
|
|
246
|
+
updates.append(local_issue)
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.logger.error(
|
|
250
|
+
"update_detection_error",
|
|
251
|
+
issue_id=issue_id,
|
|
252
|
+
error=str(e),
|
|
253
|
+
)
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"Failed to detect updates for {issue_id}: {str(e)}"
|
|
256
|
+
) from e
|
|
257
|
+
|
|
258
|
+
self.logger.info("identify_updates_complete", update_count=len(updates))
|
|
259
|
+
return updates
|
|
260
|
+
|
|
261
|
+
def identify_pulls(
|
|
262
|
+
self,
|
|
263
|
+
local: dict[str, Issue],
|
|
264
|
+
remote: dict[str, dict[str, Any]],
|
|
265
|
+
) -> list[dict[str, Any]]:
|
|
266
|
+
"""Identify remote issues that need pulling to local.
|
|
267
|
+
|
|
268
|
+
A pull is needed when:
|
|
269
|
+
1. Issue exists only in remote (new from remote)
|
|
270
|
+
2. Issue exists in both but remote is newer
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
local: Dict of local issues keyed by ID
|
|
274
|
+
remote: Dict of remote issues keyed by ID
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List of remote issues that need to be pulled
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
ValueError: If issue data is invalid
|
|
281
|
+
"""
|
|
282
|
+
self.logger.info(
|
|
283
|
+
"identify_pulls_start",
|
|
284
|
+
local_count=len(local),
|
|
285
|
+
remote_count=len(remote),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
pulls = []
|
|
289
|
+
|
|
290
|
+
for issue_id, remote_issue in remote.items():
|
|
291
|
+
try:
|
|
292
|
+
# New remote issue (doesn't exist locally)
|
|
293
|
+
if issue_id not in local:
|
|
294
|
+
self.logger.debug("new_remote_issue_identified", issue_id=issue_id)
|
|
295
|
+
pulls.append(issue_id)
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Existing issue - check if remote is newer
|
|
299
|
+
local_issue = local[issue_id]
|
|
300
|
+
remote_updated = self._extract_timestamp(remote_issue, "updated_at")
|
|
301
|
+
|
|
302
|
+
if remote_updated is None:
|
|
303
|
+
# Can't compare - skip
|
|
304
|
+
self.logger.debug(
|
|
305
|
+
"no_remote_timestamp_skipping_pull",
|
|
306
|
+
issue_id=issue_id,
|
|
307
|
+
)
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
if remote_updated > local_issue.updated:
|
|
311
|
+
time_diff = (remote_updated - local_issue.updated).total_seconds()
|
|
312
|
+
self.logger.debug(
|
|
313
|
+
"remote_is_newer_needs_pull",
|
|
314
|
+
issue_id=issue_id,
|
|
315
|
+
seconds_ahead=time_diff,
|
|
316
|
+
)
|
|
317
|
+
pulls.append(issue_id)
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self.logger.error(
|
|
321
|
+
"pull_detection_error",
|
|
322
|
+
issue_id=issue_id,
|
|
323
|
+
error=str(e),
|
|
324
|
+
)
|
|
325
|
+
raise ValueError(
|
|
326
|
+
f"Failed to detect pulls for {issue_id}: {str(e)}"
|
|
327
|
+
) from e
|
|
328
|
+
|
|
329
|
+
self.logger.info("identify_pulls_complete", pull_count=len(pulls))
|
|
330
|
+
return pulls
|
|
331
|
+
|
|
332
|
+
def identify_up_to_date(
|
|
333
|
+
self,
|
|
334
|
+
local: dict[str, Issue],
|
|
335
|
+
remote: dict[str, dict[str, Any]],
|
|
336
|
+
) -> list[str]:
|
|
337
|
+
"""Identify issues that are up-to-date in both local and remote.
|
|
338
|
+
|
|
339
|
+
An issue is up-to-date when:
|
|
340
|
+
1. It exists in both local and remote
|
|
341
|
+
2. They have identical values (or no conflict detected)
|
|
342
|
+
3. Timestamps are equal
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
local: Dict of local issues keyed by ID
|
|
346
|
+
remote: Dict of remote issues keyed by ID
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
List of issue IDs that are up-to-date
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
ValueError: If issue data is invalid
|
|
353
|
+
"""
|
|
354
|
+
self.logger.info(
|
|
355
|
+
"identify_up_to_date_start",
|
|
356
|
+
local_count=len(local),
|
|
357
|
+
remote_count=len(remote),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
up_to_date = []
|
|
361
|
+
common_ids = set(local.keys()) & set(remote.keys())
|
|
362
|
+
|
|
363
|
+
for issue_id in common_ids:
|
|
364
|
+
try:
|
|
365
|
+
local_issue = local[issue_id]
|
|
366
|
+
remote_issue = remote[issue_id]
|
|
367
|
+
|
|
368
|
+
# Check if they have conflicts
|
|
369
|
+
conflicts = self._detect_field_conflicts(local_issue, remote_issue)
|
|
370
|
+
|
|
371
|
+
if conflicts:
|
|
372
|
+
# Has conflicts - not up to date
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
# Check timestamps
|
|
376
|
+
remote_updated = self._extract_timestamp(remote_issue, "updated_at")
|
|
377
|
+
|
|
378
|
+
if remote_updated is not None and local_issue.updated != remote_updated:
|
|
379
|
+
# Different timestamps - not up to date
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# No conflicts and timestamps match (or can't compare)
|
|
383
|
+
up_to_date.append(issue_id)
|
|
384
|
+
self.logger.debug("issue_is_up_to_date", issue_id=issue_id)
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
self.logger.warning(
|
|
388
|
+
"up_to_date_check_error",
|
|
389
|
+
issue_id=issue_id,
|
|
390
|
+
error=str(e),
|
|
391
|
+
)
|
|
392
|
+
# If we can't determine, assume not up-to-date
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
self.logger.info(
|
|
396
|
+
"identify_up_to_date_complete", up_to_date_count=len(up_to_date)
|
|
397
|
+
)
|
|
398
|
+
return up_to_date
|
|
399
|
+
|
|
400
|
+
def _detect_field_conflicts(
|
|
401
|
+
self,
|
|
402
|
+
local: Issue,
|
|
403
|
+
remote: dict[str, Any] | object,
|
|
404
|
+
) -> list[ConflictField]:
|
|
405
|
+
"""Detect field-level conflicts between local and remote issues.
|
|
406
|
+
|
|
407
|
+
Compares only fields that affect workflow state. The 'title' field is
|
|
408
|
+
intentionally excluded as it's metadata that doesn't change work item
|
|
409
|
+
state or require synchronization.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
local: The local Issue
|
|
413
|
+
remote: The remote issue dict
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
List of ConflictFields representing conflicts
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
ValueError: If field data is invalid
|
|
420
|
+
"""
|
|
421
|
+
return _detect_field_conflicts_helper(
|
|
422
|
+
local,
|
|
423
|
+
remote,
|
|
424
|
+
self.fields_to_sync,
|
|
425
|
+
extract_timestamp=self._extract_timestamp,
|
|
426
|
+
logger=self.logger,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _extract_timestamp(
|
|
430
|
+
self,
|
|
431
|
+
data: dict[str, Any] | object,
|
|
432
|
+
timestamp_field: str = "updated_at",
|
|
433
|
+
) -> datetime | None:
|
|
434
|
+
"""Extract and parse a timestamp from remote issue data.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
data: The remote issue dict
|
|
438
|
+
timestamp_field: The field name containing the timestamp
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Parsed datetime or None if not found/invalid
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
ValueError: If timestamp format is invalid
|
|
445
|
+
"""
|
|
446
|
+
return _extract_timestamp_helper(data, timestamp_field, logger=self.logger)
|
|
447
|
+
|
|
448
|
+
def _resolve_issue_title(
|
|
449
|
+
self,
|
|
450
|
+
issue_id: str,
|
|
451
|
+
local: Issue | None,
|
|
452
|
+
remote: Any | None,
|
|
453
|
+
baseline: IssueBaseState | None,
|
|
454
|
+
) -> str:
|
|
455
|
+
"""Resolve a sensible title for presentation and logging."""
|
|
456
|
+
title = None
|
|
457
|
+
if local is not None:
|
|
458
|
+
title = getattr(local, "title", None)
|
|
459
|
+
if not title and remote is not None:
|
|
460
|
+
title = (
|
|
461
|
+
remote.get("title")
|
|
462
|
+
if isinstance(remote, dict)
|
|
463
|
+
else getattr(remote, "title", None)
|
|
464
|
+
)
|
|
465
|
+
if not title and baseline is not None:
|
|
466
|
+
title = getattr(baseline, "title", issue_id)
|
|
467
|
+
return title or issue_id
|
|
468
|
+
|
|
469
|
+
def _normalize_remote_state(self, remote: Any) -> dict[str, Any] | None:
|
|
470
|
+
"""Normalize remote representations into a plain dict for comparison."""
|
|
471
|
+
return _normalize_remote_state_helper(remote, logger=self.logger)
|
|
472
|
+
|
|
473
|
+
def _handle_first_sync_semantics(
|
|
474
|
+
self,
|
|
475
|
+
change: IssueChange,
|
|
476
|
+
local: Issue | None,
|
|
477
|
+
remote_state: dict | None,
|
|
478
|
+
baseline: IssueBaseState | None,
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Handle first-sync special cases where baseline is missing."""
|
|
481
|
+
if local is not None and remote_state is None:
|
|
482
|
+
change.local_changes = (
|
|
483
|
+
self._compute_changes(baseline, local) if local is not None else {}
|
|
484
|
+
)
|
|
485
|
+
change.local_changes["_new"] = True
|
|
486
|
+
change.conflict_type = "local_only"
|
|
487
|
+
change.has_conflict = False
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if remote_state is not None and local is None:
|
|
491
|
+
change.remote_changes = (
|
|
492
|
+
self._compute_changes_remote(baseline, remote_state)
|
|
493
|
+
if remote_state is not None
|
|
494
|
+
else {}
|
|
495
|
+
)
|
|
496
|
+
change.remote_changes["_new"] = True
|
|
497
|
+
change.conflict_type = "remote_only"
|
|
498
|
+
change.has_conflict = False
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
if local is not None and remote_state is not None:
|
|
502
|
+
# Both present: simplified rule - differing status => conflict
|
|
503
|
+
from roadmap.common.constants import Status
|
|
504
|
+
|
|
505
|
+
# Normalize remote status
|
|
506
|
+
remote_status = (
|
|
507
|
+
remote_state.get("status") if isinstance(remote_state, dict) else None
|
|
508
|
+
)
|
|
509
|
+
try:
|
|
510
|
+
if isinstance(remote_status, str):
|
|
511
|
+
try:
|
|
512
|
+
remote_status = Status(remote_status)
|
|
513
|
+
except Exception:
|
|
514
|
+
remote_status = Status(remote_status.lower())
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.debug(
|
|
517
|
+
"status_normalization_failed",
|
|
518
|
+
operation="normalize_status",
|
|
519
|
+
field="remote_status",
|
|
520
|
+
error=str(e),
|
|
521
|
+
action="Skipping status normalization",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
local_status = getattr(local, "status", None)
|
|
525
|
+
|
|
526
|
+
def status_value(s):
|
|
527
|
+
if s is None:
|
|
528
|
+
return None
|
|
529
|
+
return s.value if hasattr(s, "value") else str(s).lower()
|
|
530
|
+
|
|
531
|
+
change.local_changes.setdefault("_new", True)
|
|
532
|
+
change.remote_changes.setdefault("_new", True)
|
|
533
|
+
|
|
534
|
+
if status_value(local_status) == status_value(remote_status):
|
|
535
|
+
change.local_changes = {}
|
|
536
|
+
change.remote_changes = {}
|
|
537
|
+
change.conflict_type = "no_change"
|
|
538
|
+
change.has_conflict = False
|
|
539
|
+
else:
|
|
540
|
+
mutual_conflicts = self._detect_field_conflicts(local, remote_state)
|
|
541
|
+
flagged = {}
|
|
542
|
+
for c in mutual_conflicts:
|
|
543
|
+
flagged[c.field_name] = {
|
|
544
|
+
"local": c.local_value,
|
|
545
|
+
"remote": c.remote_value,
|
|
546
|
+
}
|
|
547
|
+
change.flagged_conflicts = flagged
|
|
548
|
+
change.conflict_type = "both_changed"
|
|
549
|
+
change.has_conflict = True
|
|
550
|
+
|
|
551
|
+
def _detect_and_flag_conflicts(
|
|
552
|
+
self, change: IssueChange, local: Issue | None, remote_state: dict | None
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Detect field-level conflicts when a baseline exists."""
|
|
555
|
+
if local is not None and remote_state is not None:
|
|
556
|
+
conflicts = self._detect_field_conflicts(local, remote_state)
|
|
557
|
+
if conflicts:
|
|
558
|
+
flagged = {}
|
|
559
|
+
for c in conflicts:
|
|
560
|
+
flagged[c.field_name] = {
|
|
561
|
+
"local": c.local_value,
|
|
562
|
+
"remote": c.remote_value,
|
|
563
|
+
}
|
|
564
|
+
change.flagged_conflicts = flagged
|
|
565
|
+
|
|
566
|
+
def _build_issue_change(
|
|
567
|
+
self,
|
|
568
|
+
issue_id: str,
|
|
569
|
+
local: Issue | None,
|
|
570
|
+
remote: dict[str, Any] | None,
|
|
571
|
+
baseline: IssueBaseState | None = None,
|
|
572
|
+
) -> IssueChange:
|
|
573
|
+
"""Construct an IssueChange for a single issue using three-way context.
|
|
574
|
+
|
|
575
|
+
This centralizes the logic of computing local/remote changes, detecting
|
|
576
|
+
field-level conflicts, and setting conflict metadata for the analyzer.
|
|
577
|
+
"""
|
|
578
|
+
return _build_issue_change_helper(
|
|
579
|
+
issue_id,
|
|
580
|
+
local,
|
|
581
|
+
remote,
|
|
582
|
+
baseline,
|
|
583
|
+
resolve_title=self._resolve_issue_title,
|
|
584
|
+
normalize_remote_state=self._normalize_remote_state,
|
|
585
|
+
compute_changes=self._compute_changes,
|
|
586
|
+
compute_changes_remote=self._compute_changes_remote,
|
|
587
|
+
handle_first_sync_semantics=self._handle_first_sync_semantics,
|
|
588
|
+
detect_and_flag_conflicts=self._detect_and_flag_conflicts,
|
|
589
|
+
extract_timestamp=self._extract_timestamp,
|
|
590
|
+
fields_to_sync=self.fields_to_sync,
|
|
591
|
+
logger=self.logger,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def analyze_three_way(
|
|
595
|
+
self,
|
|
596
|
+
local: dict[str, Issue],
|
|
597
|
+
remote: Mapping[str, Any],
|
|
598
|
+
baseline: dict[str, IssueBaseState] | None = None,
|
|
599
|
+
) -> list[IssueChange]:
|
|
600
|
+
"""Analyze changes using three-way merge context.
|
|
601
|
+
|
|
602
|
+
Compares baseline → local and baseline → remote to identify
|
|
603
|
+
what changed in each direction, providing complete conflict context.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
local: Dict of local issues keyed by ID
|
|
607
|
+
remote: Dict of remote SyncIssue objects keyed by ID
|
|
608
|
+
baseline: Dict of baseline states keyed by ID (from last sync)
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of IssueChange objects with three-way context
|
|
612
|
+
|
|
613
|
+
Raises:
|
|
614
|
+
ValueError: If analysis fails
|
|
615
|
+
"""
|
|
616
|
+
self.logger.info(
|
|
617
|
+
"analyze_three_way_start",
|
|
618
|
+
local_count=len(local),
|
|
619
|
+
remote_count=len(remote),
|
|
620
|
+
baseline_count=len(baseline or {}),
|
|
621
|
+
backend_aware=self.backend is not None,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if baseline is None:
|
|
625
|
+
baseline = {}
|
|
626
|
+
|
|
627
|
+
# Normalize remote keys to match local UUIDs (if backend aware)
|
|
628
|
+
local, remote = self._normalize_remote_keys(local, remote)
|
|
629
|
+
|
|
630
|
+
changes = []
|
|
631
|
+
all_issue_ids = set(local.keys()) | set(remote.keys())
|
|
632
|
+
|
|
633
|
+
for issue_id in all_issue_ids:
|
|
634
|
+
try:
|
|
635
|
+
change = self._build_issue_change(
|
|
636
|
+
issue_id,
|
|
637
|
+
local.get(issue_id),
|
|
638
|
+
remote.get(issue_id),
|
|
639
|
+
baseline.get(issue_id) if baseline else None,
|
|
640
|
+
)
|
|
641
|
+
changes.append(change)
|
|
642
|
+
self.logger.debug(
|
|
643
|
+
"three_way_analysis_complete",
|
|
644
|
+
issue_id=issue_id,
|
|
645
|
+
conflict_type=change.conflict_type,
|
|
646
|
+
)
|
|
647
|
+
except Exception as e:
|
|
648
|
+
self.logger.error(
|
|
649
|
+
"three_way_analysis_error",
|
|
650
|
+
issue_id=issue_id,
|
|
651
|
+
error=str(e),
|
|
652
|
+
)
|
|
653
|
+
raise ValueError(
|
|
654
|
+
f"Failed to analyze {issue_id} in three-way merge: {str(e)}"
|
|
655
|
+
) from e
|
|
656
|
+
|
|
657
|
+
self.logger.info("analyze_three_way_complete", change_count=len(changes))
|
|
658
|
+
return changes
|
|
659
|
+
|
|
660
|
+
def _compute_changes(
|
|
661
|
+
self,
|
|
662
|
+
baseline: IssueBaseState | None,
|
|
663
|
+
local: Issue,
|
|
664
|
+
) -> dict[str, Any]:
|
|
665
|
+
"""Compute what changed between baseline and local issue.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
baseline: The baseline state
|
|
669
|
+
local: The current local issue
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Dict of field → value for changed fields
|
|
673
|
+
"""
|
|
674
|
+
return _compute_changes_helper(baseline, local, logger=self.logger)
|
|
675
|
+
|
|
676
|
+
def _compute_changes_remote(
|
|
677
|
+
self,
|
|
678
|
+
baseline: IssueBaseState | None,
|
|
679
|
+
remote: SyncIssue | dict[str, Any],
|
|
680
|
+
) -> dict[str, Any]:
|
|
681
|
+
"""Compute what changed between baseline and remote issue.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
baseline: The baseline state
|
|
685
|
+
remote: The current remote SyncIssue
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
Dict of field → value for changed fields
|
|
689
|
+
"""
|
|
690
|
+
return _compute_changes_remote_helper(baseline, remote, logger=self.logger)
|