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,431 @@
|
|
|
1
|
+
"""Manages persistence of sync state for three-way merge operations.
|
|
2
|
+
|
|
3
|
+
The new SyncState model tracks three snapshots:
|
|
4
|
+
- base_issues: The agreed-upon state from last successful sync
|
|
5
|
+
- local_issues: Current local state (populated before sync)
|
|
6
|
+
- remote_issues: Current remote state (fetched during sync)
|
|
7
|
+
|
|
8
|
+
This manager persists the base_issues snapshot and sync metadata.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from structlog import get_logger
|
|
16
|
+
|
|
17
|
+
from roadmap.core.domain.issue import Issue
|
|
18
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState, SyncState
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SyncStateManager:
|
|
24
|
+
"""Manages loading and saving sync state from git-based baselines."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, roadmap_dir: Path, db_manager=None):
|
|
27
|
+
"""Initialize manager with roadmap directory.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
roadmap_dir: Path to .roadmap directory
|
|
31
|
+
db_manager: Optional DatabaseManager instance for persistent storage
|
|
32
|
+
"""
|
|
33
|
+
self.roadmap_dir = roadmap_dir
|
|
34
|
+
self.db_manager = db_manager
|
|
35
|
+
self.state_file = roadmap_dir / "sync_state.json"
|
|
36
|
+
|
|
37
|
+
def load_sync_state(self) -> SyncState | None:
|
|
38
|
+
"""Load sync state from git-based baseline metadata.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
SyncState if baseline metadata exists and is valid, None otherwise
|
|
42
|
+
"""
|
|
43
|
+
# Git-based baselines are retrieved via SyncRetrievalOrchestrator
|
|
44
|
+
# This method is kept for compatibility but delegates to git history
|
|
45
|
+
logger.debug(
|
|
46
|
+
"sync_state_load_deprecated",
|
|
47
|
+
reason="Use SyncRetrievalOrchestrator.get_baseline_state() instead",
|
|
48
|
+
)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def save_sync_state(self, state: SyncState) -> bool:
|
|
52
|
+
"""Save sync state to git-based baseline metadata.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
state: SyncState to save
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if save was successful
|
|
59
|
+
"""
|
|
60
|
+
# Git-based baselines are managed via SyncRetrievalOrchestrator
|
|
61
|
+
# This method is deprecated in favor of git history + YAML metadata
|
|
62
|
+
logger.debug(
|
|
63
|
+
"sync_state_save_deprecated",
|
|
64
|
+
reason="Use SyncRetrievalOrchestrator baseline management instead",
|
|
65
|
+
)
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def save_sync_state_to_db(self, state: SyncState) -> bool:
|
|
69
|
+
"""Save sync state to database (preferred storage method).
|
|
70
|
+
|
|
71
|
+
Persists the base_issues snapshot and last_sync_time metadata.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
state: SyncState to save
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if save was successful
|
|
78
|
+
"""
|
|
79
|
+
if not self.db_manager:
|
|
80
|
+
logger.debug(
|
|
81
|
+
"sync_state_db_save_skipped",
|
|
82
|
+
reason="no_database_manager",
|
|
83
|
+
)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
import json
|
|
88
|
+
|
|
89
|
+
conn = self.db_manager._get_connection()
|
|
90
|
+
|
|
91
|
+
logger.debug(
|
|
92
|
+
"sync_state_db_save_starting",
|
|
93
|
+
base_issues_count=len(state.base_issues),
|
|
94
|
+
sync_time=state.last_sync_time,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Save metadata - only last_sync_time (no backend in new model)
|
|
98
|
+
if state.last_sync_time:
|
|
99
|
+
conn.execute(
|
|
100
|
+
"INSERT OR REPLACE INTO sync_metadata (key, value, updated_at) VALUES (?, ?, ?)",
|
|
101
|
+
(
|
|
102
|
+
"last_sync",
|
|
103
|
+
state.last_sync_time.isoformat(),
|
|
104
|
+
datetime.now(UTC).isoformat(),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Save each base issue state
|
|
109
|
+
for issue_id, base_state in state.base_issues.items():
|
|
110
|
+
conn.execute(
|
|
111
|
+
"""INSERT OR REPLACE INTO sync_base_state
|
|
112
|
+
(issue_id, status, assignee, title, description, labels, synced_at)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
114
|
+
(
|
|
115
|
+
issue_id,
|
|
116
|
+
base_state.status,
|
|
117
|
+
base_state.assignee,
|
|
118
|
+
base_state.title,
|
|
119
|
+
base_state.description,
|
|
120
|
+
json.dumps(base_state.labels or []),
|
|
121
|
+
datetime.now(UTC).isoformat(),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
conn.commit()
|
|
126
|
+
logger.info(
|
|
127
|
+
"sync_state_saved_to_db",
|
|
128
|
+
base_issues_count=len(state.base_issues),
|
|
129
|
+
)
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(
|
|
134
|
+
"sync_state_db_save_error",
|
|
135
|
+
error=str(e),
|
|
136
|
+
error_type=type(e).__name__,
|
|
137
|
+
severity="system_error",
|
|
138
|
+
)
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def load_sync_state_from_db(self) -> SyncState | None:
|
|
142
|
+
"""Load sync state from database.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
SyncState with base_issues if database has valid state, None otherwise
|
|
146
|
+
"""
|
|
147
|
+
if not self.db_manager:
|
|
148
|
+
logger.debug(
|
|
149
|
+
"sync_state_db_load_skipped",
|
|
150
|
+
reason="no_database_manager",
|
|
151
|
+
)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
conn = self.db_manager._get_connection()
|
|
156
|
+
|
|
157
|
+
# Get metadata
|
|
158
|
+
metadata = {}
|
|
159
|
+
for row in conn.execute("SELECT key, value FROM sync_metadata"):
|
|
160
|
+
metadata[row[0]] = row[1]
|
|
161
|
+
|
|
162
|
+
if not metadata.get("last_sync"):
|
|
163
|
+
logger.debug(
|
|
164
|
+
"sync_state_db_empty",
|
|
165
|
+
reason="no_last_sync_metadata",
|
|
166
|
+
)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
last_sync_time = datetime.fromisoformat(metadata["last_sync"])
|
|
170
|
+
|
|
171
|
+
# Create new SyncState with metadata
|
|
172
|
+
state = SyncState(
|
|
173
|
+
last_sync_time=last_sync_time,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Load base states from database
|
|
177
|
+
try:
|
|
178
|
+
for row in conn.execute(
|
|
179
|
+
"SELECT issue_id, status, assignee, title, description, labels FROM sync_base_state"
|
|
180
|
+
):
|
|
181
|
+
issue_id, status, assignee, title, description, labels_json = row
|
|
182
|
+
base_state = IssueBaseState(
|
|
183
|
+
id=issue_id,
|
|
184
|
+
status=status,
|
|
185
|
+
title=title or "",
|
|
186
|
+
assignee=assignee,
|
|
187
|
+
description=description or "",
|
|
188
|
+
labels=json.loads(labels_json) if labels_json else [],
|
|
189
|
+
)
|
|
190
|
+
state.base_issues[issue_id] = base_state
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(
|
|
193
|
+
"sync_state_db_load_issues_failed",
|
|
194
|
+
error=str(e),
|
|
195
|
+
note="metadata_loaded_but_issues_skipped",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
logger.info(
|
|
199
|
+
"sync_state_loaded_from_db",
|
|
200
|
+
base_issues_count=len(state.base_issues),
|
|
201
|
+
)
|
|
202
|
+
return state
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(
|
|
206
|
+
"sync_state_db_load_error",
|
|
207
|
+
error=str(e),
|
|
208
|
+
error_type=type(e).__name__,
|
|
209
|
+
severity="system_error",
|
|
210
|
+
)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def create_base_state_from_issue(self, issue: Issue) -> IssueBaseState:
|
|
214
|
+
"""Create a base state snapshot from an Issue object.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
issue: Issue object to snapshot
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
IssueBaseState representing the current state of the issue
|
|
221
|
+
"""
|
|
222
|
+
# Extract status value properly - handle enum types
|
|
223
|
+
status_value = issue.status
|
|
224
|
+
if hasattr(status_value, "value"):
|
|
225
|
+
# It's an enum, use the value
|
|
226
|
+
status_value = status_value.value
|
|
227
|
+
elif status_value is None:
|
|
228
|
+
status_value = "unknown"
|
|
229
|
+
else:
|
|
230
|
+
status_value = str(status_value)
|
|
231
|
+
|
|
232
|
+
return IssueBaseState(
|
|
233
|
+
id=issue.id,
|
|
234
|
+
status=status_value,
|
|
235
|
+
title=issue.title,
|
|
236
|
+
assignee=issue.assignee,
|
|
237
|
+
description=issue.content or "",
|
|
238
|
+
labels=issue.labels or [],
|
|
239
|
+
updated_at=datetime.now(UTC),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def save_base_state(self, issue: Issue, remote_version: bool = False) -> bool:
|
|
243
|
+
"""Update the base state for a single issue after successful sync.
|
|
244
|
+
|
|
245
|
+
This method is called after successfully syncing an issue to update
|
|
246
|
+
the persisted state so it won't be re-synced next time.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
issue: Issue object that was synced
|
|
250
|
+
remote_version: If True, marks this as the remote version (post-push)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
True if update was successful
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
logger.debug(
|
|
257
|
+
"base_state_update_starting",
|
|
258
|
+
issue_id=issue.id,
|
|
259
|
+
issue_title=issue.title[:50] if issue.title else "",
|
|
260
|
+
remote_version=remote_version,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Load current sync state or create new one
|
|
264
|
+
state = self.load_sync_state_from_db()
|
|
265
|
+
if state is None:
|
|
266
|
+
logger.info(
|
|
267
|
+
"creating_new_sync_state_for_issue",
|
|
268
|
+
issue_id=issue.id,
|
|
269
|
+
reason="no_existing_state",
|
|
270
|
+
)
|
|
271
|
+
state = SyncState(
|
|
272
|
+
last_sync_time=datetime.now(UTC),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Create new base state for this issue
|
|
276
|
+
base_state = self.create_base_state_from_issue(issue)
|
|
277
|
+
state.base_issues[issue.id] = base_state
|
|
278
|
+
state.last_sync_time = datetime.now(UTC)
|
|
279
|
+
|
|
280
|
+
# Save the updated state to database
|
|
281
|
+
success = self.save_sync_state_to_db(state)
|
|
282
|
+
if success:
|
|
283
|
+
logger.info(
|
|
284
|
+
"base_state_updated_successfully",
|
|
285
|
+
issue_id=issue.id,
|
|
286
|
+
remote_version=remote_version,
|
|
287
|
+
status=base_state.status,
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
logger.warning(
|
|
291
|
+
"base_state_update_save_failed",
|
|
292
|
+
issue_id=issue.id,
|
|
293
|
+
severity="operational",
|
|
294
|
+
)
|
|
295
|
+
return success
|
|
296
|
+
except AttributeError as e:
|
|
297
|
+
logger.error(
|
|
298
|
+
"base_state_update_attribute_error",
|
|
299
|
+
issue_id=issue.id if hasattr(issue, "id") else "unknown",
|
|
300
|
+
error=str(e),
|
|
301
|
+
error_type="AttributeError",
|
|
302
|
+
severity="system_error",
|
|
303
|
+
)
|
|
304
|
+
return False
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(
|
|
307
|
+
"base_state_update_failed",
|
|
308
|
+
issue_id=issue.id if hasattr(issue, "id") else "unknown",
|
|
309
|
+
error=str(e),
|
|
310
|
+
error_type=type(e).__name__,
|
|
311
|
+
severity="system_error",
|
|
312
|
+
)
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def create_sync_state_from_issues(
|
|
316
|
+
self,
|
|
317
|
+
issues: list[Issue],
|
|
318
|
+
backend: str = "github",
|
|
319
|
+
) -> SyncState:
|
|
320
|
+
"""Create a complete sync state from a list of issues.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
issues: List of Issue objects
|
|
324
|
+
backend: Backend type (used for logging only, not stored in new model)
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
SyncState with base states for all issues
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
logger.debug(
|
|
331
|
+
"creating_sync_state_from_issues",
|
|
332
|
+
issues_count=len(issues),
|
|
333
|
+
backend=backend,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
state = SyncState(
|
|
337
|
+
last_sync_time=datetime.now(UTC),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
for issue in issues:
|
|
341
|
+
try:
|
|
342
|
+
base_state = self.create_base_state_from_issue(issue)
|
|
343
|
+
state.base_issues[issue.id] = base_state
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.warning(
|
|
346
|
+
"skipping_issue_in_state_creation",
|
|
347
|
+
issue_id=issue.id if hasattr(issue, "id") else "unknown",
|
|
348
|
+
error=str(e),
|
|
349
|
+
severity="operational",
|
|
350
|
+
)
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
logger.info(
|
|
354
|
+
"sync_state_created_successfully",
|
|
355
|
+
issues_count=len(issues),
|
|
356
|
+
backend=backend,
|
|
357
|
+
issues_in_state=len(state.base_issues),
|
|
358
|
+
)
|
|
359
|
+
return state
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(
|
|
362
|
+
"sync_state_creation_failed",
|
|
363
|
+
issues_count=len(issues) if isinstance(issues, list) else "unknown",
|
|
364
|
+
backend=backend,
|
|
365
|
+
error=str(e),
|
|
366
|
+
error_type=type(e).__name__,
|
|
367
|
+
severity="system_error",
|
|
368
|
+
)
|
|
369
|
+
# Return empty state as fallback
|
|
370
|
+
return SyncState(
|
|
371
|
+
last_sync_time=datetime.now(UTC),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def migrate_json_to_db(self) -> bool:
|
|
375
|
+
"""Migrate sync state from JSON file to database (legacy operation).
|
|
376
|
+
|
|
377
|
+
One-time operation for existing installations to move from file-based
|
|
378
|
+
to database-based sync state storage. With the new model, this operation
|
|
379
|
+
should not be needed as state is created fresh from issues.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if migration was successful or not needed
|
|
383
|
+
"""
|
|
384
|
+
if not self.db_manager:
|
|
385
|
+
logger.debug(
|
|
386
|
+
"sync_state_migration_skipped",
|
|
387
|
+
reason="no_database_manager",
|
|
388
|
+
)
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
# Check if JSON file exists
|
|
392
|
+
if not self.state_file.exists():
|
|
393
|
+
logger.debug(
|
|
394
|
+
"sync_state_migration_not_needed",
|
|
395
|
+
reason="no_legacy_json_file",
|
|
396
|
+
)
|
|
397
|
+
return True
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
logger.info("sync_state_migration_starting", source="json_file")
|
|
401
|
+
logger.warning(
|
|
402
|
+
"sync_state_migration_deprecated",
|
|
403
|
+
note="new_model_creates_fresh_state_from_issues",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Archive the JSON file
|
|
407
|
+
try:
|
|
408
|
+
archive_path = self.state_file.with_suffix(".json.backup")
|
|
409
|
+
self.state_file.rename(archive_path)
|
|
410
|
+
logger.info(
|
|
411
|
+
"sync_state_legacy_json_archived",
|
|
412
|
+
archived_to=str(archive_path),
|
|
413
|
+
)
|
|
414
|
+
return True
|
|
415
|
+
except OSError as e:
|
|
416
|
+
logger.warning(
|
|
417
|
+
"sync_state_migration_archive_failed",
|
|
418
|
+
error=str(e),
|
|
419
|
+
note="json_file_remains",
|
|
420
|
+
severity="operational",
|
|
421
|
+
)
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(
|
|
426
|
+
"sync_state_migration_error",
|
|
427
|
+
error=str(e),
|
|
428
|
+
error_type=type(e).__name__,
|
|
429
|
+
severity="system_error",
|
|
430
|
+
)
|
|
431
|
+
return False
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Helpers for normalizing remote sync state and extracting timestamps.
|
|
2
|
+
|
|
3
|
+
This module contains pure helpers extracted from SyncStateComparator to
|
|
4
|
+
encapsulate timestamp parsing and remote object normalization so they can be
|
|
5
|
+
tested and reused independently.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_timestamp(
|
|
19
|
+
data: dict[str, Any] | object,
|
|
20
|
+
timestamp_field: str = "updated_at",
|
|
21
|
+
logger: Any | None = None,
|
|
22
|
+
) -> datetime | None:
|
|
23
|
+
"""Extract and parse a timestamp from remote issue data.
|
|
24
|
+
|
|
25
|
+
Mirrors the behavior previously implemented on SyncStateComparator.
|
|
26
|
+
Accepts an optional `logger` to preserve debug logging when available.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
ts = data.get(timestamp_field)
|
|
31
|
+
else:
|
|
32
|
+
ts = getattr(data, timestamp_field, None)
|
|
33
|
+
|
|
34
|
+
if ts is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
if isinstance(ts, datetime):
|
|
38
|
+
return ts
|
|
39
|
+
|
|
40
|
+
if isinstance(ts, str):
|
|
41
|
+
if ts.endswith("Z"):
|
|
42
|
+
ts = ts[:-1] + "+00:00"
|
|
43
|
+
return datetime.fromisoformat(ts)
|
|
44
|
+
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
except Exception as e: # pragma: no cover - defensive logging
|
|
48
|
+
if logger is not None:
|
|
49
|
+
try:
|
|
50
|
+
logger.debug(
|
|
51
|
+
"timestamp_extraction_error",
|
|
52
|
+
field=timestamp_field,
|
|
53
|
+
error=str(e),
|
|
54
|
+
)
|
|
55
|
+
except Exception as logging_error:
|
|
56
|
+
logger.error(
|
|
57
|
+
"logger_failed",
|
|
58
|
+
operation="log_timestamp_extraction_error",
|
|
59
|
+
error=str(logging_error),
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def normalize_remote_state(
|
|
65
|
+
remote: Any, logger: Any | None = None
|
|
66
|
+
) -> dict[str, Any] | None:
|
|
67
|
+
"""Normalize remote representations into a plain dict for comparison.
|
|
68
|
+
|
|
69
|
+
Accepts either a dict-like remote or an object. Returns a dict or None.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
if isinstance(remote, dict) or remote is None:
|
|
73
|
+
return remote
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"id": getattr(remote, "id", None),
|
|
77
|
+
"backend_id": getattr(remote, "backend_id", None),
|
|
78
|
+
"title": getattr(remote, "title", None),
|
|
79
|
+
"status": getattr(remote, "status", None),
|
|
80
|
+
"assignee": getattr(remote, "assignee", None),
|
|
81
|
+
"milestone": getattr(remote, "milestone", None),
|
|
82
|
+
"description": getattr(remote, "headline", None)
|
|
83
|
+
or getattr(remote, "description", None),
|
|
84
|
+
"labels": list(getattr(remote, "labels", []) or []),
|
|
85
|
+
"updated_at": getattr(remote, "updated_at", None)
|
|
86
|
+
or getattr(remote, "updated", None),
|
|
87
|
+
}
|
|
88
|
+
except Exception as e: # pragma: no cover - defensive logging
|
|
89
|
+
if logger is not None:
|
|
90
|
+
try:
|
|
91
|
+
logger.debug("normalize_remote_state_error", error=str(e))
|
|
92
|
+
except Exception as logging_error:
|
|
93
|
+
logger.error(
|
|
94
|
+
"logger_failed",
|
|
95
|
+
operation="log_normalize_remote_state_error",
|
|
96
|
+
error=str(logging_error),
|
|
97
|
+
)
|
|
98
|
+
return None
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Three-way merge helpers for building IssueChange objects.
|
|
2
|
+
|
|
3
|
+
This module provides a single helper that constructs an `IssueChange` using
|
|
4
|
+
callable dependencies passed in from `SyncStateComparator`. Keeping the
|
|
5
|
+
implementation here makes it easier to unit-test the three-way logic in
|
|
6
|
+
isolation while the comparator remains a thin facade.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from roadmap.core.services.sync.sync_report import IssueChange
|
|
15
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_issue_change(
|
|
19
|
+
issue_id: str,
|
|
20
|
+
local: Any | None,
|
|
21
|
+
remote: Any | None,
|
|
22
|
+
baseline: IssueBaseState | None,
|
|
23
|
+
*,
|
|
24
|
+
resolve_title: Callable[[str, Any, Any, Any], str],
|
|
25
|
+
normalize_remote_state: Callable[[Any], dict | None],
|
|
26
|
+
compute_changes: Callable[[Any, Any], dict],
|
|
27
|
+
compute_changes_remote: Callable[[Any, Any], dict],
|
|
28
|
+
handle_first_sync_semantics: Callable[[IssueChange, Any, Any, Any], None],
|
|
29
|
+
detect_and_flag_conflicts: Callable[[IssueChange, Any, Any], None],
|
|
30
|
+
extract_timestamp: Callable[[Any, str], Any],
|
|
31
|
+
fields_to_sync: list[str],
|
|
32
|
+
logger: Any | None = None,
|
|
33
|
+
) -> IssueChange:
|
|
34
|
+
"""Construct an IssueChange for a single issue using three-way context.
|
|
35
|
+
|
|
36
|
+
Most of the behavior is the same as the old `_build_issue_change` on
|
|
37
|
+
`SyncStateComparator` but extracted to allow focused testing. The helper
|
|
38
|
+
depends on several callables passed in from the comparator to avoid
|
|
39
|
+
duplicating logic or breaking encapsulation.
|
|
40
|
+
"""
|
|
41
|
+
title = resolve_title(issue_id, local, remote, baseline)
|
|
42
|
+
change = IssueChange(issue_id=issue_id, title=title)
|
|
43
|
+
change.baseline_state = baseline
|
|
44
|
+
change.local_state = local
|
|
45
|
+
|
|
46
|
+
remote_state = normalize_remote_state(remote)
|
|
47
|
+
change.remote_state = remote_state
|
|
48
|
+
|
|
49
|
+
change.local_changes = compute_changes(baseline, local) if local is not None else {}
|
|
50
|
+
change.remote_changes = (
|
|
51
|
+
compute_changes_remote(baseline, remote_state)
|
|
52
|
+
if remote_state is not None
|
|
53
|
+
else {}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if baseline is None:
|
|
57
|
+
handle_first_sync_semantics(change, local, remote_state, baseline)
|
|
58
|
+
else:
|
|
59
|
+
detect_and_flag_conflicts(change, local, remote_state)
|
|
60
|
+
|
|
61
|
+
if change.local_changes and change.remote_changes:
|
|
62
|
+
change.conflict_type = "both_changed"
|
|
63
|
+
change.has_conflict = True
|
|
64
|
+
elif change.local_changes:
|
|
65
|
+
change.conflict_type = "local_only"
|
|
66
|
+
elif change.remote_changes:
|
|
67
|
+
change.conflict_type = "remote_only"
|
|
68
|
+
else:
|
|
69
|
+
change.conflict_type = "no_change"
|
|
70
|
+
|
|
71
|
+
last_sync = None
|
|
72
|
+
if baseline is not None and getattr(baseline, "updated_at", None):
|
|
73
|
+
last_sync = baseline.updated_at
|
|
74
|
+
elif remote is not None:
|
|
75
|
+
last_sync = extract_timestamp(remote, "updated_at")
|
|
76
|
+
change.last_sync_time = last_sync
|
|
77
|
+
|
|
78
|
+
return change
|