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,672 @@
|
|
|
1
|
+
"""Specialized synchronizers for different entity types."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from roadmap.adapters.persistence.file_parser import FileParser
|
|
9
|
+
from roadmap.common.datetime_parser import UnifiedDateTimeParser
|
|
10
|
+
from roadmap.common.logging import get_logger
|
|
11
|
+
from roadmap.core.interfaces.sync_validators import (
|
|
12
|
+
ForeignKeyValidationError,
|
|
13
|
+
ForeignKeyValidator,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EntitySyncCoordinator:
|
|
20
|
+
"""Base coordinator for syncing entity files to database."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, get_connection, transaction_context):
|
|
23
|
+
"""Initialize the coordinator.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
get_connection: Callable that returns a database connection
|
|
27
|
+
transaction_context: Context manager for transactions
|
|
28
|
+
"""
|
|
29
|
+
self._get_connection = get_connection
|
|
30
|
+
self._transaction = transaction_context
|
|
31
|
+
self._parser = FileParser()
|
|
32
|
+
|
|
33
|
+
def _get_default_project_id(self) -> str | None:
|
|
34
|
+
"""Get the first available project ID for orphaned items."""
|
|
35
|
+
try:
|
|
36
|
+
with self._transaction() as conn:
|
|
37
|
+
result = conn.execute("SELECT id FROM projects LIMIT 1").fetchone()
|
|
38
|
+
return result[0] if result else None
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(
|
|
41
|
+
"failed_to_get_default_project_id", error=str(e), severity="operational"
|
|
42
|
+
)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _get_milestone_id_by_name(self, milestone_name: str) -> str | None:
|
|
46
|
+
"""Get milestone ID by name."""
|
|
47
|
+
try:
|
|
48
|
+
with self._transaction() as conn:
|
|
49
|
+
result = conn.execute(
|
|
50
|
+
"SELECT id FROM milestones WHERE title = ?", (milestone_name,)
|
|
51
|
+
).fetchone()
|
|
52
|
+
return result[0] if result else None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"failed_to_find_milestone",
|
|
56
|
+
milestone_name=milestone_name,
|
|
57
|
+
error=str(e),
|
|
58
|
+
severity="operational",
|
|
59
|
+
)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def _normalize_date(self, date_value: Any) -> Any:
|
|
63
|
+
"""Normalize date field from YAML."""
|
|
64
|
+
if not date_value:
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
if isinstance(date_value, str):
|
|
68
|
+
dt = UnifiedDateTimeParser.parse_any_datetime(date_value)
|
|
69
|
+
return dt.date() if dt else None
|
|
70
|
+
return date_value
|
|
71
|
+
except (ValueError, AttributeError):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def _extract_metadata(self, data: dict, exclude_fields: list[str]) -> str | None:
|
|
75
|
+
"""Extract non-standard fields as JSON metadata."""
|
|
76
|
+
metadata = {k: v for k, v in data.items() if k not in exclude_fields}
|
|
77
|
+
if not metadata:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
return json.dumps(metadata, default=self._json_default)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _json_default(value: Any) -> str:
|
|
84
|
+
"""Handle JSON serialization of non-standard types."""
|
|
85
|
+
if isinstance(value, (datetime, date)):
|
|
86
|
+
return value.isoformat()
|
|
87
|
+
# Handle datetime-like objects that may have isoformat method
|
|
88
|
+
if hasattr(value, "isoformat") and callable(value.isoformat):
|
|
89
|
+
try:
|
|
90
|
+
result = value.isoformat()
|
|
91
|
+
if isinstance(result, str):
|
|
92
|
+
return result
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.warning(
|
|
95
|
+
"isoformat_serialization_failed",
|
|
96
|
+
error=str(e),
|
|
97
|
+
value_type=type(value).__name__,
|
|
98
|
+
severity="operational",
|
|
99
|
+
)
|
|
100
|
+
raise TypeError(
|
|
101
|
+
f"Object of type {type(value).__name__} is not JSON serializable"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _update_sync_status(self, file_path: Path) -> None:
|
|
105
|
+
"""Update sync status for a file."""
|
|
106
|
+
metadata = self._parser.extract_file_metadata(file_path)
|
|
107
|
+
if metadata:
|
|
108
|
+
with self._transaction() as conn:
|
|
109
|
+
conn.execute(
|
|
110
|
+
"""
|
|
111
|
+
INSERT OR REPLACE INTO file_sync_state
|
|
112
|
+
(file_path, content_hash, file_size, last_modified)
|
|
113
|
+
VALUES (?, ?, ?, ?)
|
|
114
|
+
""",
|
|
115
|
+
(
|
|
116
|
+
str(file_path),
|
|
117
|
+
metadata["hash"],
|
|
118
|
+
metadata["size"],
|
|
119
|
+
metadata["modified_time"],
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class MilestoneFKValidator(ForeignKeyValidator):
|
|
125
|
+
"""Validates foreign key constraints for milestone syncing.
|
|
126
|
+
|
|
127
|
+
Ensures that projects table has data before syncing milestones,
|
|
128
|
+
preventing FOREIGN KEY constraint violations.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, get_connection):
|
|
132
|
+
"""Initialize validator.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
get_connection: Callable that returns database connection
|
|
136
|
+
"""
|
|
137
|
+
self._get_connection = get_connection
|
|
138
|
+
|
|
139
|
+
def validate(self) -> None:
|
|
140
|
+
"""Validate that projects table has data.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ForeignKeyValidationError: If projects table is empty
|
|
144
|
+
RuntimeError: If database connection fails
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
with self._get_connection() as conn:
|
|
148
|
+
result = conn.execute(
|
|
149
|
+
"SELECT COUNT(*) as count FROM projects"
|
|
150
|
+
).fetchone()
|
|
151
|
+
project_count = result[0] if result else 0
|
|
152
|
+
|
|
153
|
+
if project_count == 0:
|
|
154
|
+
raise ForeignKeyValidationError(
|
|
155
|
+
entity_type="milestone",
|
|
156
|
+
missing_references=[],
|
|
157
|
+
error_details="Projects table is empty. Cannot sync milestones without prerequisite projects. "
|
|
158
|
+
"Ensure ProjectSyncCoordinator syncs all projects before MilestoneSyncCoordinator.",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
logger.info(
|
|
162
|
+
"milestone_fk_validation_passed",
|
|
163
|
+
project_count=project_count,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except ForeignKeyValidationError:
|
|
167
|
+
raise
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(
|
|
170
|
+
"milestone_fk_validation_error",
|
|
171
|
+
error=str(e),
|
|
172
|
+
error_type=type(e).__name__,
|
|
173
|
+
severity="system_error",
|
|
174
|
+
)
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
f"Failed to validate milestone FK constraints: {str(e)}"
|
|
177
|
+
) from e
|
|
178
|
+
|
|
179
|
+
def missing_prerequisites(self) -> list[str]:
|
|
180
|
+
"""Return list of empty prerequisite tables.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List describing which prerequisites are missing
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
with self._get_connection() as conn:
|
|
187
|
+
result = conn.execute(
|
|
188
|
+
"SELECT COUNT(*) as count FROM projects"
|
|
189
|
+
).fetchone()
|
|
190
|
+
project_count = result[0] if result else 0
|
|
191
|
+
|
|
192
|
+
if project_count == 0:
|
|
193
|
+
return ["projects (empty)"]
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(
|
|
198
|
+
"Error checking prerequisites",
|
|
199
|
+
error=str(e),
|
|
200
|
+
severity="system_error",
|
|
201
|
+
)
|
|
202
|
+
return ["unknown (database error)"]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class IssueFKValidator(ForeignKeyValidator):
|
|
206
|
+
"""Validates foreign key constraints for issue syncing.
|
|
207
|
+
|
|
208
|
+
Ensures that both projects and milestones tables have prerequisite data
|
|
209
|
+
before syncing issues, preventing FOREIGN KEY constraint violations.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(self, get_connection):
|
|
213
|
+
"""Initialize validator.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
get_connection: Callable that returns database connection
|
|
217
|
+
"""
|
|
218
|
+
self._get_connection = get_connection
|
|
219
|
+
|
|
220
|
+
def validate(self) -> None:
|
|
221
|
+
"""Validate that projects table has data.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ForeignKeyValidationError: If projects table is empty
|
|
225
|
+
RuntimeError: If database connection fails
|
|
226
|
+
|
|
227
|
+
Notes:
|
|
228
|
+
Issues depend on projects being populated.
|
|
229
|
+
Milestones are optional but must exist if referenced.
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
with self._get_connection() as conn:
|
|
233
|
+
# Check projects (required)
|
|
234
|
+
projects_result = conn.execute(
|
|
235
|
+
"SELECT COUNT(*) as count FROM projects"
|
|
236
|
+
).fetchone()
|
|
237
|
+
project_count = projects_result[0] if projects_result else 0
|
|
238
|
+
|
|
239
|
+
if project_count == 0:
|
|
240
|
+
raise ForeignKeyValidationError(
|
|
241
|
+
entity_type="issue",
|
|
242
|
+
missing_references=[],
|
|
243
|
+
error_details="Projects table is empty. Cannot sync issues without prerequisite projects. "
|
|
244
|
+
"Ensure ProjectSyncCoordinator syncs all projects before IssueSyncCoordinator.",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
logger.info(
|
|
248
|
+
"issue_fk_validation_passed",
|
|
249
|
+
project_count=project_count,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
except ForeignKeyValidationError:
|
|
253
|
+
raise
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(
|
|
256
|
+
"issue_fk_validation_error",
|
|
257
|
+
error=str(e),
|
|
258
|
+
error_type=type(e).__name__,
|
|
259
|
+
severity="system_error",
|
|
260
|
+
)
|
|
261
|
+
raise RuntimeError(
|
|
262
|
+
f"Failed to validate issue FK constraints: {str(e)}"
|
|
263
|
+
) from e
|
|
264
|
+
|
|
265
|
+
def missing_prerequisites(self) -> list[str]:
|
|
266
|
+
"""Return list of empty prerequisite tables.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List describing which prerequisites are missing
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
with self._get_connection() as conn:
|
|
273
|
+
result = conn.execute(
|
|
274
|
+
"SELECT COUNT(*) as count FROM projects"
|
|
275
|
+
).fetchone()
|
|
276
|
+
project_count = result[0] if result else 0
|
|
277
|
+
|
|
278
|
+
if project_count == 0:
|
|
279
|
+
return ["projects (empty)"]
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(
|
|
284
|
+
"Error checking prerequisites",
|
|
285
|
+
error=str(e),
|
|
286
|
+
severity="system_error",
|
|
287
|
+
)
|
|
288
|
+
return ["unknown (database error)"]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class IssueSyncCoordinator(EntitySyncCoordinator):
|
|
292
|
+
"""Handles syncing issue files to database."""
|
|
293
|
+
|
|
294
|
+
def _extract_issue_id(self, issue_data: dict, file_path: Path) -> str:
|
|
295
|
+
"""Extract issue ID from data or filename.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
issue_data: Parsed issue data
|
|
299
|
+
file_path: Path to issue file
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Extracted or derived issue ID
|
|
303
|
+
"""
|
|
304
|
+
issue_id = issue_data.get("id")
|
|
305
|
+
if not issue_id:
|
|
306
|
+
stem = file_path.stem
|
|
307
|
+
if stem.startswith("issue-"):
|
|
308
|
+
issue_id = stem[6:]
|
|
309
|
+
else:
|
|
310
|
+
issue_id = stem
|
|
311
|
+
issue_data["id"] = issue_id
|
|
312
|
+
return issue_id
|
|
313
|
+
|
|
314
|
+
def _handle_project_id(self, issue_data: dict, issue_id: str) -> str | None:
|
|
315
|
+
"""Get project ID for issue, using default if needed.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
issue_data: Issue data dict
|
|
319
|
+
issue_id: Issue ID
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Project ID or None if unavailable
|
|
323
|
+
"""
|
|
324
|
+
project_id = issue_data.get("project_id")
|
|
325
|
+
if not project_id:
|
|
326
|
+
project_id = self._get_default_project_id()
|
|
327
|
+
if not project_id:
|
|
328
|
+
logger.warning(
|
|
329
|
+
"no_projects_found_for_issue",
|
|
330
|
+
issue_id=issue_id,
|
|
331
|
+
severity="operational",
|
|
332
|
+
)
|
|
333
|
+
return None
|
|
334
|
+
issue_data["project_id"] = project_id
|
|
335
|
+
return project_id
|
|
336
|
+
|
|
337
|
+
def _handle_milestone_field(self, issue_data: dict) -> None:
|
|
338
|
+
"""Resolve milestone field (could be name or ID).
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
issue_data: Issue data dict to update
|
|
342
|
+
"""
|
|
343
|
+
milestone_id = issue_data.get("milestone_id")
|
|
344
|
+
if not milestone_id and "milestone" in issue_data:
|
|
345
|
+
milestone_name = issue_data["milestone"]
|
|
346
|
+
milestone_id = self._get_milestone_id_by_name(milestone_name)
|
|
347
|
+
issue_data["milestone_id"] = milestone_id
|
|
348
|
+
|
|
349
|
+
def _normalize_issue_fields(self, issue_data: dict) -> None:
|
|
350
|
+
"""Set defaults and normalize issue fields.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
issue_data: Issue data dict to normalize
|
|
354
|
+
"""
|
|
355
|
+
issue_data.setdefault("title", "Untitled")
|
|
356
|
+
issue_data.setdefault("status", "open")
|
|
357
|
+
issue_data.setdefault("priority", "medium")
|
|
358
|
+
issue_data.setdefault("issue_type", issue_data.pop("type", "task"))
|
|
359
|
+
issue_data["due_date"] = self._normalize_date(issue_data.get("due_date"))
|
|
360
|
+
|
|
361
|
+
def sync_issue_file(self, file_path: Path) -> bool:
|
|
362
|
+
"""Sync a single issue file to database."""
|
|
363
|
+
try:
|
|
364
|
+
if not file_path.exists():
|
|
365
|
+
logger.warning(
|
|
366
|
+
"issue_file_not_found",
|
|
367
|
+
file_path=str(file_path),
|
|
368
|
+
severity="operational",
|
|
369
|
+
)
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
# Parse YAML frontmatter
|
|
373
|
+
issue_data = self._parser.parse_yaml_frontmatter(file_path)
|
|
374
|
+
if not issue_data:
|
|
375
|
+
logger.warning(
|
|
376
|
+
"no_yaml_data_found",
|
|
377
|
+
file_path=str(file_path),
|
|
378
|
+
severity="operational",
|
|
379
|
+
)
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
# Extract issue ID
|
|
383
|
+
issue_id = self._extract_issue_id(issue_data, file_path)
|
|
384
|
+
|
|
385
|
+
# Handle project ID
|
|
386
|
+
project_id = self._handle_project_id(issue_data, issue_id)
|
|
387
|
+
if not project_id:
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
# Handle milestone field
|
|
391
|
+
self._handle_milestone_field(issue_data)
|
|
392
|
+
|
|
393
|
+
# Normalize fields
|
|
394
|
+
self._normalize_issue_fields(issue_data)
|
|
395
|
+
|
|
396
|
+
# Extract metadata
|
|
397
|
+
exclude_fields = [
|
|
398
|
+
"id",
|
|
399
|
+
"title",
|
|
400
|
+
"description",
|
|
401
|
+
"status",
|
|
402
|
+
"priority",
|
|
403
|
+
"issue_type",
|
|
404
|
+
"assignee",
|
|
405
|
+
"estimate_hours",
|
|
406
|
+
"due_date",
|
|
407
|
+
"project_id",
|
|
408
|
+
"milestone_id",
|
|
409
|
+
]
|
|
410
|
+
metadata = self._extract_metadata(issue_data, exclude_fields)
|
|
411
|
+
|
|
412
|
+
# Upsert issue
|
|
413
|
+
with self._transaction() as conn:
|
|
414
|
+
conn.execute(
|
|
415
|
+
"""
|
|
416
|
+
INSERT OR REPLACE INTO issues
|
|
417
|
+
(id, project_id, milestone_id, title, description, status,
|
|
418
|
+
priority, issue_type, assignee, estimate_hours, due_date, metadata)
|
|
419
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
420
|
+
""",
|
|
421
|
+
(
|
|
422
|
+
issue_data["id"],
|
|
423
|
+
issue_data["project_id"],
|
|
424
|
+
issue_data.get("milestone_id"),
|
|
425
|
+
issue_data["title"],
|
|
426
|
+
issue_data.get("description"),
|
|
427
|
+
issue_data["status"],
|
|
428
|
+
issue_data["priority"],
|
|
429
|
+
issue_data["issue_type"],
|
|
430
|
+
issue_data.get("assignee"),
|
|
431
|
+
issue_data.get("estimate_hours"),
|
|
432
|
+
issue_data["due_date"],
|
|
433
|
+
metadata,
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Update sync status
|
|
438
|
+
self._update_sync_status(file_path)
|
|
439
|
+
logger.info(
|
|
440
|
+
"synced_issue_file",
|
|
441
|
+
issue_id=issue_id,
|
|
442
|
+
file_path=str(file_path),
|
|
443
|
+
)
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(
|
|
448
|
+
"failed_to_sync_issue_file",
|
|
449
|
+
file_path=str(file_path),
|
|
450
|
+
error=str(e),
|
|
451
|
+
severity="operational",
|
|
452
|
+
)
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class MilestoneSyncCoordinator(EntitySyncCoordinator):
|
|
457
|
+
"""Handles syncing milestone files to database."""
|
|
458
|
+
|
|
459
|
+
def __init__(self, get_connection, transaction_context):
|
|
460
|
+
"""Initialize coordinator with FK validator.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
get_connection: Callable that returns a database connection
|
|
464
|
+
transaction_context: Context manager for transactions
|
|
465
|
+
"""
|
|
466
|
+
super().__init__(get_connection, transaction_context)
|
|
467
|
+
self._fk_validator = MilestoneFKValidator(get_connection)
|
|
468
|
+
|
|
469
|
+
def sync_milestone_file(self, file_path: Path) -> bool:
|
|
470
|
+
"""Sync a single milestone file to database.
|
|
471
|
+
|
|
472
|
+
Validates foreign key constraints before syncing to prevent
|
|
473
|
+
database corruption from FOREIGN KEY constraint violations.
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
# Validate foreign key constraints first
|
|
477
|
+
try:
|
|
478
|
+
self._fk_validator.validate()
|
|
479
|
+
except ForeignKeyValidationError as fk_error:
|
|
480
|
+
logger.error(
|
|
481
|
+
"milestone_sync_blocked_by_fk_validation",
|
|
482
|
+
file_path=str(file_path),
|
|
483
|
+
entity_type=fk_error.entity_type,
|
|
484
|
+
error=str(fk_error),
|
|
485
|
+
severity="operational",
|
|
486
|
+
)
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
if not file_path.exists():
|
|
490
|
+
logger.warning(
|
|
491
|
+
"milestone_file_not_found",
|
|
492
|
+
file_path=str(file_path),
|
|
493
|
+
severity="operational",
|
|
494
|
+
)
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
milestone_data = self._parser.parse_yaml_frontmatter(file_path)
|
|
498
|
+
if not milestone_data:
|
|
499
|
+
logger.warning(
|
|
500
|
+
"milestone_no_yaml_data_found",
|
|
501
|
+
file_path=str(file_path),
|
|
502
|
+
severity="operational",
|
|
503
|
+
)
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
# Extract milestone ID
|
|
507
|
+
milestone_id = milestone_data.get("id", file_path.stem)
|
|
508
|
+
milestone_data["id"] = milestone_id
|
|
509
|
+
|
|
510
|
+
# Handle missing project_id
|
|
511
|
+
project_id = milestone_data.get("project_id")
|
|
512
|
+
if not project_id:
|
|
513
|
+
project_id = self._get_default_project_id()
|
|
514
|
+
if not project_id:
|
|
515
|
+
logger.warning(
|
|
516
|
+
"no_projects_found_for_milestone",
|
|
517
|
+
milestone_id=milestone_id,
|
|
518
|
+
severity="operational",
|
|
519
|
+
)
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
# Set defaults and normalize
|
|
523
|
+
title = milestone_data.get("title") or milestone_data.get(
|
|
524
|
+
"name", "Untitled Milestone"
|
|
525
|
+
)
|
|
526
|
+
milestone_data["title"] = title
|
|
527
|
+
milestone_data["status"] = milestone_data.get("status", "open")
|
|
528
|
+
milestone_data["project_id"] = project_id
|
|
529
|
+
milestone_data["progress_percentage"] = milestone_data.get(
|
|
530
|
+
"progress_percentage", 0.0
|
|
531
|
+
)
|
|
532
|
+
milestone_data["due_date"] = self._normalize_date(
|
|
533
|
+
milestone_data.get("due_date")
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Extract metadata
|
|
537
|
+
exclude_fields = [
|
|
538
|
+
"id",
|
|
539
|
+
"title",
|
|
540
|
+
"description",
|
|
541
|
+
"status",
|
|
542
|
+
"due_date",
|
|
543
|
+
"progress_percentage",
|
|
544
|
+
"project_id",
|
|
545
|
+
]
|
|
546
|
+
metadata = self._extract_metadata(milestone_data, exclude_fields)
|
|
547
|
+
|
|
548
|
+
# Upsert milestone
|
|
549
|
+
with self._transaction() as conn:
|
|
550
|
+
conn.execute(
|
|
551
|
+
"""
|
|
552
|
+
INSERT OR REPLACE INTO milestones
|
|
553
|
+
(id, project_id, title, description, status, due_date, progress_percentage, metadata)
|
|
554
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
555
|
+
""",
|
|
556
|
+
(
|
|
557
|
+
milestone_data["id"],
|
|
558
|
+
milestone_data["project_id"],
|
|
559
|
+
milestone_data["title"],
|
|
560
|
+
milestone_data.get("description"),
|
|
561
|
+
milestone_data["status"],
|
|
562
|
+
milestone_data["due_date"],
|
|
563
|
+
milestone_data["progress_percentage"],
|
|
564
|
+
metadata,
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Update sync status
|
|
569
|
+
self._update_sync_status(file_path)
|
|
570
|
+
logger.info(
|
|
571
|
+
"synced_milestone_file",
|
|
572
|
+
milestone_id=milestone_id,
|
|
573
|
+
file_path=str(file_path),
|
|
574
|
+
)
|
|
575
|
+
return True
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(
|
|
579
|
+
"failed_to_sync_milestone_file",
|
|
580
|
+
file_path=str(file_path),
|
|
581
|
+
error=str(e),
|
|
582
|
+
severity="operational",
|
|
583
|
+
)
|
|
584
|
+
return False
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class ProjectSyncCoordinator(EntitySyncCoordinator):
|
|
588
|
+
"""Handles syncing project files to database."""
|
|
589
|
+
|
|
590
|
+
def sync_project_file(self, file_path: Path) -> bool:
|
|
591
|
+
"""Sync a single project file to database."""
|
|
592
|
+
try:
|
|
593
|
+
if not file_path.exists():
|
|
594
|
+
logger.warning(
|
|
595
|
+
"project_file_not_found",
|
|
596
|
+
file_path=str(file_path),
|
|
597
|
+
severity="operational",
|
|
598
|
+
)
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
project_data = self._parser.parse_yaml_frontmatter(file_path)
|
|
602
|
+
if not project_data:
|
|
603
|
+
logger.warning(
|
|
604
|
+
"project_no_yaml_data_found",
|
|
605
|
+
file_path=str(file_path),
|
|
606
|
+
severity="operational",
|
|
607
|
+
)
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
# Extract project ID
|
|
611
|
+
# If not in YAML, extract from filename (format: {uuid}-{name}.md)
|
|
612
|
+
if "id" in project_data:
|
|
613
|
+
project_id = project_data["id"]
|
|
614
|
+
else:
|
|
615
|
+
stem = file_path.stem
|
|
616
|
+
# Check if filename matches UUID pattern (8+ hex chars before hyphen)
|
|
617
|
+
parts = stem.split("-", 1)
|
|
618
|
+
first_part = parts[0]
|
|
619
|
+
# Only treat as UUID if first part is 8+ hex characters
|
|
620
|
+
if len(first_part) >= 8 and all(
|
|
621
|
+
c in "0123456789abcdefABCDEF" for c in first_part
|
|
622
|
+
):
|
|
623
|
+
project_id = first_part # Use UUID only
|
|
624
|
+
else:
|
|
625
|
+
project_id = stem # Use full filename as ID
|
|
626
|
+
project_data["id"] = project_id
|
|
627
|
+
|
|
628
|
+
# Set defaults
|
|
629
|
+
name = project_data.get("name") or project_data.get(
|
|
630
|
+
"title", "Untitled Project"
|
|
631
|
+
)
|
|
632
|
+
project_data["name"] = name
|
|
633
|
+
project_data["status"] = project_data.get("status", "active")
|
|
634
|
+
|
|
635
|
+
# Extract metadata
|
|
636
|
+
exclude_fields = ["id", "name", "description", "status"]
|
|
637
|
+
metadata = self._extract_metadata(project_data, exclude_fields)
|
|
638
|
+
|
|
639
|
+
# Upsert project
|
|
640
|
+
with self._transaction() as conn:
|
|
641
|
+
conn.execute(
|
|
642
|
+
"""
|
|
643
|
+
INSERT OR REPLACE INTO projects
|
|
644
|
+
(id, name, description, status, metadata)
|
|
645
|
+
VALUES (?, ?, ?, ?, ?)
|
|
646
|
+
""",
|
|
647
|
+
(
|
|
648
|
+
project_data["id"],
|
|
649
|
+
project_data["name"],
|
|
650
|
+
project_data.get("description"),
|
|
651
|
+
project_data["status"],
|
|
652
|
+
metadata,
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Update sync status
|
|
657
|
+
self._update_sync_status(file_path)
|
|
658
|
+
logger.info(
|
|
659
|
+
"synced_project_file",
|
|
660
|
+
project_id=project_id,
|
|
661
|
+
file_path=str(file_path),
|
|
662
|
+
)
|
|
663
|
+
return True
|
|
664
|
+
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.error(
|
|
667
|
+
"failed_to_sync_project_file",
|
|
668
|
+
file_path=str(file_path),
|
|
669
|
+
error=str(e),
|
|
670
|
+
severity="operational",
|
|
671
|
+
)
|
|
672
|
+
return False
|