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,810 @@
|
|
|
1
|
+
"""Enhanced sync orchestrator that uses git-based baselines.
|
|
2
|
+
|
|
3
|
+
This module extends SyncMergeOrchestrator with intelligent baseline management
|
|
4
|
+
using git history and YAML sync_metadata instead of database state tables.
|
|
5
|
+
|
|
6
|
+
Enforces explicit baseline creation during first sync via interactive selection.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from structlog import get_logger
|
|
14
|
+
|
|
15
|
+
from roadmap.adapters.persistence.parser.issue import IssueParser
|
|
16
|
+
from roadmap.adapters.sync.sync_merge_orchestrator import SyncMergeOrchestrator
|
|
17
|
+
from roadmap.core.models.sync_models import SyncIssue
|
|
18
|
+
from roadmap.core.services.baseline.baseline_selector import (
|
|
19
|
+
BaselineStrategy,
|
|
20
|
+
InteractiveBaselineSelector,
|
|
21
|
+
)
|
|
22
|
+
from roadmap.core.services.baseline.baseline_state_retriever import (
|
|
23
|
+
BaselineStateRetriever,
|
|
24
|
+
)
|
|
25
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState, SyncState
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SyncRetrievalOrchestrator(SyncMergeOrchestrator):
|
|
31
|
+
"""Sync orchestrator using git history and YAML-based baseline retrieval.
|
|
32
|
+
|
|
33
|
+
This enhances the base SyncMergeOrchestrator with:
|
|
34
|
+
1. Git history-based local baseline retrieval (no DB needed)
|
|
35
|
+
2. YAML sync_metadata for remote baseline snapshots
|
|
36
|
+
3. Intelligent baseline-less diff when no previous sync exists
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *args, persistence=None, **kwargs):
|
|
40
|
+
"""Initialize with baseline retriever.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
*args: Positional arguments passed to parent class
|
|
44
|
+
persistence: PersistenceInterface for git operations (optional)
|
|
45
|
+
**kwargs: Keyword arguments passed to parent class
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
# Use provided interfaces or create simple implementations
|
|
50
|
+
if persistence is None:
|
|
51
|
+
from roadmap.adapters.persistence.git_history import (
|
|
52
|
+
get_file_at_timestamp,
|
|
53
|
+
)
|
|
54
|
+
from roadmap.core.interfaces.persistence import PersistenceInterface
|
|
55
|
+
|
|
56
|
+
class SimplePersistence(PersistenceInterface):
|
|
57
|
+
def get_file_at_timestamp(self, file_path, timestamp):
|
|
58
|
+
return get_file_at_timestamp(file_path, timestamp)
|
|
59
|
+
|
|
60
|
+
def list_files_in_directory(self, directory):
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
target_dir = Path(directory)
|
|
64
|
+
if not target_dir.exists():
|
|
65
|
+
return []
|
|
66
|
+
return [
|
|
67
|
+
f.name
|
|
68
|
+
for f in target_dir.iterdir()
|
|
69
|
+
if f.is_file() and f.suffix == ".md"
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
persistence = SimplePersistence()
|
|
73
|
+
|
|
74
|
+
self.baseline_retriever = BaselineStateRetriever(
|
|
75
|
+
self.core.issues_dir, persistence
|
|
76
|
+
)
|
|
77
|
+
self.sync_metadata_cache: dict[str, Any] = {}
|
|
78
|
+
self.baseline_selector = InteractiveBaselineSelector()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def issues_dir(self) -> Path:
|
|
82
|
+
"""Get issues directory from core."""
|
|
83
|
+
# Navigate from roadmap_dir to issues directory
|
|
84
|
+
roadmap_dir = self.core.roadmap_dir
|
|
85
|
+
return roadmap_dir / "issues"
|
|
86
|
+
|
|
87
|
+
def has_baseline(self) -> bool:
|
|
88
|
+
"""Check if a baseline has been established.
|
|
89
|
+
|
|
90
|
+
A baseline exists if:
|
|
91
|
+
1. Baseline exists in database (Phase 3), OR
|
|
92
|
+
2. Previous sync state can be loaded (legacy), OR
|
|
93
|
+
3. Sync metadata with remote_state exists in issue files (legacy)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if baseline exists, False if first sync
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
# PHASE 3: Check database baseline first (fast)
|
|
100
|
+
try:
|
|
101
|
+
db_baseline = self.core.db.get_sync_baseline()
|
|
102
|
+
if db_baseline:
|
|
103
|
+
logger.debug("baseline_exists_in_database")
|
|
104
|
+
return True
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.debug("database_baseline_check_failed", error=str(e))
|
|
107
|
+
|
|
108
|
+
# Check if sync state can be loaded (legacy)
|
|
109
|
+
base_state = self.state_manager.load_sync_state()
|
|
110
|
+
if base_state and base_state.base_issues:
|
|
111
|
+
logger.debug("baseline_exists_from_sync_state")
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
# Check if sync metadata exists in any issue (including archived)
|
|
115
|
+
try:
|
|
116
|
+
local_issues = self.core.issues.list_all_including_archived()
|
|
117
|
+
for issue in local_issues:
|
|
118
|
+
issue_file = self._find_issue_file(issue.id)
|
|
119
|
+
if not issue_file:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
metadata = IssueParser.load_sync_metadata(issue_file)
|
|
123
|
+
if metadata and "last_synced" in metadata:
|
|
124
|
+
logger.debug("baseline_exists_from_sync_metadata")
|
|
125
|
+
return True
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.debug("metadata_check_failed", error=str(e))
|
|
128
|
+
|
|
129
|
+
logger.debug("no_baseline_found_first_sync")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.warning(
|
|
134
|
+
"baseline_check_failed", error=str(e), severity="operational"
|
|
135
|
+
)
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def ensure_baseline(
|
|
139
|
+
self,
|
|
140
|
+
strategy: BaselineStrategy | None = None,
|
|
141
|
+
interactive: bool = True,
|
|
142
|
+
) -> bool:
|
|
143
|
+
"""Ensure a baseline exists, creating one if necessary.
|
|
144
|
+
|
|
145
|
+
For first sync, requires explicit baseline strategy selection.
|
|
146
|
+
Strategies:
|
|
147
|
+
- LOCAL: Use local code as baseline
|
|
148
|
+
- REMOTE: Use remote as baseline
|
|
149
|
+
- INTERACTIVE: Choose per-issue
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
strategy: Override strategy (LOCAL, REMOTE, or INTERACTIVE)
|
|
153
|
+
interactive: If False, raises error instead of prompting
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if baseline exists or was created successfully
|
|
157
|
+
False if baseline creation failed
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
RuntimeError: If baseline missing and interactive=False
|
|
161
|
+
"""
|
|
162
|
+
if self.has_baseline():
|
|
163
|
+
logger.info("baseline_already_exists")
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
logger.info("baseline_required_first_sync")
|
|
167
|
+
|
|
168
|
+
# Get strategy if not provided
|
|
169
|
+
if strategy is None:
|
|
170
|
+
if not interactive:
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
"Baseline required but no strategy provided and interactive=False"
|
|
173
|
+
)
|
|
174
|
+
result = self.baseline_selector.select_baseline()
|
|
175
|
+
strategy = result.strategy
|
|
176
|
+
else:
|
|
177
|
+
logger.info("baseline_strategy_provided", strategy=strategy.value)
|
|
178
|
+
|
|
179
|
+
# Create baseline based on strategy
|
|
180
|
+
try:
|
|
181
|
+
if strategy == BaselineStrategy.LOCAL:
|
|
182
|
+
return self._create_baseline_from_local()
|
|
183
|
+
elif strategy == BaselineStrategy.REMOTE:
|
|
184
|
+
return self._create_baseline_from_remote()
|
|
185
|
+
else:
|
|
186
|
+
# INTERACTIVE - would need per-issue prompts
|
|
187
|
+
logger.warning(
|
|
188
|
+
"interactive_baseline_not_fully_implemented_yet",
|
|
189
|
+
severity="operational",
|
|
190
|
+
)
|
|
191
|
+
return self._create_baseline_from_local()
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(
|
|
195
|
+
"baseline_creation_failed", error=str(e), severity="system_error"
|
|
196
|
+
)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
def _create_baseline_from_local(self) -> bool:
|
|
200
|
+
"""Create baseline from local state.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if successful
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
logger.info("creating_baseline_from_local")
|
|
207
|
+
baseline = self._create_initial_baseline()
|
|
208
|
+
if baseline and baseline.base_issues:
|
|
209
|
+
# Save baseline to database for fast retrieval
|
|
210
|
+
baseline_dict = {
|
|
211
|
+
issue_id: {
|
|
212
|
+
"status": state.status,
|
|
213
|
+
"assignee": state.assignee,
|
|
214
|
+
"description": state.description or "",
|
|
215
|
+
"labels": state.labels or [],
|
|
216
|
+
}
|
|
217
|
+
for issue_id, state in baseline.base_issues.items()
|
|
218
|
+
}
|
|
219
|
+
self.core.db.save_sync_baseline(baseline_dict)
|
|
220
|
+
|
|
221
|
+
logger.info(
|
|
222
|
+
"baseline_created_from_local",
|
|
223
|
+
issue_count=len(baseline.base_issues),
|
|
224
|
+
)
|
|
225
|
+
return True
|
|
226
|
+
else:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"baseline_creation_produced_empty_state", severity="operational"
|
|
229
|
+
)
|
|
230
|
+
return False
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(
|
|
233
|
+
"baseline_creation_from_local_failed",
|
|
234
|
+
error=str(e),
|
|
235
|
+
severity="system_error",
|
|
236
|
+
)
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def _create_baseline_from_remote(self) -> bool:
|
|
240
|
+
"""Create baseline from remote state.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if successful
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
logger.info("creating_baseline_from_remote")
|
|
247
|
+
|
|
248
|
+
if not self._authenticate_for_baseline():
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
remote_issues = self._fetch_remote_issues_for_baseline()
|
|
252
|
+
if not remote_issues:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
baseline = self._build_baseline_from_remote_issues(remote_issues)
|
|
256
|
+
if baseline.base_issues:
|
|
257
|
+
baseline_dict = {
|
|
258
|
+
issue_id: {
|
|
259
|
+
"status": state.status,
|
|
260
|
+
"assignee": state.assignee,
|
|
261
|
+
"description": state.headline or "",
|
|
262
|
+
"labels": state.labels or [],
|
|
263
|
+
}
|
|
264
|
+
for issue_id, state in baseline.base_issues.items()
|
|
265
|
+
}
|
|
266
|
+
self.core.db.save_sync_baseline(baseline_dict)
|
|
267
|
+
logger.info(
|
|
268
|
+
"baseline_created_from_remote",
|
|
269
|
+
issue_count=len(baseline.base_issues),
|
|
270
|
+
)
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
logger.warning(
|
|
274
|
+
"baseline_creation_produced_empty_state",
|
|
275
|
+
severity="operational",
|
|
276
|
+
)
|
|
277
|
+
return False
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(
|
|
280
|
+
"baseline_creation_from_remote_failed",
|
|
281
|
+
error=str(e),
|
|
282
|
+
severity="system_error",
|
|
283
|
+
)
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def _authenticate_for_baseline(self) -> bool:
|
|
287
|
+
auth_result = self.backend.authenticate()
|
|
288
|
+
if hasattr(auth_result, "is_err") and callable(auth_result.is_err):
|
|
289
|
+
if auth_result.is_err():
|
|
290
|
+
logger.error(
|
|
291
|
+
"backend_auth_failed_for_baseline",
|
|
292
|
+
error=str(auth_result.unwrap_err()),
|
|
293
|
+
severity="infrastructure",
|
|
294
|
+
)
|
|
295
|
+
return False
|
|
296
|
+
if not auth_result.unwrap():
|
|
297
|
+
logger.error(
|
|
298
|
+
"backend_auth_failed_for_baseline", severity="infrastructure"
|
|
299
|
+
)
|
|
300
|
+
return False
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
if not auth_result:
|
|
304
|
+
logger.error("backend_auth_failed_for_baseline", severity="infrastructure")
|
|
305
|
+
return False
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
def _fetch_remote_issues_for_baseline(self) -> dict[str, SyncIssue] | None:
|
|
309
|
+
remote_issues_result = self.backend.get_issues()
|
|
310
|
+
if hasattr(remote_issues_result, "is_err") and callable(
|
|
311
|
+
remote_issues_result.is_err
|
|
312
|
+
):
|
|
313
|
+
if remote_issues_result.is_err():
|
|
314
|
+
logger.warning(
|
|
315
|
+
"backend_get_issues_failed",
|
|
316
|
+
error=str(remote_issues_result.unwrap_err()),
|
|
317
|
+
severity="operational",
|
|
318
|
+
)
|
|
319
|
+
return None
|
|
320
|
+
remote_issues = remote_issues_result.unwrap() or {}
|
|
321
|
+
elif isinstance(remote_issues_result, dict):
|
|
322
|
+
remote_issues = remote_issues_result
|
|
323
|
+
else:
|
|
324
|
+
logger.warning(
|
|
325
|
+
"backend_get_issues_unexpected_type",
|
|
326
|
+
result_type=type(remote_issues_result).__name__,
|
|
327
|
+
severity="operational",
|
|
328
|
+
)
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
if not remote_issues:
|
|
332
|
+
logger.warning("no_remote_issues_for_baseline", severity="operational")
|
|
333
|
+
return None
|
|
334
|
+
return remote_issues
|
|
335
|
+
|
|
336
|
+
def _build_baseline_from_remote_issues(
|
|
337
|
+
self, remote_issues: dict[str, SyncIssue]
|
|
338
|
+
) -> SyncState:
|
|
339
|
+
baseline = SyncState(last_sync_time=datetime.now(UTC))
|
|
340
|
+
|
|
341
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState
|
|
342
|
+
|
|
343
|
+
for issue_id, remote_issue in remote_issues.items():
|
|
344
|
+
try:
|
|
345
|
+
baseline_state = IssueBaseState(
|
|
346
|
+
id=issue_id,
|
|
347
|
+
status=remote_issue.status or "todo",
|
|
348
|
+
title=remote_issue.title or "Untitled",
|
|
349
|
+
assignee=remote_issue.assignee,
|
|
350
|
+
headline=remote_issue.headline or "",
|
|
351
|
+
labels=remote_issue.labels or [],
|
|
352
|
+
updated_at=datetime.now(UTC),
|
|
353
|
+
)
|
|
354
|
+
baseline.base_issues[issue_id] = baseline_state
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.warning(
|
|
357
|
+
"baseline_issue_conversion_failed",
|
|
358
|
+
issue_id=issue_id,
|
|
359
|
+
error=str(e),
|
|
360
|
+
severity="operational",
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
return baseline
|
|
365
|
+
|
|
366
|
+
def _build_baseline_state_from_git_history(
|
|
367
|
+
self, last_synced: datetime | None
|
|
368
|
+
) -> SyncState | None:
|
|
369
|
+
"""Build baseline state by querying git history for each issue.
|
|
370
|
+
|
|
371
|
+
Instead of loading from a sync_base_state database table, we reconstruct
|
|
372
|
+
the baseline by reading each issue file as it existed at the last_synced
|
|
373
|
+
timestamp.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
last_synced: Timestamp of last sync (from sync_metadata)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
SyncState with issues reconstructed from git history, or None
|
|
380
|
+
"""
|
|
381
|
+
if not last_synced:
|
|
382
|
+
logger.debug(
|
|
383
|
+
"baseline_no_last_synced",
|
|
384
|
+
reason="first_sync_or_timestamp_missing",
|
|
385
|
+
)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
logger.info(
|
|
390
|
+
"baseline_state_reconstruction_start",
|
|
391
|
+
last_synced=last_synced.isoformat(),
|
|
392
|
+
reason="building_from_git_history",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
baseline = SyncState(
|
|
396
|
+
last_sync_time=last_synced,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Get all current issue files (including archived)
|
|
400
|
+
try:
|
|
401
|
+
local_issues = self.core.issues.list_all_including_archived()
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.warning(
|
|
404
|
+
"baseline_reconstruction_issue_list_failed",
|
|
405
|
+
error=str(e),
|
|
406
|
+
severity="operational",
|
|
407
|
+
)
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
for issue in local_issues:
|
|
411
|
+
try:
|
|
412
|
+
# Find the issue file
|
|
413
|
+
issue_file = self._find_issue_file(issue.id)
|
|
414
|
+
if not issue_file:
|
|
415
|
+
logger.debug(
|
|
416
|
+
"baseline_issue_file_not_found",
|
|
417
|
+
issue_id=issue.id,
|
|
418
|
+
)
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
# Retrieve local baseline from git history
|
|
422
|
+
local_baseline = self.baseline_retriever.get_local_baseline(
|
|
423
|
+
issue_file, last_synced
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if local_baseline:
|
|
427
|
+
baseline.base_issues[issue.id] = local_baseline
|
|
428
|
+
logger.debug(
|
|
429
|
+
"baseline_issue_reconstructed_from_git",
|
|
430
|
+
issue_id=issue.id,
|
|
431
|
+
status=local_baseline.status,
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
logger.debug(
|
|
435
|
+
"baseline_issue_not_in_git_history",
|
|
436
|
+
issue_id=issue.id,
|
|
437
|
+
reason="file_did_not_exist_at_timestamp",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.warning(
|
|
442
|
+
"baseline_reconstruction_issue_failed",
|
|
443
|
+
issue_id=issue.id,
|
|
444
|
+
error=str(e),
|
|
445
|
+
severity="operational",
|
|
446
|
+
)
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
logger.info(
|
|
450
|
+
"baseline_state_reconstruction_complete",
|
|
451
|
+
reconstructed_count=len(baseline.base_issues),
|
|
452
|
+
reason="using_git_history",
|
|
453
|
+
)
|
|
454
|
+
return baseline
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(
|
|
458
|
+
"baseline_state_reconstruction_error",
|
|
459
|
+
error=str(e),
|
|
460
|
+
error_type=type(e).__name__,
|
|
461
|
+
severity="system_error",
|
|
462
|
+
)
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
def _build_baseline_state_from_sync_metadata(self) -> SyncState | None:
|
|
466
|
+
"""Build baseline state from sync_metadata YAML frontmatter.
|
|
467
|
+
|
|
468
|
+
The remote baseline is stored in each issue's sync_metadata.remote_state,
|
|
469
|
+
which represents the last-synced state from the remote backend.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
None (uses all local issues)
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
SyncState with remote baselines from sync_metadata
|
|
476
|
+
"""
|
|
477
|
+
try:
|
|
478
|
+
logger.debug(
|
|
479
|
+
"baseline_remote_retrieval_start",
|
|
480
|
+
reason="reading_sync_metadata_yaml",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Get last_synced from first issue that has it (including archived)
|
|
484
|
+
last_synced_time = None
|
|
485
|
+
try:
|
|
486
|
+
local_issues = self.core.issues.list_all_including_archived()
|
|
487
|
+
for issue in local_issues:
|
|
488
|
+
issue_file = self._find_issue_file(issue.id)
|
|
489
|
+
if not issue_file:
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
metadata = IssueParser.load_sync_metadata(issue_file)
|
|
493
|
+
if metadata and "last_synced" in metadata:
|
|
494
|
+
if isinstance(metadata["last_synced"], str):
|
|
495
|
+
last_synced_time = datetime.fromisoformat(
|
|
496
|
+
metadata["last_synced"]
|
|
497
|
+
)
|
|
498
|
+
else:
|
|
499
|
+
last_synced_time = metadata["last_synced"]
|
|
500
|
+
break
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.warning(
|
|
503
|
+
"baseline_last_synced_extraction_failed",
|
|
504
|
+
error=str(e),
|
|
505
|
+
severity="operational",
|
|
506
|
+
)
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
if not last_synced_time:
|
|
510
|
+
logger.debug(
|
|
511
|
+
"baseline_no_last_synced_in_metadata",
|
|
512
|
+
reason="no_sync_metadata_found",
|
|
513
|
+
)
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
baseline = SyncState(
|
|
517
|
+
last_sync_time=last_synced_time,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Build from sync_metadata remote_state
|
|
521
|
+
for issue in local_issues:
|
|
522
|
+
try:
|
|
523
|
+
issue_file = self._find_issue_file(issue.id)
|
|
524
|
+
if not issue_file:
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
remote_baseline = self.baseline_retriever.get_remote_baseline(
|
|
528
|
+
issue_file
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if remote_baseline:
|
|
532
|
+
baseline.base_issues[issue.id] = remote_baseline
|
|
533
|
+
logger.debug(
|
|
534
|
+
"baseline_remote_loaded_from_metadata",
|
|
535
|
+
issue_id=issue.id,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
logger.warning(
|
|
540
|
+
"baseline_remote_retrieval_failed",
|
|
541
|
+
issue_id=issue.id,
|
|
542
|
+
error=str(e),
|
|
543
|
+
severity="operational",
|
|
544
|
+
)
|
|
545
|
+
continue
|
|
546
|
+
|
|
547
|
+
logger.info(
|
|
548
|
+
"baseline_remote_retrieval_complete",
|
|
549
|
+
count=len(baseline.base_issues),
|
|
550
|
+
reason="from_sync_metadata_yaml",
|
|
551
|
+
)
|
|
552
|
+
return baseline
|
|
553
|
+
|
|
554
|
+
except Exception as e:
|
|
555
|
+
logger.error(
|
|
556
|
+
"baseline_remote_retrieval_error",
|
|
557
|
+
error=str(e),
|
|
558
|
+
error_type=type(e).__name__,
|
|
559
|
+
severity="system_error",
|
|
560
|
+
)
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
def _find_issue_file(self, issue_id: str) -> Path | None:
|
|
564
|
+
"""Find the actual file path for an issue.
|
|
565
|
+
|
|
566
|
+
Issues can be in different directories based on milestone.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
issue_id: Issue identifier
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Path to issue file, or None if not found
|
|
573
|
+
"""
|
|
574
|
+
issues_dir = self.issues_dir
|
|
575
|
+
if not issues_dir.exists():
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
# Search in milestone directories and backlog
|
|
579
|
+
search_dirs = [
|
|
580
|
+
issues_dir / "backlog",
|
|
581
|
+
issues_dir, # Root level
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
# Also search milestone directories
|
|
585
|
+
for subdir in issues_dir.glob("*"):
|
|
586
|
+
if subdir.is_dir() and subdir.name not in ["backlog", "archive"]:
|
|
587
|
+
search_dirs.append(subdir)
|
|
588
|
+
|
|
589
|
+
for search_dir in search_dirs:
|
|
590
|
+
if not search_dir.exists():
|
|
591
|
+
continue
|
|
592
|
+
# Look for files matching issue_id prefix
|
|
593
|
+
for file in search_dir.glob(f"{issue_id}-*.md"):
|
|
594
|
+
return file
|
|
595
|
+
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def _create_initial_baseline(self) -> SyncState:
|
|
599
|
+
"""Create initial baseline from current local state.
|
|
600
|
+
|
|
601
|
+
On first sync, we create a baseline from the current local state.
|
|
602
|
+
This becomes the reference point for future three-way merges.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
SyncState representing the current local state as the initial baseline
|
|
606
|
+
"""
|
|
607
|
+
from datetime import datetime
|
|
608
|
+
from pathlib import Path
|
|
609
|
+
|
|
610
|
+
logger.info("creating_initial_baseline_from_local_state")
|
|
611
|
+
|
|
612
|
+
baseline = SyncState(
|
|
613
|
+
last_sync_time=datetime.now(UTC),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Get all current local issues (including archived for baseline)
|
|
617
|
+
try:
|
|
618
|
+
local_issues = self.core.issues.list_all_including_archived()
|
|
619
|
+
for issue in local_issues:
|
|
620
|
+
try:
|
|
621
|
+
# Use issue.file_path which is set by the repository
|
|
622
|
+
# This handles both active and archived issues correctly
|
|
623
|
+
issue_file = Path(issue.file_path) if issue.file_path else None
|
|
624
|
+
|
|
625
|
+
if issue_file and issue_file.exists():
|
|
626
|
+
# For initial baseline, extract state directly from current file
|
|
627
|
+
# Don't use git history since we're establishing the first baseline
|
|
628
|
+
local_state = self.baseline_retriever.get_baseline_from_file(
|
|
629
|
+
issue_file
|
|
630
|
+
)
|
|
631
|
+
if local_state:
|
|
632
|
+
baseline.base_issues[issue.id] = local_state
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.warning(
|
|
635
|
+
"initial_baseline_issue_reconstruction_failed",
|
|
636
|
+
issue_id=issue.id,
|
|
637
|
+
error=str(e),
|
|
638
|
+
severity="operational",
|
|
639
|
+
)
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.error(
|
|
642
|
+
"initial_baseline_creation_failed",
|
|
643
|
+
error=str(e),
|
|
644
|
+
error_type=type(e).__name__,
|
|
645
|
+
severity="system_error",
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
logger.info(
|
|
649
|
+
"initial_baseline_created",
|
|
650
|
+
issue_count=len(baseline.base_issues),
|
|
651
|
+
)
|
|
652
|
+
return baseline
|
|
653
|
+
|
|
654
|
+
def get_baseline_state(self) -> SyncState | None:
|
|
655
|
+
"""Get baseline state using database cache with git fallback.
|
|
656
|
+
|
|
657
|
+
Strategy:
|
|
658
|
+
1. Try to load baseline from database (fast: ~10ms)
|
|
659
|
+
2. If not in database, fall back to git history (slower: ~500ms)
|
|
660
|
+
3. If git fails, load from YAML sync_metadata
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
SyncState with local and remote baselines
|
|
664
|
+
"""
|
|
665
|
+
try:
|
|
666
|
+
logger.debug("baseline_state_retrieval_start")
|
|
667
|
+
|
|
668
|
+
# PHASE 3: Try database baseline first (fast path)
|
|
669
|
+
db_baseline = self.core.db.get_sync_baseline()
|
|
670
|
+
if db_baseline:
|
|
671
|
+
logger.debug(
|
|
672
|
+
"baseline_state_loaded_from_database",
|
|
673
|
+
issue_count=len(db_baseline),
|
|
674
|
+
source="sync_base_state_table",
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Convert from database format to SyncState format
|
|
678
|
+
sync_state = SyncState(
|
|
679
|
+
last_sync_time=datetime.now(UTC),
|
|
680
|
+
)
|
|
681
|
+
for issue_id, data in db_baseline.items():
|
|
682
|
+
sync_state.base_issues[issue_id] = IssueBaseState(
|
|
683
|
+
id=issue_id,
|
|
684
|
+
status=data.get("status", "todo"),
|
|
685
|
+
title="", # Title not stored in baseline, will come from issue
|
|
686
|
+
assignee=data.get("assignee"),
|
|
687
|
+
description=data.get("description", ""),
|
|
688
|
+
labels=data.get("labels", []),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return sync_state
|
|
692
|
+
|
|
693
|
+
# Fallback: Try git history approach (original logic)
|
|
694
|
+
logger.debug("database_baseline_not_found_falling_back_to_git_history")
|
|
695
|
+
|
|
696
|
+
# Try to load last_synced timestamp from any issue's sync_metadata
|
|
697
|
+
remote_baseline = self._build_baseline_state_from_sync_metadata()
|
|
698
|
+
|
|
699
|
+
if remote_baseline:
|
|
700
|
+
# Now get local baseline from git history
|
|
701
|
+
local_baseline = self._build_baseline_state_from_git_history(
|
|
702
|
+
remote_baseline.last_sync_time
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if local_baseline:
|
|
706
|
+
# Merge: use git local baseline, keep remote from YAML
|
|
707
|
+
merged = SyncState(
|
|
708
|
+
last_sync_time=remote_baseline.last_sync_time,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Prefer git-reconstructed local baseline over YAML remote
|
|
712
|
+
# Remote is used for reference but git is source of truth
|
|
713
|
+
for issue_id, local_base in local_baseline.base_issues.items():
|
|
714
|
+
merged.base_issues[issue_id] = local_base
|
|
715
|
+
|
|
716
|
+
logger.info(
|
|
717
|
+
"baseline_state_loaded",
|
|
718
|
+
source="git_history_and_sync_metadata",
|
|
719
|
+
issue_count=len(merged.base_issues),
|
|
720
|
+
)
|
|
721
|
+
return merged
|
|
722
|
+
else:
|
|
723
|
+
# Only have remote baseline from YAML
|
|
724
|
+
logger.info(
|
|
725
|
+
"baseline_state_loaded",
|
|
726
|
+
source="sync_metadata_only",
|
|
727
|
+
issue_count=len(remote_baseline.base_issues),
|
|
728
|
+
)
|
|
729
|
+
return remote_baseline
|
|
730
|
+
|
|
731
|
+
# No baseline available (no database, git, or metadata)
|
|
732
|
+
logger.info(
|
|
733
|
+
"baseline_state_not_found",
|
|
734
|
+
reason="first_sync_or_no_metadata",
|
|
735
|
+
)
|
|
736
|
+
return None
|
|
737
|
+
|
|
738
|
+
except Exception as e:
|
|
739
|
+
logger.error(
|
|
740
|
+
"baseline_state_retrieval_error",
|
|
741
|
+
error=str(e),
|
|
742
|
+
error_type=type(e).__name__,
|
|
743
|
+
severity="system_error",
|
|
744
|
+
)
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
def sync_all_issues(
|
|
748
|
+
self,
|
|
749
|
+
dry_run: bool = True,
|
|
750
|
+
force_local: bool = False,
|
|
751
|
+
force_remote: bool = False,
|
|
752
|
+
push_only: bool = False,
|
|
753
|
+
pull_only: bool = False,
|
|
754
|
+
):
|
|
755
|
+
"""Override sync_all_issues to use git-based baselines."""
|
|
756
|
+
try:
|
|
757
|
+
# Use git-based baseline retrieval instead of JSON file
|
|
758
|
+
logger.info("sync_starting_with_git_based_baselines")
|
|
759
|
+
|
|
760
|
+
git_baseline = self.get_baseline_state()
|
|
761
|
+
if git_baseline:
|
|
762
|
+
# Temporarily replace state_manager's loaded state
|
|
763
|
+
logger.info(
|
|
764
|
+
"using_git_based_baseline",
|
|
765
|
+
issue_count=len(git_baseline.base_issues),
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Call parent sync with git-based baseline
|
|
769
|
+
# We'll do this by temporarily patching the state_manager
|
|
770
|
+
original_load = self.state_manager.load_sync_state
|
|
771
|
+
self.state_manager.load_sync_state = lambda: git_baseline
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
return super().sync_all_issues(
|
|
775
|
+
dry_run=dry_run,
|
|
776
|
+
force_local=force_local,
|
|
777
|
+
force_remote=force_remote,
|
|
778
|
+
push_only=push_only,
|
|
779
|
+
pull_only=pull_only,
|
|
780
|
+
)
|
|
781
|
+
finally:
|
|
782
|
+
self.state_manager.load_sync_state = original_load
|
|
783
|
+
else:
|
|
784
|
+
# No baseline - create initial baseline from current local state
|
|
785
|
+
logger.info("no_baseline_available_creating_initial_baseline")
|
|
786
|
+
|
|
787
|
+
initial_baseline = self._create_initial_baseline()
|
|
788
|
+
|
|
789
|
+
# Use the initial baseline for this sync
|
|
790
|
+
original_load = self.state_manager.load_sync_state
|
|
791
|
+
self.state_manager.load_sync_state = lambda: initial_baseline
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
return super().sync_all_issues(
|
|
795
|
+
dry_run=dry_run,
|
|
796
|
+
force_local=force_local,
|
|
797
|
+
force_remote=force_remote,
|
|
798
|
+
push_only=push_only,
|
|
799
|
+
pull_only=pull_only,
|
|
800
|
+
)
|
|
801
|
+
finally:
|
|
802
|
+
self.state_manager.load_sync_state = original_load
|
|
803
|
+
|
|
804
|
+
except Exception as e:
|
|
805
|
+
logger.error(
|
|
806
|
+
"sync_error",
|
|
807
|
+
error=str(e),
|
|
808
|
+
error_type=type(e).__name__,
|
|
809
|
+
)
|
|
810
|
+
raise
|