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,360 @@
|
|
|
1
|
+
"""Dependency analysis service for detecting issues in issue relationships.
|
|
2
|
+
|
|
3
|
+
Analyzes depends_on and blocks relationships to detect:
|
|
4
|
+
- Circular dependencies (A → B → A)
|
|
5
|
+
- Broken references (pointing to non-existent issues)
|
|
6
|
+
- Self-dependencies (issue depends on itself)
|
|
7
|
+
- Orphaned blockers (issue blocks non-existent issue)
|
|
8
|
+
- Deep dependency chains (chains > 5 levels)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
from roadmap.common.logging import get_logger
|
|
15
|
+
from roadmap.common.logging.error_logging import (
|
|
16
|
+
log_error_with_context,
|
|
17
|
+
)
|
|
18
|
+
from roadmap.common.observability.instrumentation import traced
|
|
19
|
+
from roadmap.core.domain.issue import Issue
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DependencyIssueType(StrEnum):
|
|
25
|
+
"""Types of dependency issues that can be found."""
|
|
26
|
+
|
|
27
|
+
CIRCULAR = "circular_dependency"
|
|
28
|
+
BROKEN = "broken_dependency"
|
|
29
|
+
SELF = "self_dependency"
|
|
30
|
+
ORPHANED_BLOCKER = "orphaned_blocker"
|
|
31
|
+
DEEP_CHAIN = "deep_dependency_chain"
|
|
32
|
+
MISSING_BIDIRECTIONAL = "missing_bidirectional"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DependencyIssue:
|
|
37
|
+
"""A problem found in issue dependencies."""
|
|
38
|
+
|
|
39
|
+
issue_id: str
|
|
40
|
+
issue_type: DependencyIssueType
|
|
41
|
+
message: str
|
|
42
|
+
affected_issues: list[str] = field(default_factory=list)
|
|
43
|
+
chain_length: int | None = None # For deep chain issues
|
|
44
|
+
severity: str = "warning" # error, warning, info
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
"""Set severity based on issue type."""
|
|
48
|
+
if self.issue_type == DependencyIssueType.CIRCULAR:
|
|
49
|
+
self.severity = "error"
|
|
50
|
+
elif self.issue_type == DependencyIssueType.BROKEN:
|
|
51
|
+
self.severity = "error"
|
|
52
|
+
elif self.issue_type == DependencyIssueType.SELF:
|
|
53
|
+
self.severity = "error"
|
|
54
|
+
elif self.issue_type == DependencyIssueType.ORPHANED_BLOCKER:
|
|
55
|
+
self.severity = "warning"
|
|
56
|
+
elif self.issue_type == DependencyIssueType.DEEP_CHAIN:
|
|
57
|
+
self.severity = "info"
|
|
58
|
+
elif self.issue_type == DependencyIssueType.MISSING_BIDIRECTIONAL:
|
|
59
|
+
self.severity = "warning"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class DependencyAnalysisResult:
|
|
64
|
+
"""Result of analyzing all dependencies."""
|
|
65
|
+
|
|
66
|
+
total_issues: int
|
|
67
|
+
issues_with_dependencies: int
|
|
68
|
+
issues_with_problems: int
|
|
69
|
+
problems: list[DependencyIssue] = field(default_factory=list)
|
|
70
|
+
circular_chains: list[list[str]] = field(
|
|
71
|
+
default_factory=list
|
|
72
|
+
) # Lists of issue IDs forming cycles
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_healthy(self) -> bool:
|
|
76
|
+
"""Check if all dependencies are healthy."""
|
|
77
|
+
return (
|
|
78
|
+
not any(p.severity == "error" for p in self.problems)
|
|
79
|
+
and not self.circular_chains
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def error_count(self) -> int:
|
|
84
|
+
"""Count of critical errors."""
|
|
85
|
+
return len([p for p in self.problems if p.severity == "error"]) + len(
|
|
86
|
+
self.circular_chains
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def warning_count(self) -> int:
|
|
91
|
+
"""Count of warnings."""
|
|
92
|
+
return len([p for p in self.problems if p.severity == "warning"])
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def info_count(self) -> int:
|
|
96
|
+
"""Count of informational issues."""
|
|
97
|
+
return len([p for p in self.problems if p.severity == "info"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class DependencyAnalyzer:
|
|
101
|
+
"""Analyzes issue dependency relationships for problems."""
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
"""Initialize the analyzer."""
|
|
105
|
+
self._issue_map: dict[str, Issue] = {}
|
|
106
|
+
self._analyzed = False
|
|
107
|
+
|
|
108
|
+
@traced("analyze_dependencies")
|
|
109
|
+
def analyze(self, issues: list[Issue]) -> DependencyAnalysisResult:
|
|
110
|
+
"""Analyze all dependencies in the issue set.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
issues: List of all issues in the system
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
DependencyAnalysisResult with all problems found
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
self._issue_map = {issue.id: issue for issue in issues}
|
|
120
|
+
self._analyzed = True
|
|
121
|
+
|
|
122
|
+
result = DependencyAnalysisResult(
|
|
123
|
+
total_issues=len(issues),
|
|
124
|
+
issues_with_dependencies=sum(
|
|
125
|
+
1 for i in issues if i.depends_on or i.blocks
|
|
126
|
+
),
|
|
127
|
+
issues_with_problems=0,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Check all issues
|
|
131
|
+
for issue in issues:
|
|
132
|
+
self._check_issue_dependencies(issue, result)
|
|
133
|
+
|
|
134
|
+
result.issues_with_problems = len({p.issue_id for p in result.problems})
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
except Exception as e:
|
|
138
|
+
log_error_with_context(
|
|
139
|
+
e,
|
|
140
|
+
operation="analyze_dependencies",
|
|
141
|
+
entity_type="Issue",
|
|
142
|
+
additional_context={"total_issues": len(issues)},
|
|
143
|
+
)
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
def _check_depends_on_links(
|
|
147
|
+
self, issue: Issue, result: DependencyAnalysisResult
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Check the depends_on list for self-deps, broken refs, and missing bidirectional links."""
|
|
150
|
+
for dep_id in issue.depends_on or []:
|
|
151
|
+
if dep_id == issue.id:
|
|
152
|
+
result.problems.append(
|
|
153
|
+
DependencyIssue(
|
|
154
|
+
issue_id=issue.id,
|
|
155
|
+
issue_type=DependencyIssueType.SELF,
|
|
156
|
+
message="Issue depends on itself",
|
|
157
|
+
affected_issues=[dep_id],
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
continue
|
|
161
|
+
if dep_id not in self._issue_map:
|
|
162
|
+
result.problems.append(
|
|
163
|
+
DependencyIssue(
|
|
164
|
+
issue_id=issue.id,
|
|
165
|
+
issue_type=DependencyIssueType.BROKEN,
|
|
166
|
+
message=f"Depends on non-existent issue {dep_id}",
|
|
167
|
+
affected_issues=[dep_id],
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
dep_issue = self._issue_map[dep_id]
|
|
172
|
+
if issue.id not in (dep_issue.blocks or []):
|
|
173
|
+
result.problems.append(
|
|
174
|
+
DependencyIssue(
|
|
175
|
+
issue_id=issue.id,
|
|
176
|
+
issue_type=DependencyIssueType.MISSING_BIDIRECTIONAL,
|
|
177
|
+
message=f"Depends on {dep_id} but {dep_id} does not list this as a blocker",
|
|
178
|
+
affected_issues=[dep_id],
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _check_blocks_links(
|
|
183
|
+
self, issue: Issue, result: DependencyAnalysisResult
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Check the blocks list for self-blocks, orphaned refs, and missing bidirectional links."""
|
|
186
|
+
for blocked_id in issue.blocks or []:
|
|
187
|
+
if blocked_id == issue.id:
|
|
188
|
+
result.problems.append(
|
|
189
|
+
DependencyIssue(
|
|
190
|
+
issue_id=issue.id,
|
|
191
|
+
issue_type=DependencyIssueType.SELF,
|
|
192
|
+
message="Issue blocks itself",
|
|
193
|
+
affected_issues=[blocked_id],
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
197
|
+
if blocked_id not in self._issue_map:
|
|
198
|
+
result.problems.append(
|
|
199
|
+
DependencyIssue(
|
|
200
|
+
issue_id=issue.id,
|
|
201
|
+
issue_type=DependencyIssueType.ORPHANED_BLOCKER,
|
|
202
|
+
message=f"Blocks non-existent issue {blocked_id}",
|
|
203
|
+
affected_issues=[blocked_id],
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
207
|
+
blocked_issue = self._issue_map[blocked_id]
|
|
208
|
+
if issue.id not in (blocked_issue.depends_on or []):
|
|
209
|
+
result.problems.append(
|
|
210
|
+
DependencyIssue(
|
|
211
|
+
issue_id=issue.id,
|
|
212
|
+
issue_type=DependencyIssueType.MISSING_BIDIRECTIONAL,
|
|
213
|
+
message=f"Blocks {blocked_id} but {blocked_id} does not list this as a dependency",
|
|
214
|
+
affected_issues=[blocked_id],
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _check_chain_and_cycles(
|
|
219
|
+
self, issue: Issue, result: DependencyAnalysisResult
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Check for deep dependency chains and circular dependencies."""
|
|
222
|
+
if not issue.depends_on:
|
|
223
|
+
return
|
|
224
|
+
max_depth = self._find_max_chain_depth(issue.id, set(), 0)
|
|
225
|
+
if max_depth > 5:
|
|
226
|
+
result.problems.append(
|
|
227
|
+
DependencyIssue(
|
|
228
|
+
issue_id=issue.id,
|
|
229
|
+
issue_type=DependencyIssueType.DEEP_CHAIN,
|
|
230
|
+
message=f"Deep dependency chain ({max_depth} levels)",
|
|
231
|
+
affected_issues=issue.depends_on,
|
|
232
|
+
chain_length=max_depth,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
for dep_id in issue.depends_on:
|
|
236
|
+
cycle = self._find_cycle_from(dep_id, issue.id, set())
|
|
237
|
+
if cycle:
|
|
238
|
+
cycle_list = sorted(list(cycle) + [issue.id])
|
|
239
|
+
if cycle_list not in result.circular_chains:
|
|
240
|
+
result.circular_chains.append(cycle_list)
|
|
241
|
+
result.problems.append(
|
|
242
|
+
DependencyIssue(
|
|
243
|
+
issue_id=issue.id,
|
|
244
|
+
issue_type=DependencyIssueType.CIRCULAR,
|
|
245
|
+
message=f"Circular dependency detected: {' → '.join(cycle_list)} → {issue.id}",
|
|
246
|
+
affected_issues=cycle_list,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _check_issue_dependencies(
|
|
251
|
+
self, issue: Issue, result: DependencyAnalysisResult
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Check all dependency relationships for a single issue."""
|
|
254
|
+
self._check_depends_on_links(issue, result)
|
|
255
|
+
self._check_blocks_links(issue, result)
|
|
256
|
+
self._check_chain_and_cycles(issue, result)
|
|
257
|
+
|
|
258
|
+
def _find_cycle_from(
|
|
259
|
+
self, current_id: str, target_id: str, visited: set[str]
|
|
260
|
+
) -> set[str] | None:
|
|
261
|
+
"""Find if there's a path from current_id back to target_id.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
current_id: ID to start searching from
|
|
265
|
+
target_id: ID we're trying to reach (forming a cycle)
|
|
266
|
+
visited: IDs already visited in this search
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Set of IDs in the cycle if found, None otherwise
|
|
270
|
+
"""
|
|
271
|
+
if current_id not in self._issue_map:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
if current_id in visited:
|
|
275
|
+
return None # Already visited this node, not our cycle
|
|
276
|
+
|
|
277
|
+
if current_id == target_id:
|
|
278
|
+
return {current_id} # Found the cycle!
|
|
279
|
+
|
|
280
|
+
visited.add(current_id)
|
|
281
|
+
issue = self._issue_map[current_id]
|
|
282
|
+
|
|
283
|
+
# Check all issues this one depends on
|
|
284
|
+
for dep_id in issue.depends_on or []:
|
|
285
|
+
result = self._find_cycle_from(dep_id, target_id, visited.copy())
|
|
286
|
+
if result:
|
|
287
|
+
result.add(current_id)
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def _find_max_chain_depth(
|
|
293
|
+
self, current_id: str, visited: set[str], depth: int
|
|
294
|
+
) -> int:
|
|
295
|
+
"""Find the maximum dependency chain depth from current issue.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
current_id: Issue ID to start from
|
|
299
|
+
visited: Set of visited IDs (to avoid infinite loops)
|
|
300
|
+
depth: Current depth in the chain
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Maximum chain depth found
|
|
304
|
+
"""
|
|
305
|
+
if current_id not in self._issue_map:
|
|
306
|
+
return depth
|
|
307
|
+
|
|
308
|
+
if current_id in visited:
|
|
309
|
+
return depth # Cycle detected, return current depth
|
|
310
|
+
|
|
311
|
+
visited.add(current_id)
|
|
312
|
+
issue = self._issue_map[current_id]
|
|
313
|
+
|
|
314
|
+
if not issue.depends_on:
|
|
315
|
+
return depth # No further dependencies
|
|
316
|
+
|
|
317
|
+
# Find the maximum depth through any dependency
|
|
318
|
+
max_depth = depth
|
|
319
|
+
for dep_id in issue.depends_on:
|
|
320
|
+
depth_through_dep = self._find_max_chain_depth(
|
|
321
|
+
dep_id, visited.copy(), depth + 1
|
|
322
|
+
)
|
|
323
|
+
max_depth = max(max_depth, depth_through_dep)
|
|
324
|
+
|
|
325
|
+
return max_depth
|
|
326
|
+
|
|
327
|
+
@traced("get_issues_affecting")
|
|
328
|
+
def get_issues_affecting(self, issue_id: str) -> list[str]:
|
|
329
|
+
"""Get all issues that this one affects through dependency chain.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
issue_id: The issue to start from
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
List of issue IDs that would be affected if this issue changes
|
|
336
|
+
"""
|
|
337
|
+
if issue_id not in self._issue_map:
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
affected = set()
|
|
341
|
+
self._collect_affected(issue_id, affected)
|
|
342
|
+
return sorted(affected)
|
|
343
|
+
|
|
344
|
+
def _collect_affected(self, issue_id: str, affected: set[str]):
|
|
345
|
+
"""Recursively collect all issues affected by this one.
|
|
346
|
+
|
|
347
|
+
An issue is affected if this one blocks it (directly or indirectly).
|
|
348
|
+
"""
|
|
349
|
+
if not self._analyzed:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
# Find all issues that depend on this one (it affects them)
|
|
353
|
+
if issue_id in self._issue_map:
|
|
354
|
+
issue = self._issue_map[issue_id]
|
|
355
|
+
for blocked_id in issue.blocks or []:
|
|
356
|
+
if blocked_id in self._issue_map:
|
|
357
|
+
if blocked_id not in affected:
|
|
358
|
+
affected.add(blocked_id)
|
|
359
|
+
# Recursively get issues affected by the blocked issue
|
|
360
|
+
self._collect_affected(blocked_id, affected)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Detects field-level conflicts between local and remote issues.
|
|
2
|
+
|
|
3
|
+
Extracted from SyncStateComparator to separate conflict detection logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from structlog import get_logger
|
|
9
|
+
|
|
10
|
+
from roadmap.core.domain.issue import Issue
|
|
11
|
+
from roadmap.core.services.sync.sync_conflict_resolver import ConflictField
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FieldConflictDetector:
|
|
17
|
+
"""Detects field-level conflicts between local and remote issues."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, fields_to_sync: list[str] | None = None):
|
|
20
|
+
"""Initialize with fields to check for conflicts.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
fields_to_sync: List of field names to check for conflicts
|
|
24
|
+
"""
|
|
25
|
+
self.fields_to_sync = fields_to_sync or [
|
|
26
|
+
"status",
|
|
27
|
+
"priority",
|
|
28
|
+
"content",
|
|
29
|
+
"labels",
|
|
30
|
+
"assignee",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def detect_field_conflicts(
|
|
34
|
+
self, local_issue: Issue, remote_issue: Any, extractors: dict | None = None
|
|
35
|
+
) -> list[ConflictField]:
|
|
36
|
+
"""Detect which fields differ between local and remote issues.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
local_issue: Local Issue object
|
|
40
|
+
remote_issue: Remote issue object or dict
|
|
41
|
+
extractors: Optional dict of field→extractor function mappings
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of ConflictField objects for conflicting fields
|
|
45
|
+
"""
|
|
46
|
+
conflicting_fields = []
|
|
47
|
+
|
|
48
|
+
if extractors is None:
|
|
49
|
+
extractors = {}
|
|
50
|
+
|
|
51
|
+
for field in self.fields_to_sync:
|
|
52
|
+
try:
|
|
53
|
+
# Get local value
|
|
54
|
+
local_value = self._get_field_value(local_issue, field)
|
|
55
|
+
|
|
56
|
+
# Get remote value
|
|
57
|
+
remote_value = self._get_field_value(remote_issue, field)
|
|
58
|
+
|
|
59
|
+
# Check for conflict
|
|
60
|
+
if local_value != remote_value:
|
|
61
|
+
conflict_field = ConflictField(
|
|
62
|
+
field_name=field,
|
|
63
|
+
local_value=local_value,
|
|
64
|
+
remote_value=remote_value,
|
|
65
|
+
local_updated=getattr(local_issue, "updated", None),
|
|
66
|
+
remote_updated=getattr(remote_issue, "updated_at", None),
|
|
67
|
+
)
|
|
68
|
+
conflicting_fields.append(conflict_field)
|
|
69
|
+
logger.debug(
|
|
70
|
+
"conflict_detected_in_field",
|
|
71
|
+
field=field,
|
|
72
|
+
local_value=str(local_value)[:100],
|
|
73
|
+
remote_value=str(remote_value)[:100],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"conflict_detection_error",
|
|
79
|
+
field=field,
|
|
80
|
+
error=str(e),
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
return conflicting_fields
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _get_field_value(obj: Any, field: str) -> Any:
|
|
88
|
+
"""Get field value from object (handles dict and objects).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
obj: Object or dict to extract field from
|
|
92
|
+
field: Field name
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Field value, or None if not found
|
|
96
|
+
"""
|
|
97
|
+
if obj is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
if isinstance(obj, dict):
|
|
102
|
+
return obj.get(field)
|
|
103
|
+
|
|
104
|
+
# Handle enums (e.g., Status enum)
|
|
105
|
+
value = getattr(obj, field, None)
|
|
106
|
+
if value is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
# If it's an enum with .value, use that
|
|
110
|
+
if hasattr(value, "value"):
|
|
111
|
+
return value.value
|
|
112
|
+
|
|
113
|
+
# Handle special cases
|
|
114
|
+
if field == "labels" and isinstance(value, list):
|
|
115
|
+
return sorted(value) if value else []
|
|
116
|
+
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug(
|
|
121
|
+
"normalize_field_value_failed",
|
|
122
|
+
operation="normalize_field_value",
|
|
123
|
+
error=str(e),
|
|
124
|
+
action="Returning None",
|
|
125
|
+
)
|
|
126
|
+
return None
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Remote data fetching with retry logic and request management."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from structlog import get_logger
|
|
8
|
+
|
|
9
|
+
from roadmap.core.services.utils.retry_policy import RetryPolicy
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RemoteFetcher:
|
|
15
|
+
"""Utility for fetching remote issue data via an adapter.
|
|
16
|
+
|
|
17
|
+
Adds retry/backoff logic and basic rate-limit handling to adapter calls.
|
|
18
|
+
Prefer batch APIs (`pull_issues`) when available; fall back to
|
|
19
|
+
per-item APIs (`pull_issue`) otherwise.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
DEFAULT_RETRY_POLICY = RetryPolicy(max_retries=3, base_delay=0.5, factor=2.0)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _handle_rate_limit_from_response(resp: Any) -> float:
|
|
26
|
+
"""Inspect a response-like object for rate-limit headers and return sleep seconds.
|
|
27
|
+
|
|
28
|
+
Returns 0.0 if no rate-limit sleep is required. If the response exposes
|
|
29
|
+
`headers` or `raw_response` with `X-RateLimit-Remaining` and
|
|
30
|
+
`X-RateLimit-Reset`, this will compute the seconds until reset.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
from collections.abc import Mapping
|
|
34
|
+
|
|
35
|
+
headers = None
|
|
36
|
+
if isinstance(resp, Mapping):
|
|
37
|
+
headers = resp.get("headers")
|
|
38
|
+
else:
|
|
39
|
+
headers = getattr(resp, "headers", None)
|
|
40
|
+
if headers is None:
|
|
41
|
+
raw = getattr(resp, "raw_response", None)
|
|
42
|
+
if isinstance(raw, Mapping):
|
|
43
|
+
headers = raw.get("headers")
|
|
44
|
+
else:
|
|
45
|
+
headers = getattr(raw, "headers", None)
|
|
46
|
+
|
|
47
|
+
if not headers or not isinstance(headers, Mapping):
|
|
48
|
+
return 0.0
|
|
49
|
+
|
|
50
|
+
remaining = headers.get("X-RateLimit-Remaining") or headers.get(
|
|
51
|
+
"x-ratelimit-remaining"
|
|
52
|
+
)
|
|
53
|
+
reset = headers.get("X-RateLimit-Reset") or headers.get("x-ratelimit-reset")
|
|
54
|
+
if remaining is not None and str(remaining) == "0" and reset:
|
|
55
|
+
try:
|
|
56
|
+
reset_ts = int(reset)
|
|
57
|
+
now_ts = int(time.time())
|
|
58
|
+
wait = max(0, reset_ts - now_ts)
|
|
59
|
+
return float(wait)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.debug(
|
|
62
|
+
"reset_time_parsing_failed",
|
|
63
|
+
operation="parse_reset_time",
|
|
64
|
+
reset_value=str(reset),
|
|
65
|
+
error=str(e),
|
|
66
|
+
action="Using default wait time 0",
|
|
67
|
+
)
|
|
68
|
+
return 0.0
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.debug(
|
|
71
|
+
"rate_limit_check_failed",
|
|
72
|
+
operation="check_rate_limit",
|
|
73
|
+
error=str(e),
|
|
74
|
+
action="Assuming no rate limit",
|
|
75
|
+
)
|
|
76
|
+
return 0.0
|
|
77
|
+
return 0.0
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def fetch_issue(
|
|
81
|
+
adapter: Any, issue_id: str, retry_policy: RetryPolicy | None = None
|
|
82
|
+
) -> Any:
|
|
83
|
+
"""Fetch a single issue from remote adapter with retry logic.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
adapter: Remote adapter instance with pull_issue method.
|
|
87
|
+
issue_id: The issue ID to fetch.
|
|
88
|
+
retry_policy: Optional custom retry policy.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The fetched issue or None if fetch fails.
|
|
92
|
+
"""
|
|
93
|
+
if adapter is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
rp = retry_policy or RemoteFetcher.DEFAULT_RETRY_POLICY
|
|
97
|
+
|
|
98
|
+
for attempt in range(1, rp.max_retries + 1):
|
|
99
|
+
try:
|
|
100
|
+
if hasattr(adapter, "pull_issue"):
|
|
101
|
+
result = adapter.pull_issue(issue_id)
|
|
102
|
+
# Check for rate-limit hints
|
|
103
|
+
sleep_for = RemoteFetcher._handle_rate_limit_from_response(result)
|
|
104
|
+
if sleep_for and sleep_for > 0:
|
|
105
|
+
logger.info(
|
|
106
|
+
"remote_rate_limited",
|
|
107
|
+
issue_id=issue_id,
|
|
108
|
+
wait_seconds=sleep_for,
|
|
109
|
+
)
|
|
110
|
+
time.sleep(sleep_for)
|
|
111
|
+
return result
|
|
112
|
+
return None
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"remote_fetch_issue_error",
|
|
116
|
+
issue_id=issue_id,
|
|
117
|
+
attempt=attempt,
|
|
118
|
+
error=str(e),
|
|
119
|
+
severity="infrastructure",
|
|
120
|
+
)
|
|
121
|
+
if attempt == rp.max_retries:
|
|
122
|
+
logger.error(
|
|
123
|
+
"remote_fetch_issue_failed",
|
|
124
|
+
issue_id=issue_id,
|
|
125
|
+
attempts=attempt,
|
|
126
|
+
severity="infrastructure",
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
# Backoff
|
|
130
|
+
delay = rp.get_backoff_delay(attempt)
|
|
131
|
+
logger.info(
|
|
132
|
+
"remote_fetch_retry",
|
|
133
|
+
issue_id=issue_id,
|
|
134
|
+
attempt=attempt + 1,
|
|
135
|
+
delay=delay,
|
|
136
|
+
)
|
|
137
|
+
time.sleep(delay)
|
|
138
|
+
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def fetch_issues(
|
|
143
|
+
adapter: Any, issue_ids: Iterable[str], retry_policy: RetryPolicy | None = None
|
|
144
|
+
) -> list[Any]:
|
|
145
|
+
"""Fetch multiple issues from remote adapter with retry logic.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
adapter: Remote adapter instance with pull_issue method.
|
|
149
|
+
issue_ids: Iterable of issue IDs to fetch.
|
|
150
|
+
retry_policy: Optional custom retry policy.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of fetched issues.
|
|
154
|
+
"""
|
|
155
|
+
if adapter is None:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
rp = retry_policy or RemoteFetcher.DEFAULT_RETRY_POLICY
|
|
159
|
+
|
|
160
|
+
ids = list(issue_ids)
|
|
161
|
+
if not ids:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
# Try batch API first with retries
|
|
165
|
+
if hasattr(adapter, "pull_issues"):
|
|
166
|
+
for attempt in range(1, rp.max_retries + 1):
|
|
167
|
+
try:
|
|
168
|
+
result = adapter.pull_issues(ids)
|
|
169
|
+
# If adapter returns report-like object, inspect for rate-limit
|
|
170
|
+
sleep_for = RemoteFetcher._handle_rate_limit_from_response(result)
|
|
171
|
+
if sleep_for and sleep_for > 0:
|
|
172
|
+
logger.info(
|
|
173
|
+
"remote_rate_limited_batch",
|
|
174
|
+
count=len(ids),
|
|
175
|
+
wait_seconds=sleep_for,
|
|
176
|
+
)
|
|
177
|
+
time.sleep(sleep_for)
|
|
178
|
+
return result or []
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(
|
|
181
|
+
"remote_fetch_issues_error",
|
|
182
|
+
attempt=attempt,
|
|
183
|
+
error=str(e),
|
|
184
|
+
severity="infrastructure",
|
|
185
|
+
)
|
|
186
|
+
if attempt == rp.max_retries:
|
|
187
|
+
logger.error(
|
|
188
|
+
"remote_fetch_issues_failed",
|
|
189
|
+
attempts=attempt,
|
|
190
|
+
severity="infrastructure",
|
|
191
|
+
)
|
|
192
|
+
break
|
|
193
|
+
delay = rp.get_backoff_delay(attempt)
|
|
194
|
+
logger.info("remote_fetch_retry", attempt=attempt + 1, delay=delay)
|
|
195
|
+
time.sleep(delay)
|
|
196
|
+
|
|
197
|
+
# Fallback to individual calls with retries per-item
|
|
198
|
+
results: list[Any] = []
|
|
199
|
+
for i in ids:
|
|
200
|
+
res = RemoteFetcher.fetch_issue(adapter, i, retry_policy=rp)
|
|
201
|
+
results.append(res)
|
|
202
|
+
return results
|