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,263 @@
|
|
|
1
|
+
"""Service for persisting issues to local storage (YAML files)."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
from roadmap.core.domain.issue import Issue
|
|
9
|
+
from roadmap.core.models.sync_models import SyncIssue
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore # noqa: F401
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IssuePersistenceService:
|
|
18
|
+
"""Handles persistence of issues to local storage (YAML files).
|
|
19
|
+
|
|
20
|
+
Centralizes all operations for:
|
|
21
|
+
- Updating the github_issue field on Issue objects
|
|
22
|
+
- Managing remote_ids dictionaries
|
|
23
|
+
- Saving issues to YAML repository
|
|
24
|
+
|
|
25
|
+
This eliminates the duplication of save patterns in pull_issue() and push_issue().
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def update_issue_with_remote_id(
|
|
30
|
+
issue: Issue,
|
|
31
|
+
backend_name: str,
|
|
32
|
+
backend_id: int | str,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Update an issue's remote_ids dictionary with a new backend mapping.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
issue: Issue to update
|
|
38
|
+
backend_name: Backend name (e.g., "github")
|
|
39
|
+
backend_id: ID from remote backend
|
|
40
|
+
"""
|
|
41
|
+
if issue.remote_ids is None:
|
|
42
|
+
issue.remote_ids = {}
|
|
43
|
+
|
|
44
|
+
issue.remote_ids[backend_name] = str(backend_id)
|
|
45
|
+
|
|
46
|
+
logger.debug(
|
|
47
|
+
"updated_remote_ids",
|
|
48
|
+
issue_id=issue.id,
|
|
49
|
+
backend_name=backend_name,
|
|
50
|
+
backend_id=backend_id,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def update_github_issue_number(issue: Issue, github_number: int) -> None:
|
|
55
|
+
"""Update the github_issue field on an Issue object.
|
|
56
|
+
|
|
57
|
+
This field is used for quick GitHub lookups without querying remote_ids.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
issue: Issue to update
|
|
61
|
+
github_number: GitHub issue number to set
|
|
62
|
+
"""
|
|
63
|
+
issue.github_issue = github_number
|
|
64
|
+
|
|
65
|
+
logger.debug(
|
|
66
|
+
"updated_github_issue_number",
|
|
67
|
+
issue_id=issue.id,
|
|
68
|
+
github_issue=github_number,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def save_issue(issue: Issue, core: "RoadmapCore") -> bool:
|
|
73
|
+
"""Save an issue to local YAML repository.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
issue: Issue to save
|
|
77
|
+
core: RoadmapCore for accessing repositories
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if save succeeded, False otherwise
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
if not issue or not issue.id:
|
|
84
|
+
logger.warning(
|
|
85
|
+
"cannot_save_invalid_issue",
|
|
86
|
+
issue_id=getattr(issue, "id", None),
|
|
87
|
+
)
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
repo = core.issue_service.repository
|
|
91
|
+
if not repo:
|
|
92
|
+
logger.error(
|
|
93
|
+
"issue_repository_not_available",
|
|
94
|
+
issue_id=issue.id,
|
|
95
|
+
)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
attempts = 0
|
|
99
|
+
max_attempts = 3
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
repo.save(issue)
|
|
103
|
+
break
|
|
104
|
+
except OSError as e:
|
|
105
|
+
attempts += 1
|
|
106
|
+
if attempts >= max_attempts:
|
|
107
|
+
raise
|
|
108
|
+
backoff = 0.1 * (2 ** (attempts - 1))
|
|
109
|
+
logger.warning(
|
|
110
|
+
"issue_persistence_retrying",
|
|
111
|
+
issue_id=issue.id,
|
|
112
|
+
attempt=attempts,
|
|
113
|
+
delay_seconds=backoff,
|
|
114
|
+
error=str(e),
|
|
115
|
+
)
|
|
116
|
+
time.sleep(backoff)
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
"issue_saved_to_yaml",
|
|
120
|
+
issue_id=issue.id,
|
|
121
|
+
title=issue.title,
|
|
122
|
+
)
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
except AttributeError as e:
|
|
126
|
+
logger.error(
|
|
127
|
+
"issue_persistence_attribute_error",
|
|
128
|
+
issue_id=getattr(issue, "id", "unknown"),
|
|
129
|
+
error=str(e),
|
|
130
|
+
error_type="AttributeError",
|
|
131
|
+
)
|
|
132
|
+
return False
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(
|
|
135
|
+
"issue_persistence_save_failed",
|
|
136
|
+
issue_id=getattr(issue, "id", "unknown"),
|
|
137
|
+
error=str(e),
|
|
138
|
+
error_type=type(e).__name__,
|
|
139
|
+
exc_info=True,
|
|
140
|
+
)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def apply_sync_issue_to_local(
|
|
145
|
+
local_issue: Issue,
|
|
146
|
+
sync_issue: SyncIssue,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Apply data from a SyncIssue to update a local Issue object.
|
|
149
|
+
|
|
150
|
+
Used when pulling changes from remote backend to update local state.
|
|
151
|
+
Preserves local ID while updating fields from remote.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
local_issue: Local issue to update (ID preserved)
|
|
155
|
+
sync_issue: Remote sync_issue with new data
|
|
156
|
+
"""
|
|
157
|
+
# Update core fields from sync_issue
|
|
158
|
+
local_issue.title = sync_issue.title or local_issue.title
|
|
159
|
+
local_issue.assignee = sync_issue.assignee or local_issue.assignee
|
|
160
|
+
local_issue.milestone = sync_issue.milestone or local_issue.milestone
|
|
161
|
+
local_issue.labels = sync_issue.labels or local_issue.labels
|
|
162
|
+
|
|
163
|
+
# Update timestamps
|
|
164
|
+
if sync_issue.updated_at:
|
|
165
|
+
local_issue.updated = sync_issue.updated_at
|
|
166
|
+
|
|
167
|
+
# Update remote_ids from sync_issue (important for tracking)
|
|
168
|
+
if sync_issue.remote_ids:
|
|
169
|
+
if local_issue.remote_ids is None:
|
|
170
|
+
local_issue.remote_ids = {}
|
|
171
|
+
local_issue.remote_ids.update(sync_issue.remote_ids)
|
|
172
|
+
|
|
173
|
+
# Merge metadata while preserving local metadata
|
|
174
|
+
if sync_issue.metadata:
|
|
175
|
+
if local_issue.github_sync_metadata is None:
|
|
176
|
+
local_issue.github_sync_metadata = {}
|
|
177
|
+
local_issue.github_sync_metadata.update(sync_issue.metadata)
|
|
178
|
+
|
|
179
|
+
logger.debug(
|
|
180
|
+
"applied_sync_issue_to_local",
|
|
181
|
+
issue_id=local_issue.id,
|
|
182
|
+
title=local_issue.title,
|
|
183
|
+
from_backend=sync_issue.backend_name,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def get_issue_from_repo(
|
|
188
|
+
issue_id: str,
|
|
189
|
+
core: "RoadmapCore",
|
|
190
|
+
) -> Issue | None:
|
|
191
|
+
"""Retrieve an issue from the local repository.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
issue_id: Issue ID to retrieve
|
|
195
|
+
core: RoadmapCore for accessing repositories
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Issue if found, None otherwise
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
repo = core.issue_service.repository
|
|
202
|
+
issue = repo.get(issue_id)
|
|
203
|
+
|
|
204
|
+
if issue:
|
|
205
|
+
logger.debug(
|
|
206
|
+
"retrieved_issue_from_repo",
|
|
207
|
+
issue_id=issue_id,
|
|
208
|
+
title=issue.get("title")
|
|
209
|
+
if isinstance(issue, dict)
|
|
210
|
+
else issue.title,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
logger.debug("issue_not_found_in_repo", issue_id=issue_id)
|
|
214
|
+
|
|
215
|
+
return issue
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(
|
|
219
|
+
"failed_to_retrieve_issue_from_repo",
|
|
220
|
+
issue_id=issue_id,
|
|
221
|
+
error=str(e),
|
|
222
|
+
)
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def is_github_linked(issue: Issue) -> bool:
|
|
227
|
+
"""Check if an issue is already linked to GitHub.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
issue: Issue to check
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if issue has github_issue set or github in remote_ids
|
|
234
|
+
"""
|
|
235
|
+
if hasattr(issue, "github_issue") and issue.github_issue:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
if issue.remote_ids and "github" in issue.remote_ids:
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def get_github_issue_number(issue: Issue) -> int | str | None:
|
|
245
|
+
"""Extract the GitHub issue number from an Issue.
|
|
246
|
+
|
|
247
|
+
Checks both github_issue field and remote_ids dict.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
issue: Issue to extract from
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
GitHub issue number if found, None otherwise
|
|
254
|
+
"""
|
|
255
|
+
# First check direct field (property)
|
|
256
|
+
if issue.github_issue is not None:
|
|
257
|
+
return issue.github_issue
|
|
258
|
+
|
|
259
|
+
# Then check remote_ids
|
|
260
|
+
if issue.remote_ids and "github" in issue.remote_ids:
|
|
261
|
+
return issue.remote_ids["github"]
|
|
262
|
+
|
|
263
|
+
return None
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Service for converting between SyncIssue and local Issue domain objects."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
from roadmap.common.constants import Priority, Status
|
|
9
|
+
from roadmap.core.domain.issue import Issue
|
|
10
|
+
from roadmap.core.models.sync_models import SyncIssue
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IssueStateService:
|
|
16
|
+
"""Handles conversion between SyncIssue (remote) and Issue (local) domain objects.
|
|
17
|
+
|
|
18
|
+
Centralizes field mapping, normalization, and type conversions to eliminate
|
|
19
|
+
duplication between pull_issue() and push_issue() methods.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def sync_issue_to_issue(issue_id: str, sync_issue: SyncIssue) -> Issue:
|
|
24
|
+
"""Convert a SyncIssue (from backend) to a local Issue domain object.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
issue_id: Local issue ID to assign
|
|
28
|
+
sync_issue: Remote SyncIssue with backend data
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Issue object ready for local storage/update
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If required fields are missing
|
|
35
|
+
"""
|
|
36
|
+
IssueStateService._validate_sync_issue_input(issue_id, sync_issue)
|
|
37
|
+
IssueStateService._warn_if_missing_sync_title(issue_id, sync_issue)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
created_at, updated_at = IssueStateService._resolve_sync_timestamps(
|
|
41
|
+
sync_issue
|
|
42
|
+
)
|
|
43
|
+
status = IssueStateService.normalize_status(sync_issue.status)
|
|
44
|
+
content = IssueStateService._resolve_sync_content(sync_issue)
|
|
45
|
+
|
|
46
|
+
issue = Issue(
|
|
47
|
+
id=issue_id,
|
|
48
|
+
title=sync_issue.title or "Untitled",
|
|
49
|
+
content=content,
|
|
50
|
+
status=status,
|
|
51
|
+
priority=Priority.MEDIUM, # Default priority (not in SyncIssue)
|
|
52
|
+
labels=sync_issue.labels or [],
|
|
53
|
+
assignee=sync_issue.assignee,
|
|
54
|
+
milestone=sync_issue.milestone,
|
|
55
|
+
created=created_at,
|
|
56
|
+
updated=updated_at,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if sync_issue.remote_ids:
|
|
60
|
+
issue.remote_ids = sync_issue.remote_ids.copy()
|
|
61
|
+
|
|
62
|
+
issue.github_sync_metadata = IssueStateService._build_sync_metadata(
|
|
63
|
+
sync_issue
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
logger.info(
|
|
67
|
+
"converted_sync_issue_to_issue",
|
|
68
|
+
issue_id=issue_id,
|
|
69
|
+
title=issue.title,
|
|
70
|
+
status=status.value,
|
|
71
|
+
from_backend=sync_issue.backend_name,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return issue
|
|
75
|
+
except ValueError as e:
|
|
76
|
+
logger.error(
|
|
77
|
+
"sync_issue_conversion_validation_error",
|
|
78
|
+
issue_id=issue_id,
|
|
79
|
+
error=str(e),
|
|
80
|
+
)
|
|
81
|
+
raise
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(
|
|
84
|
+
"sync_issue_conversion_failed",
|
|
85
|
+
issue_id=issue_id,
|
|
86
|
+
backend_name=sync_issue.backend_name,
|
|
87
|
+
error=str(e),
|
|
88
|
+
error_type=type(e).__name__,
|
|
89
|
+
exc_info=True,
|
|
90
|
+
)
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _validate_sync_issue_input(issue_id: str, sync_issue: SyncIssue | None) -> None:
|
|
95
|
+
"""Validate required inputs for sync issue conversion."""
|
|
96
|
+
if not issue_id or not issue_id.strip():
|
|
97
|
+
logger.error("cannot_convert_sync_issue_empty_issue_id")
|
|
98
|
+
raise ValueError("issue_id cannot be empty")
|
|
99
|
+
if not sync_issue:
|
|
100
|
+
logger.error("cannot_convert_none_sync_issue")
|
|
101
|
+
raise ValueError("sync_issue cannot be None")
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _warn_if_missing_sync_title(issue_id: str, sync_issue: SyncIssue) -> None:
|
|
105
|
+
"""Emit warning when remote issue title is missing."""
|
|
106
|
+
if sync_issue.title and sync_issue.title.strip():
|
|
107
|
+
return
|
|
108
|
+
logger.warning(
|
|
109
|
+
"sync_issue_missing_title",
|
|
110
|
+
issue_id=issue_id,
|
|
111
|
+
backend_name=sync_issue.backend_name,
|
|
112
|
+
backend_id=sync_issue.backend_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _resolve_sync_timestamps(sync_issue: SyncIssue) -> tuple[datetime, datetime]:
|
|
117
|
+
"""Resolve created/updated timestamps for a sync issue."""
|
|
118
|
+
created_at = sync_issue.created_at or datetime.now(UTC)
|
|
119
|
+
updated_at = sync_issue.updated_at or datetime.now(UTC)
|
|
120
|
+
return created_at, updated_at
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _resolve_sync_content(sync_issue: SyncIssue) -> str:
|
|
124
|
+
"""Resolve normalized content from sync issue headline."""
|
|
125
|
+
if sync_issue.headline and sync_issue.headline.strip():
|
|
126
|
+
return sync_issue.headline
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _build_sync_metadata(sync_issue: SyncIssue) -> dict[str, Any]:
|
|
131
|
+
"""Build metadata payload persisted on local issue."""
|
|
132
|
+
return {
|
|
133
|
+
"backend_name": sync_issue.backend_name,
|
|
134
|
+
"backend_id": sync_issue.backend_id,
|
|
135
|
+
**(sync_issue.metadata or {}),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def issue_to_push_payload(issue: Issue) -> dict[str, Any]:
|
|
140
|
+
"""Convert a local Issue to a push payload for remote backend.
|
|
141
|
+
|
|
142
|
+
Extracts fields that are relevant for pushing to GitHub/remote.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
issue: Local Issue to convert
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary with fields safe for remote API
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: If issue is invalid or missing required fields
|
|
152
|
+
"""
|
|
153
|
+
if not issue:
|
|
154
|
+
logger.error("cannot_build_push_payload_none_issue")
|
|
155
|
+
raise ValueError("issue cannot be None")
|
|
156
|
+
|
|
157
|
+
if not issue.title or not issue.title.strip():
|
|
158
|
+
logger.error(
|
|
159
|
+
"cannot_build_push_payload_empty_title",
|
|
160
|
+
issue_id=issue.id,
|
|
161
|
+
)
|
|
162
|
+
raise ValueError("issue.title cannot be empty")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
# Normalize status to backend format
|
|
166
|
+
state = "open"
|
|
167
|
+
if issue.status == Status.CLOSED:
|
|
168
|
+
state = "closed"
|
|
169
|
+
|
|
170
|
+
payload: dict[str, Any] = {
|
|
171
|
+
"title": issue.title,
|
|
172
|
+
"body": issue.content or "",
|
|
173
|
+
"state": state,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Handle labels - may be comma-separated strings
|
|
177
|
+
if issue.labels:
|
|
178
|
+
labels_list: list[str] = []
|
|
179
|
+
for label in issue.labels:
|
|
180
|
+
if isinstance(label, str):
|
|
181
|
+
labels_list.extend(
|
|
182
|
+
[
|
|
183
|
+
label_item.strip()
|
|
184
|
+
for label_item in label.split(",")
|
|
185
|
+
if label_item.strip()
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
labels_list.append(str(label).strip())
|
|
190
|
+
|
|
191
|
+
if labels_list:
|
|
192
|
+
payload["labels"] = labels_list
|
|
193
|
+
|
|
194
|
+
logger.info(
|
|
195
|
+
"issue_converted_to_push_payload",
|
|
196
|
+
issue_id=issue.id,
|
|
197
|
+
title=issue.title,
|
|
198
|
+
state=state,
|
|
199
|
+
has_labels=bool(payload.get("labels")),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return payload
|
|
203
|
+
except ValueError as e:
|
|
204
|
+
logger.error(
|
|
205
|
+
"push_payload_validation_error",
|
|
206
|
+
issue_id=issue.id,
|
|
207
|
+
error=str(e),
|
|
208
|
+
)
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(
|
|
212
|
+
"push_payload_creation_failed",
|
|
213
|
+
issue_id=issue.id,
|
|
214
|
+
title=issue.title,
|
|
215
|
+
error=str(e),
|
|
216
|
+
error_type=type(e).__name__,
|
|
217
|
+
exc_info=True,
|
|
218
|
+
)
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def normalize_status(status_value: str | None) -> Status:
|
|
223
|
+
"""Normalize a status string from any backend to local Status enum.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
status_value: Status string from backend
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Normalized Status enum value
|
|
230
|
+
"""
|
|
231
|
+
if not status_value:
|
|
232
|
+
return Status.TODO
|
|
233
|
+
|
|
234
|
+
status_lower = status_value.lower()
|
|
235
|
+
|
|
236
|
+
if status_lower in ("closed", "done", "completed", "resolved"):
|
|
237
|
+
return Status.CLOSED
|
|
238
|
+
elif status_lower in ("in_progress", "in progress", "active", "started"):
|
|
239
|
+
return Status.IN_PROGRESS
|
|
240
|
+
elif status_lower in ("on_hold", "on hold", "blocked", "paused"):
|
|
241
|
+
return Status.BLOCKED
|
|
242
|
+
else:
|
|
243
|
+
return Status.TODO
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Filters local changes from baseline for sync operations.
|
|
2
|
+
|
|
3
|
+
Extracted from SyncMergeEngine to separate change filtering logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LocalChangeFilter:
|
|
12
|
+
"""Filters and identifies local changes since baseline."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def filter_unchanged_from_base(
|
|
16
|
+
issues: list, current_local: dict, base_state_issues: dict
|
|
17
|
+
) -> list:
|
|
18
|
+
"""Filter out unchanged issues, keeping only changed or new ones.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
issues: List of issues to filter
|
|
22
|
+
current_local: Dict of current local issues keyed by ID
|
|
23
|
+
base_state_issues: Dict of baseline state issues keyed by ID
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Filtered list containing only changed or new issues
|
|
27
|
+
"""
|
|
28
|
+
if not base_state_issues:
|
|
29
|
+
logger.debug(
|
|
30
|
+
"filter_no_base_state",
|
|
31
|
+
input_count=len(issues),
|
|
32
|
+
reason="first_sync_no_previous_state",
|
|
33
|
+
)
|
|
34
|
+
return issues
|
|
35
|
+
|
|
36
|
+
filtered = []
|
|
37
|
+
skipped_count = 0
|
|
38
|
+
new_count = 0
|
|
39
|
+
changed_count = 0
|
|
40
|
+
|
|
41
|
+
for issue in issues:
|
|
42
|
+
issue_id = issue.id if hasattr(issue, "id") else issue
|
|
43
|
+
if issue_id not in current_local:
|
|
44
|
+
logger.debug(
|
|
45
|
+
"filter_issue_not_in_local",
|
|
46
|
+
issue_id=issue_id,
|
|
47
|
+
reason="might_be_stale",
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
if issue_id not in base_state_issues:
|
|
51
|
+
logger.debug(
|
|
52
|
+
"filter_new_local_issue",
|
|
53
|
+
issue_id=issue_id,
|
|
54
|
+
reason="not_in_previous_sync",
|
|
55
|
+
)
|
|
56
|
+
new_count += 1
|
|
57
|
+
filtered.append(issue)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
local_issue = current_local[issue_id]
|
|
61
|
+
base_state = base_state_issues[issue_id]
|
|
62
|
+
|
|
63
|
+
fields_to_check = {
|
|
64
|
+
"status": lambda obj: (
|
|
65
|
+
obj.status.value
|
|
66
|
+
if hasattr(obj.status, "value")
|
|
67
|
+
else str(obj.status)
|
|
68
|
+
),
|
|
69
|
+
"assignee": lambda obj: obj.assignee,
|
|
70
|
+
"description": lambda obj: obj.description,
|
|
71
|
+
"labels": lambda obj: sorted(obj.labels or []),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
has_local_changes = False
|
|
75
|
+
changed_fields = []
|
|
76
|
+
|
|
77
|
+
for field_name, getter in fields_to_check.items():
|
|
78
|
+
try:
|
|
79
|
+
local_value = getter(local_issue)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(
|
|
82
|
+
"filter_field_extraction_failed",
|
|
83
|
+
issue_id=issue_id,
|
|
84
|
+
field=field_name,
|
|
85
|
+
error=str(e),
|
|
86
|
+
)
|
|
87
|
+
local_value = None
|
|
88
|
+
|
|
89
|
+
base_value = getattr(base_state, field_name, None)
|
|
90
|
+
|
|
91
|
+
if local_value != base_value:
|
|
92
|
+
logger.debug(
|
|
93
|
+
"filter_local_change_detected",
|
|
94
|
+
issue_id=issue_id,
|
|
95
|
+
field=field_name,
|
|
96
|
+
base_value=base_value,
|
|
97
|
+
local_value=local_value,
|
|
98
|
+
)
|
|
99
|
+
changed_fields.append(field_name)
|
|
100
|
+
has_local_changes = True
|
|
101
|
+
|
|
102
|
+
if has_local_changes:
|
|
103
|
+
logger.debug(
|
|
104
|
+
"filter_issue_has_local_changes",
|
|
105
|
+
issue_id=issue_id,
|
|
106
|
+
changed_fields=changed_fields,
|
|
107
|
+
)
|
|
108
|
+
changed_count += 1
|
|
109
|
+
filtered.append(issue)
|
|
110
|
+
else:
|
|
111
|
+
logger.debug(
|
|
112
|
+
"filter_issue_unchanged_since_sync",
|
|
113
|
+
issue_id=issue_id,
|
|
114
|
+
reason="no_local_modifications",
|
|
115
|
+
)
|
|
116
|
+
skipped_count += 1
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
"filter_complete",
|
|
120
|
+
input_count=len(issues),
|
|
121
|
+
output_count=len(filtered),
|
|
122
|
+
skipped_count=skipped_count,
|
|
123
|
+
new_count=new_count,
|
|
124
|
+
changed_count=changed_count,
|
|
125
|
+
filtered_out_percentage=round((skipped_count / len(issues) * 100), 1)
|
|
126
|
+
if len(issues) > 0
|
|
127
|
+
else 0,
|
|
128
|
+
)
|
|
129
|
+
return filtered
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Processes pull results from sync operations.
|
|
2
|
+
|
|
3
|
+
Extracted from SyncMergeEngine to separate pull processing logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from structlog import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PullResultProcessor:
|
|
12
|
+
"""Processes results from pull operations."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def process_pull_result(fetched) -> tuple[int, list, list]:
|
|
16
|
+
"""Process pull result and extract metrics.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
fetched: Pull result (list or report object)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Tuple of (pulled_count, pull_errors, pulled_remote_ids)
|
|
23
|
+
"""
|
|
24
|
+
pulled_count = 0
|
|
25
|
+
pull_errors = []
|
|
26
|
+
pulled_remote_ids = []
|
|
27
|
+
|
|
28
|
+
if isinstance(fetched, list):
|
|
29
|
+
pulled_items = [r for r in fetched if r]
|
|
30
|
+
pulled_count = len(pulled_items)
|
|
31
|
+
for item in pulled_items:
|
|
32
|
+
try:
|
|
33
|
+
rid = getattr(item, "backend_id", None) or getattr(item, "id", None)
|
|
34
|
+
if rid is not None:
|
|
35
|
+
pulled_remote_ids.append(str(rid))
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.debug("pulled_item_id_extraction_failed", error=str(e))
|
|
38
|
+
continue
|
|
39
|
+
else:
|
|
40
|
+
pull_report = fetched
|
|
41
|
+
if getattr(pull_report, "errors", None):
|
|
42
|
+
try:
|
|
43
|
+
err_keys = (
|
|
44
|
+
list(pull_report.errors.keys())
|
|
45
|
+
if isinstance(pull_report.errors, dict)
|
|
46
|
+
else []
|
|
47
|
+
)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.debug("error_keys_extraction_failed", error=str(e))
|
|
50
|
+
err_keys = []
|
|
51
|
+
pull_errors = err_keys
|
|
52
|
+
logger.warning(
|
|
53
|
+
"pull_batch_had_errors",
|
|
54
|
+
error_count=len(pull_errors),
|
|
55
|
+
errors=str(pull_report.errors)[:200],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
pulled_raw = getattr(pull_report, "pulled", None)
|
|
59
|
+
if pulled_raw is None:
|
|
60
|
+
pulled_count = 0
|
|
61
|
+
pulled_remote_ids = []
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
pulled_iter = list(pulled_raw)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.debug("pulled_iter_conversion_failed", error=str(e))
|
|
67
|
+
pulled_iter = [pulled_raw]
|
|
68
|
+
pulled_remote_ids = [str(i) for i in pulled_iter]
|
|
69
|
+
pulled_count = len(pulled_remote_ids)
|
|
70
|
+
|
|
71
|
+
return pulled_count, pull_errors, pulled_remote_ids
|