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,288 @@
|
|
|
1
|
+
"""Enhanced YAML validation utilities for roadmap persistence."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from roadmap.common.datetime_parser import parse_datetime
|
|
11
|
+
from roadmap.common.utils.file_utils import ensure_directory_exists
|
|
12
|
+
from roadmap.common.validation import validate_frontmatter_structure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class YAMLRecoveryManager:
|
|
16
|
+
"""Manages YAML file recovery and validation."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, backup_dir: Path | None = None):
|
|
19
|
+
"""Initialize YAMLRecoveryManager.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
backup_dir: Directory for storing backups. Defaults to .roadmap/backups.
|
|
23
|
+
"""
|
|
24
|
+
self.backup_dir = backup_dir or Path(".roadmap/backups")
|
|
25
|
+
ensure_directory_exists(self.backup_dir)
|
|
26
|
+
|
|
27
|
+
def create_backup(self, file_path: Path) -> Path:
|
|
28
|
+
"""Create a backup of a YAML file before modification."""
|
|
29
|
+
if not file_path.exists():
|
|
30
|
+
raise FileNotFoundError(f"Cannot backup non-existent file: {file_path}")
|
|
31
|
+
|
|
32
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
|
|
33
|
+
backup_name = f"{file_path.stem}_{timestamp}.backup{file_path.suffix}"
|
|
34
|
+
backup_path = self.backup_dir / backup_name
|
|
35
|
+
|
|
36
|
+
shutil.copy2(file_path, backup_path)
|
|
37
|
+
return backup_path
|
|
38
|
+
|
|
39
|
+
def list_backups(self, file_path: Path) -> list[Path]:
|
|
40
|
+
"""List all backups for a specific file."""
|
|
41
|
+
pattern = f"{file_path.stem}_*.backup{file_path.suffix}"
|
|
42
|
+
return sorted(self.backup_dir.glob(pattern), reverse=True)
|
|
43
|
+
|
|
44
|
+
def validate_yaml_syntax(
|
|
45
|
+
self, content: str, file_path: Path | None = None
|
|
46
|
+
) -> tuple[bool, str | None]:
|
|
47
|
+
"""Validate YAML syntax and return error details if invalid."""
|
|
48
|
+
try:
|
|
49
|
+
yaml.safe_load(content)
|
|
50
|
+
return True, None
|
|
51
|
+
except yaml.YAMLError as e:
|
|
52
|
+
error_msg = f"YAML syntax error: {e}"
|
|
53
|
+
if hasattr(e, "problem_mark") and e.problem_mark: # type: ignore
|
|
54
|
+
line_num = e.problem_mark.line + 1 # type: ignore
|
|
55
|
+
col_num = e.problem_mark.column + 1 # type: ignore
|
|
56
|
+
error_msg += f" at line {line_num}, column {col_num}"
|
|
57
|
+
return False, error_msg
|
|
58
|
+
|
|
59
|
+
def validate_frontmatter_structure(
|
|
60
|
+
self, frontmatter: dict[str, Any], expected_type: str
|
|
61
|
+
) -> tuple[bool, list[str]]:
|
|
62
|
+
"""Validate frontmatter structure for issues or milestones."""
|
|
63
|
+
# Use unified validation framework
|
|
64
|
+
is_valid, errors = validate_frontmatter_structure(frontmatter, expected_type)
|
|
65
|
+
|
|
66
|
+
# Add datetime validation (specific to persistence layer)
|
|
67
|
+
datetime_fields = ["created", "updated", "due_date"]
|
|
68
|
+
for field in datetime_fields:
|
|
69
|
+
if field in frontmatter and frontmatter[field] is not None:
|
|
70
|
+
if isinstance(frontmatter[field], str):
|
|
71
|
+
try:
|
|
72
|
+
parse_datetime(frontmatter[field], "file")
|
|
73
|
+
except ValueError:
|
|
74
|
+
errors.append(
|
|
75
|
+
f"Invalid datetime format for {field}: {frontmatter[field]}"
|
|
76
|
+
)
|
|
77
|
+
is_valid = False
|
|
78
|
+
|
|
79
|
+
return is_valid, errors
|
|
80
|
+
|
|
81
|
+
def attempt_recovery(self, file_path: Path) -> tuple[bool, str | None]:
|
|
82
|
+
"""Attempt to recover a corrupted YAML file."""
|
|
83
|
+
try:
|
|
84
|
+
# Try to read the file
|
|
85
|
+
content = file_path.read_text(encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
# Check if it's a frontmatter issue
|
|
88
|
+
if content.startswith("---"):
|
|
89
|
+
parts = content.split("---", 2)
|
|
90
|
+
if len(parts) >= 3:
|
|
91
|
+
frontmatter_content = parts[1]
|
|
92
|
+
markdown_content = parts[2].strip() if len(parts) > 2 else ""
|
|
93
|
+
|
|
94
|
+
# Try to fix common YAML issues
|
|
95
|
+
fixed_frontmatter = self._fix_common_yaml_issues(
|
|
96
|
+
frontmatter_content
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Validate the fixed YAML
|
|
100
|
+
is_valid, _ = self.validate_yaml_syntax(fixed_frontmatter)
|
|
101
|
+
if is_valid:
|
|
102
|
+
# Create backup before fixing
|
|
103
|
+
self.create_backup(file_path)
|
|
104
|
+
|
|
105
|
+
# Write the fixed content
|
|
106
|
+
fixed_content = (
|
|
107
|
+
f"---\n{fixed_frontmatter}\n---\n\n{markdown_content}"
|
|
108
|
+
)
|
|
109
|
+
file_path.write_text(fixed_content, encoding="utf-8")
|
|
110
|
+
return True, "File recovered with YAML fixes applied"
|
|
111
|
+
|
|
112
|
+
return False, "Could not automatically recover the file"
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
return False, f"Recovery failed: {e}"
|
|
116
|
+
|
|
117
|
+
def _is_already_quoted(self, value: str) -> bool:
|
|
118
|
+
"""Check if value is already properly quoted.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
value: Value to check
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if value is already quoted
|
|
125
|
+
"""
|
|
126
|
+
return (value.startswith('"') and value.endswith('"')) or (
|
|
127
|
+
value.startswith("'") and value.endswith("'")
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _is_yaml_literal(self, value: str) -> bool:
|
|
131
|
+
"""Check if value is a YAML literal (bool, null, number, collection).
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
value: Value to check
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if value is a YAML literal
|
|
138
|
+
"""
|
|
139
|
+
return (
|
|
140
|
+
value in ["true", "false", "null"]
|
|
141
|
+
or value.isdigit()
|
|
142
|
+
or value.startswith("[")
|
|
143
|
+
or value.startswith("{")
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _needs_quoting(self, value: str) -> bool:
|
|
147
|
+
"""Check if value contains special YAML characters requiring quoting.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
value: Value to check
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if value needs quoting
|
|
154
|
+
"""
|
|
155
|
+
special_chars = [
|
|
156
|
+
":",
|
|
157
|
+
"#",
|
|
158
|
+
"@",
|
|
159
|
+
"`",
|
|
160
|
+
"|",
|
|
161
|
+
">",
|
|
162
|
+
"*",
|
|
163
|
+
"&",
|
|
164
|
+
"!",
|
|
165
|
+
"%",
|
|
166
|
+
"{",
|
|
167
|
+
"}",
|
|
168
|
+
"[",
|
|
169
|
+
"]",
|
|
170
|
+
]
|
|
171
|
+
return any(char in value for char in special_chars)
|
|
172
|
+
|
|
173
|
+
def _quote_if_needed(self, value: str) -> str:
|
|
174
|
+
"""Quote value if it needs quoting and isn't already quoted.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
value: Value to potentially quote
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Quoted value if needed, otherwise original value
|
|
181
|
+
"""
|
|
182
|
+
if value and not (
|
|
183
|
+
self._is_already_quoted(value) or self._is_yaml_literal(value)
|
|
184
|
+
):
|
|
185
|
+
if self._needs_quoting(value):
|
|
186
|
+
return f'"{value}"'
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
def _fix_yaml_line(self, line: str) -> str:
|
|
190
|
+
"""Fix a single YAML line.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
line: Single line of YAML to fix
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Fixed line
|
|
197
|
+
"""
|
|
198
|
+
if not line.strip() or line.strip().startswith("#"):
|
|
199
|
+
return line
|
|
200
|
+
|
|
201
|
+
if ":" in line:
|
|
202
|
+
key, _, value = line.partition(":")
|
|
203
|
+
value = value.strip()
|
|
204
|
+
fixed_value = self._quote_if_needed(value)
|
|
205
|
+
return f"{key}: {fixed_value}"
|
|
206
|
+
|
|
207
|
+
return line
|
|
208
|
+
|
|
209
|
+
def _fix_common_yaml_issues(self, yaml_content: str) -> str:
|
|
210
|
+
"""Fix common YAML formatting issues."""
|
|
211
|
+
lines = yaml_content.split("\n")
|
|
212
|
+
fixed_lines = [self._fix_yaml_line(line) for line in lines]
|
|
213
|
+
return "\n".join(fixed_lines)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class EnhancedYAMLPersistence:
|
|
217
|
+
"""Enhanced YAML persistence with validation and recovery."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, recovery_manager: YAMLRecoveryManager | None = None):
|
|
220
|
+
"""Initialize EnhancedYAMLPersistence.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
recovery_manager: YAMLRecoveryManager instance. Defaults to new instance.
|
|
224
|
+
"""
|
|
225
|
+
self.recovery_manager = recovery_manager or YAMLRecoveryManager()
|
|
226
|
+
|
|
227
|
+
def safe_load_with_validation(
|
|
228
|
+
self, file_path: Path, expected_type: str
|
|
229
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
230
|
+
"""Safely load and validate a YAML file with recovery options."""
|
|
231
|
+
try:
|
|
232
|
+
if not file_path.exists():
|
|
233
|
+
return False, f"File does not exist: {file_path}"
|
|
234
|
+
|
|
235
|
+
content = file_path.read_text(encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
# Parse frontmatter
|
|
238
|
+
if not content.startswith("---"):
|
|
239
|
+
return False, "File does not contain YAML frontmatter"
|
|
240
|
+
|
|
241
|
+
parts = content.split("---", 2)
|
|
242
|
+
if len(parts) < 3:
|
|
243
|
+
return False, "Invalid frontmatter format"
|
|
244
|
+
|
|
245
|
+
frontmatter_content = parts[1]
|
|
246
|
+
markdown_content = parts[2].strip()
|
|
247
|
+
|
|
248
|
+
# Validate YAML syntax
|
|
249
|
+
is_valid, error = self.recovery_manager.validate_yaml_syntax(
|
|
250
|
+
frontmatter_content, file_path
|
|
251
|
+
)
|
|
252
|
+
if not is_valid:
|
|
253
|
+
# Attempt recovery
|
|
254
|
+
recovered, recovery_msg = self.recovery_manager.attempt_recovery(
|
|
255
|
+
file_path
|
|
256
|
+
)
|
|
257
|
+
if recovered:
|
|
258
|
+
# Retry loading after recovery
|
|
259
|
+
return self.safe_load_with_validation(file_path, expected_type)
|
|
260
|
+
else:
|
|
261
|
+
return (
|
|
262
|
+
False,
|
|
263
|
+
f"YAML validation failed: {error}. Recovery failed: {recovery_msg}",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Parse YAML
|
|
267
|
+
loaded = yaml.safe_load(frontmatter_content)
|
|
268
|
+
frontmatter: dict = loaded if isinstance(loaded, dict) else {}
|
|
269
|
+
|
|
270
|
+
# Validate structure
|
|
271
|
+
is_valid, errors = self.recovery_manager.validate_frontmatter_structure(
|
|
272
|
+
frontmatter, expected_type
|
|
273
|
+
)
|
|
274
|
+
if not is_valid:
|
|
275
|
+
return False, f"Validation errors: {'; '.join(errors)}"
|
|
276
|
+
|
|
277
|
+
# Add content to frontmatter
|
|
278
|
+
frontmatter["content"] = markdown_content
|
|
279
|
+
|
|
280
|
+
return True, frontmatter
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return False, f"Failed to load file: {e}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Global instances for easy access
|
|
287
|
+
recovery_manager = YAMLRecoveryManager()
|
|
288
|
+
enhanced_persistence = EnhancedYAMLPersistence(recovery_manager)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Repository classes for persistence layer."""
|
|
2
|
+
|
|
3
|
+
from .issue_repository import IssueRepository
|
|
4
|
+
from .milestone_repository import MilestoneRepository
|
|
5
|
+
from .project_repository import ProjectRepository
|
|
6
|
+
from .remote_link_repository import RemoteLinkRepository
|
|
7
|
+
from .sync_state_repository import SyncStateRepository
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ProjectRepository",
|
|
11
|
+
"MilestoneRepository",
|
|
12
|
+
"IssueRepository",
|
|
13
|
+
"SyncStateRepository",
|
|
14
|
+
"RemoteLinkRepository",
|
|
15
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Repository for issue persistence operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from roadmap.common.errors import OperationType, safe_operation
|
|
6
|
+
from roadmap.common.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IssueRepository:
|
|
12
|
+
"""Handles all issue-related database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, get_connection, transaction):
|
|
15
|
+
"""Initialize repository with database connection methods.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
get_connection: Callable that returns sqlite3 Connection
|
|
19
|
+
transaction: Context manager for database transactions
|
|
20
|
+
"""
|
|
21
|
+
self._get_connection = get_connection
|
|
22
|
+
self._transaction = transaction
|
|
23
|
+
|
|
24
|
+
@safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
|
|
25
|
+
@safe_operation(OperationType.CREATE, "Issue", include_traceback=True)
|
|
26
|
+
def create(self, issue_data: dict[str, Any]) -> str:
|
|
27
|
+
"""Create a new issue.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
issue_data: Dictionary with issue fields
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Issue ID
|
|
34
|
+
"""
|
|
35
|
+
with self._transaction() as conn:
|
|
36
|
+
project_id = issue_data.get("project_id")
|
|
37
|
+
if project_id:
|
|
38
|
+
exists = conn.execute(
|
|
39
|
+
"SELECT 1 FROM projects WHERE id = ?",
|
|
40
|
+
(project_id,),
|
|
41
|
+
).fetchone()
|
|
42
|
+
if not exists:
|
|
43
|
+
logger.warning(
|
|
44
|
+
"issue_create_missing_project",
|
|
45
|
+
issue_id=issue_data.get("id"),
|
|
46
|
+
project_id=project_id,
|
|
47
|
+
)
|
|
48
|
+
project_id = None
|
|
49
|
+
|
|
50
|
+
milestone_id = issue_data.get("milestone_id")
|
|
51
|
+
if milestone_id:
|
|
52
|
+
exists = conn.execute(
|
|
53
|
+
"SELECT 1 FROM milestones WHERE id = ?",
|
|
54
|
+
(milestone_id,),
|
|
55
|
+
).fetchone()
|
|
56
|
+
if not exists:
|
|
57
|
+
logger.warning(
|
|
58
|
+
"issue_create_missing_milestone",
|
|
59
|
+
issue_id=issue_data.get("id"),
|
|
60
|
+
milestone_id=milestone_id,
|
|
61
|
+
)
|
|
62
|
+
milestone_id = None
|
|
63
|
+
|
|
64
|
+
conn.execute(
|
|
65
|
+
"""
|
|
66
|
+
INSERT INTO issues (id, project_id, milestone_id, title, headline, description, status, priority, issue_type, assignee, estimate_hours, due_date, metadata)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
""",
|
|
69
|
+
(
|
|
70
|
+
issue_data["id"],
|
|
71
|
+
project_id,
|
|
72
|
+
milestone_id,
|
|
73
|
+
issue_data.get("title"),
|
|
74
|
+
issue_data.get("headline", ""),
|
|
75
|
+
issue_data.get("description"),
|
|
76
|
+
issue_data.get("status", "open"),
|
|
77
|
+
issue_data.get("priority", "medium"),
|
|
78
|
+
issue_data.get("issue_type", "task"),
|
|
79
|
+
issue_data.get("assignee"),
|
|
80
|
+
issue_data.get("estimate_hours"),
|
|
81
|
+
issue_data.get("due_date"),
|
|
82
|
+
issue_data.get("metadata"),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info("Created issue", issue_id=issue_data["id"])
|
|
87
|
+
return issue_data["id"]
|
|
88
|
+
|
|
89
|
+
def get(self, issue_id: str) -> dict[str, Any] | None:
|
|
90
|
+
"""Get issue by ID.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
issue_id: Issue identifier
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Issue data or None if not found
|
|
97
|
+
"""
|
|
98
|
+
conn = self._get_connection()
|
|
99
|
+
row = conn.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
|
|
100
|
+
|
|
101
|
+
return dict(row) if row else None
|
|
102
|
+
|
|
103
|
+
@safe_operation(OperationType.UPDATE, "Issue")
|
|
104
|
+
@safe_operation(OperationType.UPDATE, "Issue")
|
|
105
|
+
def update(self, issue_id: str, updates: dict[str, Any]) -> bool:
|
|
106
|
+
"""Update issue.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
issue_id: Issue identifier
|
|
110
|
+
updates: Dictionary of fields to update
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if successful, False otherwise
|
|
114
|
+
"""
|
|
115
|
+
if not updates:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
set_clause = ", ".join(f"{key} = ?" for key in updates.keys())
|
|
119
|
+
values = list(updates.values()) + [issue_id]
|
|
120
|
+
|
|
121
|
+
with self._transaction() as conn:
|
|
122
|
+
cursor = conn.execute(
|
|
123
|
+
f"""
|
|
124
|
+
UPDATE issues SET {set_clause} WHERE id = ?
|
|
125
|
+
""",
|
|
126
|
+
values,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
updated = cursor.rowcount > 0
|
|
130
|
+
if updated:
|
|
131
|
+
logger.info(
|
|
132
|
+
"Updated issue", issue_id=issue_id, updates=list(updates.keys())
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return updated
|
|
136
|
+
|
|
137
|
+
@safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
|
|
138
|
+
@safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
|
|
139
|
+
def delete(self, issue_id: str) -> bool:
|
|
140
|
+
"""Delete issue.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
issue_id: Issue identifier
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if successful, False otherwise
|
|
147
|
+
"""
|
|
148
|
+
with self._transaction() as conn:
|
|
149
|
+
cursor = conn.execute("DELETE FROM issues WHERE id = ?", (issue_id,))
|
|
150
|
+
|
|
151
|
+
deleted = cursor.rowcount > 0
|
|
152
|
+
if deleted:
|
|
153
|
+
logger.info("Deleted issue", issue_id=issue_id)
|
|
154
|
+
|
|
155
|
+
return deleted
|
|
156
|
+
|
|
157
|
+
@safe_operation(OperationType.DELETE, "Issue", include_traceback=True)
|
|
158
|
+
def delete_many(self, issue_ids: list[str]) -> int:
|
|
159
|
+
"""Delete multiple issues in a single transaction.
|
|
160
|
+
|
|
161
|
+
Batch delete is much more efficient than deleting one-by-one
|
|
162
|
+
as it avoids repeated cache invalidations and enumeration.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
issue_ids: List of issue identifiers to delete
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Number of issues successfully deleted
|
|
169
|
+
"""
|
|
170
|
+
if not issue_ids:
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
with self._transaction() as conn:
|
|
174
|
+
placeholders = ",".join("?" * len(issue_ids))
|
|
175
|
+
cursor = conn.execute(
|
|
176
|
+
f"DELETE FROM issues WHERE id IN ({placeholders})",
|
|
177
|
+
issue_ids,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
deleted_count = cursor.rowcount
|
|
181
|
+
if deleted_count > 0:
|
|
182
|
+
logger.info(
|
|
183
|
+
"Deleted multiple issues",
|
|
184
|
+
count=deleted_count,
|
|
185
|
+
issue_count=len(issue_ids),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return deleted_count
|
|
189
|
+
|
|
190
|
+
@safe_operation(OperationType.UPDATE, "Issue")
|
|
191
|
+
@safe_operation(OperationType.UPDATE, "Issue")
|
|
192
|
+
def mark_archived(self, issue_id: str, archived: bool = True) -> bool:
|
|
193
|
+
"""Mark an issue as archived or unarchived.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
issue_id: Issue identifier
|
|
197
|
+
archived: True to archive, False to unarchive
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if successful, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
with self._transaction() as conn:
|
|
203
|
+
if archived:
|
|
204
|
+
cursor = conn.execute(
|
|
205
|
+
"UPDATE issues SET archived = 1, archived_at = CURRENT_TIMESTAMP WHERE id = ?",
|
|
206
|
+
(issue_id,),
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
cursor = conn.execute(
|
|
210
|
+
"UPDATE issues SET archived = 0, archived_at = NULL WHERE id = ?",
|
|
211
|
+
(issue_id,),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
updated = cursor.rowcount > 0
|
|
215
|
+
if updated:
|
|
216
|
+
action = "archived" if archived else "unarchived"
|
|
217
|
+
logger.info(
|
|
218
|
+
"issue_status_changed",
|
|
219
|
+
issue_id=issue_id,
|
|
220
|
+
action=action,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return updated
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Repository for milestone persistence operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from roadmap.common.errors import OperationType, safe_operation
|
|
6
|
+
from roadmap.common.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MilestoneRepository:
|
|
12
|
+
"""Handles all milestone-related database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, get_connection, transaction):
|
|
15
|
+
"""Initialize repository with database connection methods.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
get_connection: Callable that returns sqlite3 Connection
|
|
19
|
+
transaction: Context manager for database transactions
|
|
20
|
+
"""
|
|
21
|
+
self._get_connection = get_connection
|
|
22
|
+
self._transaction = transaction
|
|
23
|
+
|
|
24
|
+
@safe_operation(OperationType.CREATE, "Milestone", include_traceback=True)
|
|
25
|
+
@safe_operation(OperationType.CREATE, "Milestone", include_traceback=True)
|
|
26
|
+
def create(self, milestone_data: dict[str, Any]) -> str:
|
|
27
|
+
"""Create a new milestone.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
milestone_data: Dictionary with milestone fields
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Milestone ID
|
|
34
|
+
"""
|
|
35
|
+
with self._transaction() as conn:
|
|
36
|
+
conn.execute(
|
|
37
|
+
"""
|
|
38
|
+
INSERT INTO milestones (id, project_id, title, description, status, due_date, metadata)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
40
|
+
""",
|
|
41
|
+
(
|
|
42
|
+
milestone_data["id"],
|
|
43
|
+
milestone_data.get("project_id"),
|
|
44
|
+
milestone_data.get("title"),
|
|
45
|
+
milestone_data.get("description"),
|
|
46
|
+
milestone_data.get("status", "open"),
|
|
47
|
+
milestone_data.get("due_date"),
|
|
48
|
+
milestone_data.get("metadata"),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger.info("Created milestone", milestone_id=milestone_data["id"])
|
|
53
|
+
return milestone_data["id"]
|
|
54
|
+
|
|
55
|
+
def get(self, milestone_id: str) -> dict[str, Any] | None:
|
|
56
|
+
"""Get milestone by ID.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
milestone_id: Milestone identifier
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Milestone data or None if not found
|
|
63
|
+
"""
|
|
64
|
+
conn = self._get_connection()
|
|
65
|
+
row = conn.execute(
|
|
66
|
+
"SELECT * FROM milestones WHERE id = ?", (milestone_id,)
|
|
67
|
+
).fetchone()
|
|
68
|
+
|
|
69
|
+
return dict(row) if row else None
|
|
70
|
+
|
|
71
|
+
@safe_operation(OperationType.UPDATE, "Milestone")
|
|
72
|
+
@safe_operation(OperationType.UPDATE, "Milestone")
|
|
73
|
+
def update(self, milestone_id: str, updates: dict[str, Any]) -> bool:
|
|
74
|
+
"""Update milestone.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
milestone_id: Milestone identifier
|
|
78
|
+
updates: Dictionary of fields to update
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if successful, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
if not updates:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
set_clause = ", ".join(f"{key} = ?" for key in updates.keys())
|
|
87
|
+
values = list(updates.values()) + [milestone_id]
|
|
88
|
+
|
|
89
|
+
with self._transaction() as conn:
|
|
90
|
+
cursor = conn.execute(
|
|
91
|
+
f"""
|
|
92
|
+
UPDATE milestones SET {set_clause} WHERE id = ?
|
|
93
|
+
""",
|
|
94
|
+
values,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
updated = cursor.rowcount > 0
|
|
98
|
+
if updated:
|
|
99
|
+
logger.info(
|
|
100
|
+
"Updated milestone",
|
|
101
|
+
milestone_id=milestone_id,
|
|
102
|
+
updates=list(updates.keys()),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return updated
|
|
106
|
+
|
|
107
|
+
@safe_operation(OperationType.UPDATE, "Milestone")
|
|
108
|
+
@safe_operation(OperationType.UPDATE, "Milestone")
|
|
109
|
+
def mark_archived(self, milestone_id: str, archived: bool = True) -> bool:
|
|
110
|
+
"""Mark a milestone as archived or unarchived.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
milestone_id: Milestone identifier
|
|
114
|
+
archived: True to archive, False to unarchive
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if successful, False otherwise
|
|
118
|
+
"""
|
|
119
|
+
with self._transaction() as conn:
|
|
120
|
+
if archived:
|
|
121
|
+
cursor = conn.execute(
|
|
122
|
+
"UPDATE milestones SET archived = 1, archived_at = CURRENT_TIMESTAMP WHERE id = ?",
|
|
123
|
+
(milestone_id,),
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
cursor = conn.execute(
|
|
127
|
+
"UPDATE milestones SET archived = 0, archived_at = NULL WHERE id = ?",
|
|
128
|
+
(milestone_id,),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
updated = cursor.rowcount > 0
|
|
132
|
+
if updated:
|
|
133
|
+
action = "archived" if archived else "unarchived"
|
|
134
|
+
logger.info(
|
|
135
|
+
"milestone_status_changed",
|
|
136
|
+
milestone_id=milestone_id,
|
|
137
|
+
action=action,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return updated
|