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,178 @@
|
|
|
1
|
+
"""Three-way merge logic for intelligent sync conflict resolution.
|
|
2
|
+
|
|
3
|
+
Implements the core algorithm: base vs local vs remote comparison
|
|
4
|
+
to automatically resolve most conflicts without data loss.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MergeStatus(StrEnum):
|
|
13
|
+
"""Result of merging a field."""
|
|
14
|
+
|
|
15
|
+
CLEAN = "clean" # Auto-resolved, no conflict
|
|
16
|
+
CONFLICT = "conflict" # Both sides changed differently
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FieldMergeResult:
|
|
21
|
+
"""Result of merging a single field."""
|
|
22
|
+
|
|
23
|
+
value: Any
|
|
24
|
+
status: MergeStatus
|
|
25
|
+
reason: str # Why this decision was made
|
|
26
|
+
|
|
27
|
+
def is_conflict(self) -> bool:
|
|
28
|
+
"""Check if this is a conflict."""
|
|
29
|
+
return self.status == MergeStatus.CONFLICT
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ThreeWayMerger:
|
|
33
|
+
"""Implements three-way merge logic.
|
|
34
|
+
|
|
35
|
+
Given base, local, and remote versions of a field, intelligently
|
|
36
|
+
determines the merged value using this algorithm:
|
|
37
|
+
|
|
38
|
+
- If only local changed: take local
|
|
39
|
+
- If only remote changed: take remote
|
|
40
|
+
- If both changed the same way: take either (no conflict)
|
|
41
|
+
- If neither changed: take either (no conflict)
|
|
42
|
+
- If both changed differently: CONFLICT (needs resolution)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def merge_field(
|
|
46
|
+
self,
|
|
47
|
+
field_name: str,
|
|
48
|
+
base: Any,
|
|
49
|
+
local: Any,
|
|
50
|
+
remote: Any,
|
|
51
|
+
) -> FieldMergeResult:
|
|
52
|
+
"""Merge a single field using three-way logic.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
field_name: Name of the field (for debugging)
|
|
56
|
+
base: The agreed-upon value from last sync
|
|
57
|
+
local: Current local value
|
|
58
|
+
remote: Current remote value
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
FieldMergeResult with merged value and status
|
|
62
|
+
"""
|
|
63
|
+
# Case 1: Neither changed (everything stayed at base)
|
|
64
|
+
if local == base and remote == base:
|
|
65
|
+
return FieldMergeResult(
|
|
66
|
+
value=local,
|
|
67
|
+
status=MergeStatus.CLEAN,
|
|
68
|
+
reason=f"{field_name}: no changes",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Case 2: Only local changed (remote stayed at base)
|
|
72
|
+
if local != base and remote == base:
|
|
73
|
+
return FieldMergeResult(
|
|
74
|
+
value=local,
|
|
75
|
+
status=MergeStatus.CLEAN,
|
|
76
|
+
reason=f"{field_name}: only local changed",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Case 3: Only remote changed (local stayed at base)
|
|
80
|
+
if remote != base and local == base:
|
|
81
|
+
return FieldMergeResult(
|
|
82
|
+
value=remote,
|
|
83
|
+
status=MergeStatus.CLEAN,
|
|
84
|
+
reason=f"{field_name}: only remote changed",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Case 4: Both changed, but to the same value
|
|
88
|
+
if local == remote:
|
|
89
|
+
return FieldMergeResult(
|
|
90
|
+
value=local,
|
|
91
|
+
status=MergeStatus.CLEAN,
|
|
92
|
+
reason=f"{field_name}: both changed to same value",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Case 5: Both changed differently - TRUE CONFLICT
|
|
96
|
+
return FieldMergeResult(
|
|
97
|
+
value=None, # Can't auto-resolve
|
|
98
|
+
status=MergeStatus.CONFLICT,
|
|
99
|
+
reason=f"{field_name}: both sides changed differently (base={base}, local={local}, remote={remote})",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def merge_issue(
|
|
103
|
+
self,
|
|
104
|
+
issue_id: str,
|
|
105
|
+
base: dict[str, Any],
|
|
106
|
+
local: dict[str, Any],
|
|
107
|
+
remote: dict[str, Any],
|
|
108
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
109
|
+
"""Merge all fields of an issue.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
(merged_fields, list_of_conflicted_field_names)
|
|
113
|
+
|
|
114
|
+
Note: Conflicted fields are NOT included in merged_fields.
|
|
115
|
+
They must be resolved separately by ConflictResolver.
|
|
116
|
+
"""
|
|
117
|
+
merged = {}
|
|
118
|
+
conflicts = []
|
|
119
|
+
|
|
120
|
+
# Get all field names from all three versions
|
|
121
|
+
all_fields = set(base.keys()) | set(local.keys()) | set(remote.keys())
|
|
122
|
+
|
|
123
|
+
for field in all_fields:
|
|
124
|
+
result = self.merge_field(
|
|
125
|
+
field,
|
|
126
|
+
base.get(field),
|
|
127
|
+
local.get(field),
|
|
128
|
+
remote.get(field),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if result.is_conflict():
|
|
132
|
+
conflicts.append(field)
|
|
133
|
+
# Don't include conflicted fields in merged
|
|
134
|
+
# They'll be handled by ConflictResolver
|
|
135
|
+
else:
|
|
136
|
+
merged[field] = result.value
|
|
137
|
+
|
|
138
|
+
return merged, conflicts
|
|
139
|
+
|
|
140
|
+
def merge_issues(
|
|
141
|
+
self,
|
|
142
|
+
issues: dict[str, dict[str, Any]],
|
|
143
|
+
base_issues: dict[str, dict[str, Any]],
|
|
144
|
+
local_issues: dict[str, dict[str, Any]],
|
|
145
|
+
remote_issues: dict[str, dict[str, Any]],
|
|
146
|
+
) -> tuple[dict[str, tuple[dict, list[str]]], list[str]]:
|
|
147
|
+
"""Merge multiple issues.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
(issue_results, deleted_issues)
|
|
151
|
+
|
|
152
|
+
where issue_results[issue_id] = (merged_fields, conflict_fields)
|
|
153
|
+
and deleted_issues is a list of issues removed on remote
|
|
154
|
+
"""
|
|
155
|
+
results = {}
|
|
156
|
+
deleted = []
|
|
157
|
+
|
|
158
|
+
# Get all issue IDs from all three versions
|
|
159
|
+
all_issues = (
|
|
160
|
+
set(base_issues.keys())
|
|
161
|
+
| set(local_issues.keys())
|
|
162
|
+
| set(remote_issues.keys())
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
for issue_id in all_issues:
|
|
166
|
+
base = base_issues.get(issue_id, {})
|
|
167
|
+
local = local_issues.get(issue_id, {})
|
|
168
|
+
remote = remote_issues.get(issue_id, {})
|
|
169
|
+
|
|
170
|
+
# If issue was deleted remotely and not modified locally, skip it
|
|
171
|
+
if not remote and base and not local:
|
|
172
|
+
deleted.append(issue_id)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
merged, conflicts = self.merge_issue(issue_id, base, local, remote)
|
|
176
|
+
results[issue_id] = (merged, conflicts)
|
|
177
|
+
|
|
178
|
+
return results, deleted
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization file."""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Configuration management service for roadmap application.
|
|
2
|
+
|
|
3
|
+
Consolidates settings and credentials management into a single service layer,
|
|
4
|
+
providing unified access to application configuration and secrets.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from roadmap.core.interfaces import CredentialProvider
|
|
13
|
+
from roadmap.settings import settings
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigurationService:
|
|
19
|
+
"""Service for managing application configuration and credentials."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, credential_provider: CredentialProvider | None = None):
|
|
22
|
+
"""Initialize configuration service.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
credential_provider: Optional credential provider implementation.
|
|
26
|
+
If None, defaults to infrastructure CredentialManager.
|
|
27
|
+
"""
|
|
28
|
+
if credential_provider is None:
|
|
29
|
+
from roadmap.infrastructure.security.credentials import CredentialManager
|
|
30
|
+
|
|
31
|
+
credential_provider = CredentialManager()
|
|
32
|
+
|
|
33
|
+
self.credential_manager = credential_provider
|
|
34
|
+
self._settings = settings
|
|
35
|
+
|
|
36
|
+
# ==================== Configuration Access ====================
|
|
37
|
+
|
|
38
|
+
def get_setting(self, key: str, default: Any = None) -> Any:
|
|
39
|
+
"""Get a configuration setting by key.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key: Configuration key (dot notation supported, e.g. 'database.path')
|
|
43
|
+
default: Default value if setting not found
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Configuration value or default
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return self._settings.get(key, default)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.debug(
|
|
52
|
+
"config_get_failed",
|
|
53
|
+
operation="get_config",
|
|
54
|
+
key=key,
|
|
55
|
+
error=str(e),
|
|
56
|
+
action="Returning default value",
|
|
57
|
+
)
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
def get_github_config(self) -> dict[str, Any]:
|
|
61
|
+
"""Get GitHub integration configuration.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
GitHub configuration dictionary
|
|
65
|
+
"""
|
|
66
|
+
return {
|
|
67
|
+
"enabled": self.get_setting("github.enabled", False),
|
|
68
|
+
"repo": self.get_setting("github.repo", ""),
|
|
69
|
+
"sync_labels": self.get_setting("github.sync_labels", True),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def get_sync_config(self) -> dict[str, Any]:
|
|
73
|
+
"""Get sync operation configuration.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Sync configuration dictionary including duplicate detection thresholds
|
|
77
|
+
"""
|
|
78
|
+
return {
|
|
79
|
+
"enable_duplicate_detection": self.get_setting(
|
|
80
|
+
"sync.enable_duplicate_detection", True
|
|
81
|
+
),
|
|
82
|
+
"duplicate_title_threshold": self.get_setting(
|
|
83
|
+
"sync.duplicate_title_threshold", 0.90
|
|
84
|
+
),
|
|
85
|
+
"duplicate_content_threshold": self.get_setting(
|
|
86
|
+
"sync.duplicate_content_threshold", 0.85
|
|
87
|
+
),
|
|
88
|
+
"duplicate_auto_resolve_threshold": self.get_setting(
|
|
89
|
+
"sync.duplicate_auto_resolve_threshold", 0.95
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# ==================== Credential Management ====================
|
|
94
|
+
|
|
95
|
+
def get_github_token(self) -> str | None:
|
|
96
|
+
"""Retrieve GitHub token securely.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
GitHub token if found, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
return self.credential_manager.get_token()
|
|
102
|
+
|
|
103
|
+
# ==================== Configuration Paths ====================
|
|
104
|
+
|
|
105
|
+
def get_config_paths(self) -> dict[str, Path]:
|
|
106
|
+
"""Get configuration file paths.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary of configuration file paths
|
|
110
|
+
"""
|
|
111
|
+
from roadmap.settings import get_config_paths
|
|
112
|
+
|
|
113
|
+
return get_config_paths()
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Critical path analysis for issue dependencies.
|
|
2
|
+
|
|
3
|
+
Calculates which issues are on the critical path and impact project timeline.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import UTC, datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from roadmap.core.domain.issue import Issue
|
|
10
|
+
from roadmap.core.domain.milestone import Milestone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PathNode:
|
|
15
|
+
"""Represents an issue in the critical path analysis."""
|
|
16
|
+
|
|
17
|
+
issue_id: str
|
|
18
|
+
issue_title: str
|
|
19
|
+
duration_hours: float
|
|
20
|
+
dependencies: list[str] = field(default_factory=list)
|
|
21
|
+
start_date: datetime | None = None
|
|
22
|
+
end_date: datetime | None = None
|
|
23
|
+
slack_time: float = 0.0 # Time available before blocking other tasks
|
|
24
|
+
is_critical: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CriticalPathResult:
|
|
29
|
+
"""Results from critical path analysis."""
|
|
30
|
+
|
|
31
|
+
critical_path: list[PathNode]
|
|
32
|
+
total_duration: float
|
|
33
|
+
critical_issue_ids: list[str]
|
|
34
|
+
blocking_issues: dict[str, list[str]] # issue_id -> list of blocked issue_ids
|
|
35
|
+
project_end_date: datetime | None = None
|
|
36
|
+
issues_by_criticality: dict[str, list[str]] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CriticalPathCalculator:
|
|
40
|
+
"""Calculates critical path for issues based on dependencies."""
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
"""Initialize calculator."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def calculate_critical_path(
|
|
47
|
+
self, issues: list[Issue], milestone: Milestone | None = None
|
|
48
|
+
) -> CriticalPathResult:
|
|
49
|
+
"""Calculate critical path from issues with dependencies.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
issues: List of issues to analyze
|
|
53
|
+
milestone: Optional milestone context for date calculations
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
CriticalPathResult with critical path information
|
|
57
|
+
"""
|
|
58
|
+
if not issues:
|
|
59
|
+
return CriticalPathResult(
|
|
60
|
+
critical_path=[],
|
|
61
|
+
total_duration=0.0,
|
|
62
|
+
critical_issue_ids=[],
|
|
63
|
+
blocking_issues={},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Build dependency graph
|
|
67
|
+
graph = self._build_dependency_graph(issues)
|
|
68
|
+
|
|
69
|
+
# Calculate earliest start/end dates
|
|
70
|
+
node_data = self._calculate_schedule(issues, graph)
|
|
71
|
+
|
|
72
|
+
# Find critical path
|
|
73
|
+
critical_path = self._find_critical_path(node_data, graph)
|
|
74
|
+
|
|
75
|
+
# Analyze blocking relationships
|
|
76
|
+
blocking_issues = self._analyze_blocking(issues)
|
|
77
|
+
|
|
78
|
+
# Calculate slack time
|
|
79
|
+
self._calculate_slack(node_data, critical_path)
|
|
80
|
+
|
|
81
|
+
# Determine project end date
|
|
82
|
+
project_end_date = self._calculate_project_end_date(node_data, milestone)
|
|
83
|
+
|
|
84
|
+
return CriticalPathResult(
|
|
85
|
+
critical_path=critical_path,
|
|
86
|
+
total_duration=sum(node.duration_hours for node in critical_path),
|
|
87
|
+
critical_issue_ids=[node.issue_id for node in critical_path],
|
|
88
|
+
blocking_issues=blocking_issues,
|
|
89
|
+
project_end_date=project_end_date,
|
|
90
|
+
issues_by_criticality=self._group_by_criticality(issues, critical_path),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _build_dependency_graph(self, issues: list[Issue]) -> dict[str, list[str]]:
|
|
94
|
+
"""Build dependency graph from issues.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
issues: List of issues
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Dictionary mapping issue_id to list of dependent issue_ids
|
|
101
|
+
"""
|
|
102
|
+
graph = {issue.id: issue.depends_on or [] for issue in issues}
|
|
103
|
+
return graph
|
|
104
|
+
|
|
105
|
+
def _calculate_schedule(
|
|
106
|
+
self, issues: list[Issue], graph: dict[str, list[str]]
|
|
107
|
+
) -> dict[str, PathNode]:
|
|
108
|
+
"""Calculate earliest start and end times.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
issues: List of issues
|
|
112
|
+
graph: Dependency graph
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dictionary mapping issue_id to PathNode
|
|
116
|
+
"""
|
|
117
|
+
nodes = {}
|
|
118
|
+
for issue in issues:
|
|
119
|
+
duration = self._estimate_duration(issue)
|
|
120
|
+
nodes[issue.id] = PathNode(
|
|
121
|
+
issue_id=issue.id,
|
|
122
|
+
issue_title=issue.title,
|
|
123
|
+
duration_hours=duration,
|
|
124
|
+
dependencies=graph.get(issue.id, []),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Calculate earliest start/end times using topological sort
|
|
128
|
+
start_times = {}
|
|
129
|
+
visiting = set() # Track nodes currently being processed to detect cycles
|
|
130
|
+
|
|
131
|
+
def calculate_start_time(issue_id: str) -> float:
|
|
132
|
+
if issue_id in start_times:
|
|
133
|
+
return start_times[issue_id]
|
|
134
|
+
|
|
135
|
+
# Detect circular dependencies
|
|
136
|
+
if issue_id in visiting:
|
|
137
|
+
# Return 0 for circular refs (they shouldn't exist, but graceful fallback)
|
|
138
|
+
return 0.0
|
|
139
|
+
|
|
140
|
+
visiting.add(issue_id)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
dependencies = graph.get(issue_id, [])
|
|
144
|
+
if not dependencies:
|
|
145
|
+
start_times[issue_id] = 0.0
|
|
146
|
+
return 0.0
|
|
147
|
+
|
|
148
|
+
# Max end time of all dependencies
|
|
149
|
+
max_end_time = 0.0
|
|
150
|
+
for dep_id in dependencies:
|
|
151
|
+
if dep_id in nodes:
|
|
152
|
+
dep_start = calculate_start_time(dep_id)
|
|
153
|
+
dep_duration = nodes[dep_id].duration_hours
|
|
154
|
+
max_end_time = max(max_end_time, dep_start + dep_duration)
|
|
155
|
+
|
|
156
|
+
start_times[issue_id] = max_end_time
|
|
157
|
+
return max_end_time
|
|
158
|
+
finally:
|
|
159
|
+
visiting.discard(issue_id)
|
|
160
|
+
|
|
161
|
+
# Calculate all start times
|
|
162
|
+
for issue_id in nodes:
|
|
163
|
+
start_time = calculate_start_time(issue_id)
|
|
164
|
+
nodes[issue_id].start_date = datetime.now(UTC) + timedelta(hours=start_time)
|
|
165
|
+
nodes[issue_id].end_date = datetime.now(UTC) + timedelta(
|
|
166
|
+
hours=start_time + nodes[issue_id].duration_hours
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return nodes
|
|
170
|
+
|
|
171
|
+
def _find_critical_path(
|
|
172
|
+
self, nodes: dict[str, PathNode], graph: dict[str, list[str]]
|
|
173
|
+
) -> list[PathNode]:
|
|
174
|
+
"""Find the critical path (longest duration path through dependencies).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
nodes: Dictionary of nodes with schedule info
|
|
178
|
+
graph: Dependency graph
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of PathNode objects on critical path, ordered by dependency
|
|
182
|
+
"""
|
|
183
|
+
if not nodes:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
# Find leaf nodes (no blocks)
|
|
187
|
+
leaf_nodes = [
|
|
188
|
+
issue_id
|
|
189
|
+
for issue_id, _ in nodes.items()
|
|
190
|
+
if not any(issue_id in graph.get(other_id, []) for other_id in nodes)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if not leaf_nodes:
|
|
194
|
+
# If no clear leaf, use nodes with latest end dates
|
|
195
|
+
leaf_nodes = sorted(
|
|
196
|
+
nodes.keys(),
|
|
197
|
+
key=lambda x: nodes[x].end_date or datetime.now(UTC),
|
|
198
|
+
reverse=True,
|
|
199
|
+
)[:1]
|
|
200
|
+
|
|
201
|
+
# Trace back from leaf nodes to find longest path
|
|
202
|
+
critical_paths = []
|
|
203
|
+
|
|
204
|
+
for leaf_id in leaf_nodes:
|
|
205
|
+
path = self._trace_path_backward(leaf_id, nodes, graph)
|
|
206
|
+
critical_paths.append(path)
|
|
207
|
+
|
|
208
|
+
# Return longest path
|
|
209
|
+
critical_path = max(critical_paths, key=lambda p: len(p), default=[])
|
|
210
|
+
|
|
211
|
+
# Mark critical nodes
|
|
212
|
+
for node in critical_path:
|
|
213
|
+
node.is_critical = True
|
|
214
|
+
|
|
215
|
+
return critical_path
|
|
216
|
+
|
|
217
|
+
def _trace_path_backward(
|
|
218
|
+
self, issue_id: str, nodes: dict[str, PathNode], graph: dict[str, list[str]]
|
|
219
|
+
) -> list[PathNode]:
|
|
220
|
+
"""Trace path backward from a node to root dependencies.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
issue_id: Starting issue ID
|
|
224
|
+
nodes: Node dictionary
|
|
225
|
+
graph: Dependency graph
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of nodes in dependency chain
|
|
229
|
+
"""
|
|
230
|
+
path = []
|
|
231
|
+
current_id = issue_id
|
|
232
|
+
visited = set() # Track visited nodes to prevent infinite loops
|
|
233
|
+
|
|
234
|
+
while current_id in nodes:
|
|
235
|
+
# Detect circular dependencies
|
|
236
|
+
if current_id in visited:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
visited.add(current_id)
|
|
240
|
+
path.append(nodes[current_id])
|
|
241
|
+
dependencies = graph.get(current_id, [])
|
|
242
|
+
|
|
243
|
+
if not dependencies:
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
# Follow the dependency with longest duration
|
|
247
|
+
current_id = max(
|
|
248
|
+
dependencies,
|
|
249
|
+
key=lambda d: nodes.get(d, PathNode("", "", 0)).duration_hours,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return list(reversed(path))
|
|
253
|
+
|
|
254
|
+
def _estimate_duration(self, issue: Issue) -> float:
|
|
255
|
+
"""Estimate duration from issue estimated_hours field.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
issue: Issue to estimate
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Duration in hours
|
|
262
|
+
"""
|
|
263
|
+
if not issue.estimated_hours:
|
|
264
|
+
return 4.0 # Default 4 hours
|
|
265
|
+
|
|
266
|
+
return float(issue.estimated_hours)
|
|
267
|
+
|
|
268
|
+
def _calculate_slack(
|
|
269
|
+
self, nodes: dict[str, PathNode], critical_path: list[PathNode]
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Calculate slack time for non-critical paths.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
nodes: Dictionary of all nodes
|
|
275
|
+
critical_path: List of critical path nodes
|
|
276
|
+
"""
|
|
277
|
+
# Critical path nodes have no slack
|
|
278
|
+
critical_ids = {node.issue_id for node in critical_path}
|
|
279
|
+
|
|
280
|
+
if critical_path:
|
|
281
|
+
max_end_time = max(
|
|
282
|
+
(node.end_date or datetime.now(UTC)) for node in critical_path
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
for node_id, node in nodes.items():
|
|
286
|
+
if node_id not in critical_ids and node.end_date:
|
|
287
|
+
slack_hours = (max_end_time - node.end_date).total_seconds() / 3600
|
|
288
|
+
node.slack_time = max(0.0, slack_hours)
|
|
289
|
+
|
|
290
|
+
def _calculate_project_end_date(
|
|
291
|
+
self,
|
|
292
|
+
nodes: dict[str, PathNode],
|
|
293
|
+
milestone: Milestone | None = None,
|
|
294
|
+
) -> datetime | None:
|
|
295
|
+
"""Calculate expected project completion date.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
nodes: Dictionary of nodes
|
|
299
|
+
milestone: Optional milestone with due date
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Expected project end date
|
|
303
|
+
"""
|
|
304
|
+
if not nodes:
|
|
305
|
+
return milestone.due_date if milestone else None
|
|
306
|
+
|
|
307
|
+
# Project ends when all critical path items complete
|
|
308
|
+
latest_end = max(
|
|
309
|
+
(node.end_date for node in nodes.values() if node.end_date),
|
|
310
|
+
default=None,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if latest_end and milestone and milestone.due_date:
|
|
314
|
+
return min(latest_end, milestone.due_date)
|
|
315
|
+
|
|
316
|
+
return latest_end or (milestone.due_date if milestone else None)
|
|
317
|
+
|
|
318
|
+
def _analyze_blocking(self, issues: list[Issue]) -> dict[str, list[str]]:
|
|
319
|
+
"""Analyze which issues block others.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
issues: List of issues
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Dictionary mapping issue_id to list of blocked issue_ids
|
|
326
|
+
"""
|
|
327
|
+
blocking = {}
|
|
328
|
+
|
|
329
|
+
for issue in issues:
|
|
330
|
+
if issue.blocks:
|
|
331
|
+
blocking[issue.id] = issue.blocks
|
|
332
|
+
|
|
333
|
+
return blocking
|
|
334
|
+
|
|
335
|
+
def _group_by_criticality(
|
|
336
|
+
self, issues: list[Issue], critical_path: list[PathNode]
|
|
337
|
+
) -> dict[str, list[str]]:
|
|
338
|
+
"""Group issues by criticality level.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
issues: List of all issues
|
|
342
|
+
critical_path: List of critical path nodes
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dictionary with criticality groups
|
|
346
|
+
"""
|
|
347
|
+
critical_ids = {node.issue_id for node in critical_path}
|
|
348
|
+
blocking_issues = set()
|
|
349
|
+
blocked_issues = set()
|
|
350
|
+
|
|
351
|
+
for issue in issues:
|
|
352
|
+
if issue.blocks:
|
|
353
|
+
blocking_issues.add(issue.id)
|
|
354
|
+
if issue.depends_on:
|
|
355
|
+
blocked_issues.add(issue.id)
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"critical": list(critical_ids),
|
|
359
|
+
"blocking": list(blocking_issues),
|
|
360
|
+
"blocked": list(blocked_issues),
|
|
361
|
+
"independent": [
|
|
362
|
+
issue.id
|
|
363
|
+
for issue in issues
|
|
364
|
+
if issue.id not in critical_ids
|
|
365
|
+
and issue.id not in blocking_issues
|
|
366
|
+
and issue.id not in blocked_issues
|
|
367
|
+
],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
def find_blocking_issues(
|
|
371
|
+
self, issues: list[Issue], target_issue_id: str
|
|
372
|
+
) -> list[Issue]:
|
|
373
|
+
"""Find all issues blocking a specific issue.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
issues: List of all issues
|
|
377
|
+
target_issue_id: ID of issue to check
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
List of issues blocking the target
|
|
381
|
+
"""
|
|
382
|
+
issue_map = {issue.id: issue for issue in issues}
|
|
383
|
+
target = issue_map.get(target_issue_id)
|
|
384
|
+
|
|
385
|
+
if not target or not target.depends_on:
|
|
386
|
+
return []
|
|
387
|
+
|
|
388
|
+
blocking = []
|
|
389
|
+
for dep_id in target.depends_on:
|
|
390
|
+
if dep_id in issue_map:
|
|
391
|
+
blocking.append(issue_map[dep_id])
|
|
392
|
+
|
|
393
|
+
return blocking
|
|
394
|
+
|
|
395
|
+
def find_blocked_issues(
|
|
396
|
+
self, issues: list[Issue], blocker_issue_id: str
|
|
397
|
+
) -> list[Issue]:
|
|
398
|
+
"""Find all issues blocked by a specific issue.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
issues: List of all issues
|
|
402
|
+
blocker_issue_id: ID of blocking issue
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of issues blocked by the blocker
|
|
406
|
+
"""
|
|
407
|
+
issue_map = {issue.id: issue for issue in issues}
|
|
408
|
+
blocker = issue_map.get(blocker_issue_id)
|
|
409
|
+
|
|
410
|
+
if not blocker or not blocker.blocks:
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
blocked = []
|
|
414
|
+
for blocked_id in blocker.blocks:
|
|
415
|
+
if blocked_id in issue_map:
|
|
416
|
+
blocked.append(issue_map[blocked_id])
|
|
417
|
+
|
|
418
|
+
return blocked
|