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,284 @@
|
|
|
1
|
+
"""Baseline state retrieval using git history and YAML metadata.
|
|
2
|
+
|
|
3
|
+
This module replaces the previous DB-based baseline approach with:
|
|
4
|
+
1. Local baseline: Retrieved from git history at last_synced timestamp
|
|
5
|
+
2. Remote baseline: Stored in sync_metadata YAML frontmatter
|
|
6
|
+
|
|
7
|
+
This enables idempotent syncs without complex database state management.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from structlog import get_logger
|
|
15
|
+
|
|
16
|
+
from roadmap.core.interfaces.persistence import (
|
|
17
|
+
FileNotFound,
|
|
18
|
+
GitHistoryError,
|
|
19
|
+
PersistenceInterface,
|
|
20
|
+
)
|
|
21
|
+
from roadmap.core.services.sync.sync_state import IssueBaseState
|
|
22
|
+
from roadmap.infrastructure.persistence_gateway import PersistenceGateway
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaselineRetrievalError(Exception):
|
|
28
|
+
"""Raised when baseline retrieval fails."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaselineStateRetriever:
|
|
34
|
+
"""Retrieves baseline state from git history and YAML metadata."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
issues_dir: Path,
|
|
39
|
+
persistence: PersistenceInterface,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize with issues directory and persistence interface.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
issues_dir: Path to issues directory
|
|
45
|
+
persistence: Interface for git history access
|
|
46
|
+
"""
|
|
47
|
+
self.issues_dir = issues_dir
|
|
48
|
+
self.persistence = persistence
|
|
49
|
+
|
|
50
|
+
def get_baseline_from_file(self, issue_file: Path) -> IssueBaseState | None:
|
|
51
|
+
"""Get baseline state directly from current issue file.
|
|
52
|
+
|
|
53
|
+
Used for initial baseline creation on first sync, where we extract
|
|
54
|
+
the current state directly without looking in git history.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
issue_file: Path to issue file
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
IssueBaseState extracted from file, or None if extraction fails
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
if not issue_file.exists():
|
|
64
|
+
logger.debug(
|
|
65
|
+
"baseline_file_not_found",
|
|
66
|
+
issue_file=str(issue_file),
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
content = issue_file.read_text(encoding="utf-8")
|
|
71
|
+
return self._extract_baseline_from_content(content, issue_file)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning(
|
|
74
|
+
"baseline_from_file_extraction_error",
|
|
75
|
+
issue_file=str(issue_file),
|
|
76
|
+
error=str(e),
|
|
77
|
+
severity="operational",
|
|
78
|
+
)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def get_local_baseline(
|
|
82
|
+
self, issue_file: Path, last_synced: datetime
|
|
83
|
+
) -> IssueBaseState | None:
|
|
84
|
+
"""Get local baseline state from git history.
|
|
85
|
+
|
|
86
|
+
Retrieves the issue file as it existed at the last_synced timestamp,
|
|
87
|
+
then extracts the relevant fields to create a baseline.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
issue_file: Path to issue file
|
|
91
|
+
last_synced: Timestamp of last successful sync
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
IssueBaseState if found, None if file didn't exist at that time
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Get file content as it existed at last_synced time
|
|
98
|
+
file_content = self.persistence.get_file_at_timestamp(
|
|
99
|
+
str(issue_file), last_synced
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not file_content:
|
|
103
|
+
logger.debug(
|
|
104
|
+
"local_baseline_file_not_found",
|
|
105
|
+
issue_file=str(issue_file),
|
|
106
|
+
last_synced=last_synced.isoformat(),
|
|
107
|
+
)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# Parse the file to extract baseline fields
|
|
111
|
+
baseline = self._extract_baseline_from_content(file_content, issue_file)
|
|
112
|
+
|
|
113
|
+
logger.debug(
|
|
114
|
+
"local_baseline_retrieved",
|
|
115
|
+
issue_file=str(issue_file),
|
|
116
|
+
status=baseline.status if baseline else None,
|
|
117
|
+
)
|
|
118
|
+
return baseline
|
|
119
|
+
|
|
120
|
+
except FileNotFound:
|
|
121
|
+
logger.debug(
|
|
122
|
+
"local_baseline_file_not_in_history",
|
|
123
|
+
issue_file=str(issue_file),
|
|
124
|
+
last_synced=last_synced.isoformat(),
|
|
125
|
+
reason="file_did_not_exist_at_timestamp",
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
except GitHistoryError as e:
|
|
129
|
+
logger.warning(
|
|
130
|
+
"local_baseline_git_error",
|
|
131
|
+
issue_file=str(issue_file),
|
|
132
|
+
error=str(e),
|
|
133
|
+
severity="operational",
|
|
134
|
+
)
|
|
135
|
+
raise BaselineRetrievalError(
|
|
136
|
+
f"Failed to retrieve baseline from git: {e}"
|
|
137
|
+
) from e
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(
|
|
140
|
+
"local_baseline_extraction_error",
|
|
141
|
+
issue_file=str(issue_file),
|
|
142
|
+
error=str(e),
|
|
143
|
+
error_type=type(e).__name__,
|
|
144
|
+
severity="operational",
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def get_remote_baseline(self, issue_file: Path) -> IssueBaseState | None:
|
|
149
|
+
"""Get remote baseline from sync_metadata in issue file.
|
|
150
|
+
|
|
151
|
+
The sync_metadata YAML frontmatter contains a remote_state snapshot
|
|
152
|
+
that represents the last-synced state from the remote backend.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
issue_file: Path to issue file
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
IssueBaseState extracted from remote_state, or None if not present
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
sync_metadata = PersistenceGateway.load_sync_metadata(issue_file)
|
|
162
|
+
|
|
163
|
+
if not sync_metadata:
|
|
164
|
+
logger.debug(
|
|
165
|
+
"remote_baseline_not_found",
|
|
166
|
+
issue_file=str(issue_file),
|
|
167
|
+
reason="no_sync_metadata",
|
|
168
|
+
)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
remote_state = sync_metadata.get("remote_state")
|
|
172
|
+
if not remote_state:
|
|
173
|
+
logger.debug(
|
|
174
|
+
"remote_baseline_not_found",
|
|
175
|
+
issue_file=str(issue_file),
|
|
176
|
+
reason="no_remote_state_in_metadata",
|
|
177
|
+
)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
baseline = self._extract_baseline_from_remote_state(
|
|
181
|
+
remote_state, issue_file
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
logger.debug(
|
|
185
|
+
"remote_baseline_retrieved",
|
|
186
|
+
issue_file=str(issue_file),
|
|
187
|
+
status=baseline.status if baseline else None,
|
|
188
|
+
)
|
|
189
|
+
return baseline
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(
|
|
193
|
+
"remote_baseline_extraction_error",
|
|
194
|
+
issue_file=str(issue_file),
|
|
195
|
+
error=str(e),
|
|
196
|
+
error_type=type(e).__name__,
|
|
197
|
+
severity="operational",
|
|
198
|
+
)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def _extract_baseline_from_content(
|
|
202
|
+
self, content: str, issue_file: Path
|
|
203
|
+
) -> IssueBaseState | None:
|
|
204
|
+
"""Extract baseline fields from issue file content.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
content: File content (with YAML frontmatter)
|
|
208
|
+
issue_file: Path to issue file (for logging)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
IssueBaseState or None if extraction fails
|
|
212
|
+
"""
|
|
213
|
+
from roadmap.common.datetime_parser import parse_datetime
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
frontmatter, _ = PersistenceGateway.parse_frontmatter(content)
|
|
217
|
+
|
|
218
|
+
# Parse updated datetime
|
|
219
|
+
updated_at = datetime.now(UTC)
|
|
220
|
+
if frontmatter.get("updated"):
|
|
221
|
+
updated_value = frontmatter["updated"]
|
|
222
|
+
if isinstance(updated_value, datetime):
|
|
223
|
+
updated_at = updated_value
|
|
224
|
+
else:
|
|
225
|
+
parsed = parse_datetime(str(updated_value), "file")
|
|
226
|
+
if parsed:
|
|
227
|
+
updated_at = parsed
|
|
228
|
+
|
|
229
|
+
return IssueBaseState(
|
|
230
|
+
id=frontmatter.get("id", ""),
|
|
231
|
+
title=frontmatter.get("title", ""),
|
|
232
|
+
status=frontmatter.get("status", "todo"),
|
|
233
|
+
assignee=frontmatter.get("assignee"),
|
|
234
|
+
headline=frontmatter.get("headline", ""),
|
|
235
|
+
content=frontmatter.get("content", ""),
|
|
236
|
+
labels=frontmatter.get("labels", []),
|
|
237
|
+
updated_at=updated_at,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.debug(
|
|
241
|
+
"baseline_extraction_failed",
|
|
242
|
+
issue_file=str(issue_file),
|
|
243
|
+
error=str(e),
|
|
244
|
+
)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def _extract_baseline_from_remote_state(
|
|
248
|
+
self, remote_state: dict[str, Any], issue_file: Path
|
|
249
|
+
) -> IssueBaseState | None:
|
|
250
|
+
"""Extract baseline from remote_state snapshot in sync_metadata.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
remote_state: Remote state dict from sync_metadata
|
|
254
|
+
issue_file: Path to issue file (for logging)
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
IssueBaseState or None if extraction fails
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
# Parse datetime if present
|
|
261
|
+
updated_at = datetime.now(UTC)
|
|
262
|
+
if "updated_at" in remote_state:
|
|
263
|
+
if isinstance(remote_state["updated_at"], str):
|
|
264
|
+
updated_at = datetime.fromisoformat(remote_state["updated_at"])
|
|
265
|
+
else:
|
|
266
|
+
updated_at = remote_state["updated_at"]
|
|
267
|
+
|
|
268
|
+
return IssueBaseState(
|
|
269
|
+
id=remote_state.get("id", ""),
|
|
270
|
+
title=remote_state.get("title", ""),
|
|
271
|
+
status=remote_state.get("status", "open"),
|
|
272
|
+
assignee=remote_state.get("assignee"),
|
|
273
|
+
headline=remote_state.get("headline", ""),
|
|
274
|
+
content=remote_state.get("content", ""),
|
|
275
|
+
labels=remote_state.get("labels", []),
|
|
276
|
+
updated_at=updated_at,
|
|
277
|
+
)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.debug(
|
|
280
|
+
"remote_baseline_extraction_failed",
|
|
281
|
+
issue_file=str(issue_file),
|
|
282
|
+
error=str(e),
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Optimized sync state rebuilding using changed file detection.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent caching strategies for sync state reconstruction,
|
|
4
|
+
using git history to detect which files have actually changed since last baseline.
|
|
5
|
+
This dramatically speeds up the rebuild process for large issue sets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from structlog import get_logger
|
|
11
|
+
|
|
12
|
+
from roadmap.core.interfaces.persistence import GitHistoryError
|
|
13
|
+
from roadmap.core.services.sync.sync_state import SyncState
|
|
14
|
+
from roadmap.infrastructure.persistence_gateway import PersistenceGateway
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OptimizedBaselineBuilder:
|
|
20
|
+
"""Builds sync state baselines with intelligent caching.
|
|
21
|
+
|
|
22
|
+
Instead of rebuilding the entire sync state from git history, this class:
|
|
23
|
+
1. Checks which files have changed since a reference point
|
|
24
|
+
2. Only rebuilds baselines for modified issues
|
|
25
|
+
3. Reuses cached baseline states for unchanged issues
|
|
26
|
+
4. Reduces rebuild time from ~50-100ms to ~5-10ms for typical changes
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, issues_dir: Path):
|
|
30
|
+
"""Initialize builder.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
issues_dir: Path to issues directory
|
|
34
|
+
"""
|
|
35
|
+
self.issues_dir = issues_dir
|
|
36
|
+
self._progress = None # Optional rich Progress context
|
|
37
|
+
|
|
38
|
+
def set_progress_context(self, progress) -> None:
|
|
39
|
+
"""Set optional progress tracker for rebuild operations.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
progress: rich.progress.Progress instance or None
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> from rich.progress import Progress
|
|
46
|
+
>>> with Progress() as progress:
|
|
47
|
+
... builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
|
|
48
|
+
... builder.set_progress_context(progress)
|
|
49
|
+
... # Rebuilds will now show progress
|
|
50
|
+
"""
|
|
51
|
+
self._progress = progress
|
|
52
|
+
|
|
53
|
+
def _update_progress(
|
|
54
|
+
self,
|
|
55
|
+
description: str,
|
|
56
|
+
completed: int | None = None,
|
|
57
|
+
total: int | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Update progress bar if tracking is enabled.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
description: What operation is being performed
|
|
63
|
+
completed: Number of items completed
|
|
64
|
+
total: Total items to complete
|
|
65
|
+
|
|
66
|
+
Internal use only.
|
|
67
|
+
"""
|
|
68
|
+
if self._progress is None:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Update task description if progress tracking is active
|
|
73
|
+
logger.debug(
|
|
74
|
+
"baseline_rebuild_progress",
|
|
75
|
+
description=description,
|
|
76
|
+
completed=completed,
|
|
77
|
+
total=total,
|
|
78
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.debug(
|
|
81
|
+
"progress_update_failed",
|
|
82
|
+
operation="report_progress",
|
|
83
|
+
error=str(e),
|
|
84
|
+
action="Continuing without progress update",
|
|
85
|
+
)
|
|
86
|
+
# Silently ignore progress update errors
|
|
87
|
+
|
|
88
|
+
def get_changed_issue_files(
|
|
89
|
+
self,
|
|
90
|
+
git_ref: str = "HEAD~1",
|
|
91
|
+
limit_to_issues: list[str] | None = None,
|
|
92
|
+
) -> set[str]:
|
|
93
|
+
"""Get issue files that changed since a git reference.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
git_ref: Git reference to compare against (default: previous commit)
|
|
97
|
+
limit_to_issues: Optional list of issue IDs to limit detection to
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Set of issue file paths that changed
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
>>> builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
|
|
104
|
+
>>> changed = builder.get_changed_issue_files("HEAD~1")
|
|
105
|
+
>>> print(changed) # {'.../TASK-123-example.md', ...}
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
changed = PersistenceGateway.get_changed_files_since_commit(
|
|
109
|
+
git_ref, str(self.issues_dir)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Filter to only issue files (*.md)
|
|
113
|
+
issue_files = {f for f in changed if f.endswith(".md")}
|
|
114
|
+
|
|
115
|
+
if limit_to_issues:
|
|
116
|
+
# Filter to specific issues
|
|
117
|
+
limited = {
|
|
118
|
+
f
|
|
119
|
+
for f in issue_files
|
|
120
|
+
if any(issue_id in f for issue_id in limit_to_issues)
|
|
121
|
+
}
|
|
122
|
+
logger.debug(
|
|
123
|
+
"changed_issue_files_filtered",
|
|
124
|
+
total_changed=len(issue_files),
|
|
125
|
+
limited_count=len(limited),
|
|
126
|
+
)
|
|
127
|
+
return limited
|
|
128
|
+
|
|
129
|
+
logger.debug(
|
|
130
|
+
"changed_issue_files_detected",
|
|
131
|
+
count=len(issue_files),
|
|
132
|
+
git_ref=git_ref,
|
|
133
|
+
)
|
|
134
|
+
return issue_files
|
|
135
|
+
|
|
136
|
+
except GitHistoryError as e:
|
|
137
|
+
logger.warning(
|
|
138
|
+
"changed_file_detection_failed",
|
|
139
|
+
error=str(e),
|
|
140
|
+
using_fallback="rebuild_all",
|
|
141
|
+
severity="operational",
|
|
142
|
+
)
|
|
143
|
+
# If git history fails, return empty set to trigger full rebuild
|
|
144
|
+
return set()
|
|
145
|
+
|
|
146
|
+
def should_rebuild_all(
|
|
147
|
+
self,
|
|
148
|
+
cached_state: SyncState | None,
|
|
149
|
+
time_since_last_sync: float | None = None,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""Determine if full rebuild is necessary.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
cached_state: Previous sync state (if any)
|
|
155
|
+
time_since_last_sync: Seconds elapsed since last sync
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if full rebuild recommended, False if incremental is OK
|
|
159
|
+
|
|
160
|
+
Rules:
|
|
161
|
+
- If no cached state: always rebuild
|
|
162
|
+
- If more than 1 hour since last sync: rebuild (safer)
|
|
163
|
+
- If metadata indicates corruption: rebuild
|
|
164
|
+
- Otherwise: incremental update possible
|
|
165
|
+
"""
|
|
166
|
+
if not cached_state:
|
|
167
|
+
logger.debug(
|
|
168
|
+
"rebuild_required_reason",
|
|
169
|
+
reason="no_cached_state",
|
|
170
|
+
)
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
if time_since_last_sync is not None and time_since_last_sync > 3600:
|
|
174
|
+
logger.debug(
|
|
175
|
+
"rebuild_recommended_reason",
|
|
176
|
+
reason="stale_cache_1hour_old",
|
|
177
|
+
seconds_old=time_since_last_sync,
|
|
178
|
+
)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
# If we get here, incremental update is safe
|
|
182
|
+
logger.debug(
|
|
183
|
+
"incremental_update_safe",
|
|
184
|
+
cached_issues=len(cached_state.base_issues),
|
|
185
|
+
)
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def extract_issue_id_from_path(self, file_path: str) -> str | None:
|
|
189
|
+
"""Extract issue ID from file path.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
file_path: Path like 'issues/backlog/TASK-123-example.md'
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Issue ID like 'TASK-123' or None if not parseable
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
>>> builder.extract_issue_id_from_path("issues/backlog/TASK-123-example.md")
|
|
199
|
+
'TASK-123'
|
|
200
|
+
>>> builder.extract_issue_id_from_path("invalid.txt")
|
|
201
|
+
None
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
# Extract filename
|
|
205
|
+
filename = Path(file_path).name
|
|
206
|
+
# Remove .md extension
|
|
207
|
+
base = filename.replace(".md", "")
|
|
208
|
+
# Take part before first dash and second dash
|
|
209
|
+
# Format is typically TASK-123-description or similar
|
|
210
|
+
parts = base.split("-")
|
|
211
|
+
if len(parts) >= 2:
|
|
212
|
+
# Check if first part is alphabetic and second is numeric
|
|
213
|
+
if parts[0].isalpha() and parts[1].isdigit():
|
|
214
|
+
# Return TYPE-ID format (e.g., TASK-123)
|
|
215
|
+
issue_id = f"{parts[0]}-{parts[1]}"
|
|
216
|
+
return issue_id
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.debug(
|
|
219
|
+
"issue_id_parsing_failed",
|
|
220
|
+
operation="parse_issue_id",
|
|
221
|
+
error=str(e),
|
|
222
|
+
action="Returning None",
|
|
223
|
+
)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def get_issue_files_to_update(
|
|
227
|
+
self,
|
|
228
|
+
all_issue_files: list[Path],
|
|
229
|
+
changed_files: set[str],
|
|
230
|
+
cached_state: SyncState | None,
|
|
231
|
+
) -> dict[str, Path]:
|
|
232
|
+
"""Determine which issue files need baseline updates.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
all_issue_files: All issue file paths
|
|
236
|
+
changed_files: Files that changed since git_ref
|
|
237
|
+
cached_state: Previous sync state (for unchanged issues)
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dictionary mapping issue_id to file_path for issues to update
|
|
241
|
+
|
|
242
|
+
Logic:
|
|
243
|
+
- New issues (not in cache): always update
|
|
244
|
+
- Changed files: always update
|
|
245
|
+
- Unchanged files: skip (use cached)
|
|
246
|
+
- Deleted issues: mark for removal
|
|
247
|
+
"""
|
|
248
|
+
files_to_update = {}
|
|
249
|
+
|
|
250
|
+
for file_path in all_issue_files:
|
|
251
|
+
relative_path = str(file_path.relative_to(self.issues_dir))
|
|
252
|
+
issue_id = self.extract_issue_id_from_path(relative_path)
|
|
253
|
+
|
|
254
|
+
if not issue_id:
|
|
255
|
+
logger.debug(
|
|
256
|
+
"skipping_unparseable_issue_file",
|
|
257
|
+
file_path=relative_path,
|
|
258
|
+
)
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# Check if file changed
|
|
262
|
+
is_changed = any(
|
|
263
|
+
relative_path in f or file_path.name in f for f in changed_files
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Check if new (or full rebuild if no cached state)
|
|
267
|
+
is_new = cached_state is None or issue_id not in cached_state.base_issues
|
|
268
|
+
|
|
269
|
+
if is_changed or is_new:
|
|
270
|
+
files_to_update[issue_id] = file_path
|
|
271
|
+
logger.debug(
|
|
272
|
+
"issue_requires_update",
|
|
273
|
+
issue_id=issue_id,
|
|
274
|
+
reason="changed" if is_changed else "new",
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
logger.debug(
|
|
278
|
+
"issue_using_cached_baseline",
|
|
279
|
+
issue_id=issue_id,
|
|
280
|
+
reason="unchanged_file",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return files_to_update
|
|
284
|
+
|
|
285
|
+
def get_incremental_update_issues(
|
|
286
|
+
self,
|
|
287
|
+
all_issue_files: list[Path],
|
|
288
|
+
changed_files: set[str],
|
|
289
|
+
cached_state: SyncState,
|
|
290
|
+
) -> tuple[dict[str, Path], list[str]]:
|
|
291
|
+
"""Get issues for incremental baseline update.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
all_issue_files: All current issue files
|
|
295
|
+
changed_files: Files changed since last sync
|
|
296
|
+
cached_state: Previous sync state
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Tuple of (issues_to_update, issues_to_remove)
|
|
300
|
+
- issues_to_update: {issue_id: file_path} for changed/new issues
|
|
301
|
+
- issues_to_remove: [issue_id] for deleted issues
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> files = [Path("issues/backlog/TASK-123-example.md")]
|
|
305
|
+
>>> changed = {"issues/backlog/TASK-123-example.md"}
|
|
306
|
+
>>> updates, removals = builder.get_incremental_update_issues(files, changed, state)
|
|
307
|
+
>>> print(updates) # {"TASK-123": Path(...)}
|
|
308
|
+
>>> print(removals) # []
|
|
309
|
+
"""
|
|
310
|
+
# Update progress: analyzing files
|
|
311
|
+
self._update_progress("Analyzing changed files...", 0, len(all_issue_files))
|
|
312
|
+
|
|
313
|
+
# Get issues to update
|
|
314
|
+
issues_to_update = self.get_issue_files_to_update(
|
|
315
|
+
all_issue_files, changed_files, cached_state
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Update progress: detecting deletions
|
|
319
|
+
self._update_progress(
|
|
320
|
+
f"Checking for deletions ({len(issues_to_update)} changed)...",
|
|
321
|
+
len(issues_to_update),
|
|
322
|
+
len(all_issue_files),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Find deleted issues (in cache but not in current files)
|
|
326
|
+
current_ids = {self.extract_issue_id_from_path(str(f)) for f in all_issue_files}
|
|
327
|
+
current_ids.discard(None)
|
|
328
|
+
|
|
329
|
+
deleted_ids = [
|
|
330
|
+
issue_id
|
|
331
|
+
for issue_id in cached_state.base_issues.keys()
|
|
332
|
+
if issue_id not in current_ids
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
if deleted_ids:
|
|
336
|
+
logger.info(
|
|
337
|
+
"detected_deleted_issues",
|
|
338
|
+
count=len(deleted_ids),
|
|
339
|
+
issue_ids=deleted_ids,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return issues_to_update, deleted_ids
|
|
343
|
+
|
|
344
|
+
def estimate_rebuild_time(
|
|
345
|
+
self,
|
|
346
|
+
issues_count: int,
|
|
347
|
+
git_operations_per_issue: float = 0.010, # ~10ms per git operation
|
|
348
|
+
parse_overhead: float = 0.001, # ~1ms per file parse
|
|
349
|
+
) -> float:
|
|
350
|
+
"""Estimate time to rebuild baseline state.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
issues_count: Number of issues to rebuild
|
|
354
|
+
git_operations_per_issue: Average time for git operations (seconds)
|
|
355
|
+
parse_overhead: Time to parse each file (seconds)
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Estimated time in milliseconds
|
|
359
|
+
|
|
360
|
+
Examples:
|
|
361
|
+
>>> builder = OptimizedBaselineBuilder(Path("roadmap/issues"))
|
|
362
|
+
>>> time_ms = builder.estimate_rebuild_time(100)
|
|
363
|
+
>>> print(f"Est. time: {time_ms:.1f}ms") # ~1.1s for 100 issues
|
|
364
|
+
>>> time_ms = builder.estimate_rebuild_time(5)
|
|
365
|
+
>>> print(f"Est. time: {time_ms:.1f}ms") # ~55ms for 5 issues (incremental)
|
|
366
|
+
"""
|
|
367
|
+
total_seconds = issues_count * (git_operations_per_issue + parse_overhead)
|
|
368
|
+
return total_seconds * 1000
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class CachedBaselineState:
|
|
372
|
+
"""Wrapper for baseline state with cache metadata.
|
|
373
|
+
|
|
374
|
+
Tracks whether the state is from cache or freshly rebuilt,
|
|
375
|
+
and provides statistics about the rebuild process.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
def __init__(
|
|
379
|
+
self,
|
|
380
|
+
state: SyncState,
|
|
381
|
+
from_cache: bool = False,
|
|
382
|
+
rebuilt_issues: int | None = None,
|
|
383
|
+
reused_issues: int | None = None,
|
|
384
|
+
rebuild_time_ms: float | None = None,
|
|
385
|
+
):
|
|
386
|
+
"""Initialize cached baseline state.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
state: The SyncState object
|
|
390
|
+
from_cache: Whether this is from cache (True) or rebuilt (False)
|
|
391
|
+
rebuilt_issues: Count of issues rebuilt
|
|
392
|
+
reused_issues: Count of issues reused from cache
|
|
393
|
+
rebuild_time_ms: Time to rebuild in milliseconds
|
|
394
|
+
"""
|
|
395
|
+
self.state = state
|
|
396
|
+
self.from_cache = from_cache
|
|
397
|
+
self.rebuilt_issues = rebuilt_issues or 0
|
|
398
|
+
self.reused_issues = reused_issues or 0
|
|
399
|
+
self.rebuild_time_ms = rebuild_time_ms or 0.0
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def is_full_rebuild(self) -> bool:
|
|
403
|
+
"""Whether this is a full rebuild (no cache reuse)."""
|
|
404
|
+
return self.reused_issues == 0 and not self.from_cache
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def is_incremental(self) -> bool:
|
|
408
|
+
"""Whether this is an incremental update (reused some cached issues)."""
|
|
409
|
+
return self.reused_issues > 0
|
|
410
|
+
|
|
411
|
+
def to_dict(self) -> dict:
|
|
412
|
+
"""Convert to dictionary for logging.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Dictionary with all metadata
|
|
416
|
+
"""
|
|
417
|
+
return {
|
|
418
|
+
"from_cache": self.from_cache,
|
|
419
|
+
"is_full_rebuild": self.is_full_rebuild,
|
|
420
|
+
"is_incremental": self.is_incremental,
|
|
421
|
+
"rebuilt_issues": self.rebuilt_issues,
|
|
422
|
+
"reused_issues": self.reused_issues,
|
|
423
|
+
"total_issues": len(self.state.base_issues),
|
|
424
|
+
"rebuild_time_ms": f"{self.rebuild_time_ms:.1f}",
|
|
425
|
+
}
|