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,711 @@
|
|
|
1
|
+
"""YAML-based repository implementations.
|
|
2
|
+
|
|
3
|
+
This module provides concrete implementations of repository protocols
|
|
4
|
+
using YAML file storage, maintaining backward compatibility with existing
|
|
5
|
+
file-based storage while decoupling services from implementation details.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from roadmap.adapters.persistence.parser import (
|
|
14
|
+
IssueParser,
|
|
15
|
+
MilestoneParser,
|
|
16
|
+
ProjectParser,
|
|
17
|
+
)
|
|
18
|
+
from roadmap.adapters.persistence.storage import StateManager
|
|
19
|
+
from roadmap.common.logging import get_logger
|
|
20
|
+
from roadmap.core.domain import Status
|
|
21
|
+
from roadmap.core.domain.issue import Issue
|
|
22
|
+
from roadmap.core.domain.milestone import Milestone
|
|
23
|
+
from roadmap.core.domain.project import Project
|
|
24
|
+
from roadmap.core.repositories import (
|
|
25
|
+
IssueRepository,
|
|
26
|
+
MilestoneRepository,
|
|
27
|
+
ProjectRepository,
|
|
28
|
+
)
|
|
29
|
+
from roadmap.infrastructure.validation.file_enumeration import FileEnumerationService
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class YAMLIssueRepository(IssueRepository):
|
|
35
|
+
"""Issue repository using YAML file storage.
|
|
36
|
+
|
|
37
|
+
This repository implements a file-based storage system with the following
|
|
38
|
+
organizational contract:
|
|
39
|
+
|
|
40
|
+
**File Organization by Milestone:**
|
|
41
|
+
- Issues with no milestone → `.roadmap/issues/backlog/`
|
|
42
|
+
- Issues with milestone "M1" → `.roadmap/issues/M1/`
|
|
43
|
+
- Archived issues with no milestone → `.roadmap/archive/issues/backlog/`
|
|
44
|
+
- Archived issues with milestone "M1" → `.roadmap/archive/issues/M1/`
|
|
45
|
+
|
|
46
|
+
**Duplicate Prevention:**
|
|
47
|
+
- save() method cleans up stale copies in other directories before writing
|
|
48
|
+
- update() with milestone change uses atomic file moves
|
|
49
|
+
- Only one file per issue exists at any time
|
|
50
|
+
|
|
51
|
+
**Key Invariants:**
|
|
52
|
+
- Each issue has exactly one file in the filesystem
|
|
53
|
+
- File location matches issue.milestone value
|
|
54
|
+
- issue.file_path always points to actual file location
|
|
55
|
+
- No orphaned copies left in subdirectories after moves
|
|
56
|
+
|
|
57
|
+
**Error Handling:**
|
|
58
|
+
- If cleanup fails, logs warning but continues (fail-open)
|
|
59
|
+
- If save fails, raises exception (fail-safe)
|
|
60
|
+
- Atomic rename ensures consistency
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, db: StateManager, issues_dir: Path):
|
|
64
|
+
"""Initialize issue repository.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
db: State manager for metadata operations
|
|
68
|
+
issues_dir: Path to issues directory containing issue markdown files
|
|
69
|
+
"""
|
|
70
|
+
self.db = db
|
|
71
|
+
self.issues_dir = issues_dir
|
|
72
|
+
|
|
73
|
+
def get(self, issue_id: str) -> Issue | None:
|
|
74
|
+
"""Get a specific issue by ID.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
issue_id: Issue identifier
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Issue object if found, None otherwise
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def id_matcher(issue: Issue) -> bool:
|
|
84
|
+
return issue.id.startswith(issue_id)
|
|
85
|
+
|
|
86
|
+
issues = FileEnumerationService.enumerate_with_filter(
|
|
87
|
+
self.issues_dir,
|
|
88
|
+
IssueParser.parse_issue_file,
|
|
89
|
+
id_matcher,
|
|
90
|
+
)
|
|
91
|
+
if not issues:
|
|
92
|
+
archive_dir = self.issues_dir.parent / "archive" / "issues"
|
|
93
|
+
if archive_dir.exists():
|
|
94
|
+
issues = FileEnumerationService.enumerate_with_filter(
|
|
95
|
+
archive_dir,
|
|
96
|
+
IssueParser.parse_issue_file,
|
|
97
|
+
id_matcher,
|
|
98
|
+
)
|
|
99
|
+
return issues[0] if issues else None
|
|
100
|
+
|
|
101
|
+
def list(
|
|
102
|
+
self, milestone: str | None = None, status: str | None = None
|
|
103
|
+
) -> list[Issue]:
|
|
104
|
+
"""List issues with optional filtering (active issues only).
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
milestone: Optional milestone filter
|
|
108
|
+
status: Optional status filter
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of active Issue objects matching filters (not including archived)
|
|
112
|
+
"""
|
|
113
|
+
# Get issues from active directory only
|
|
114
|
+
issues = FileEnumerationService.enumerate_and_parse(
|
|
115
|
+
self.issues_dir,
|
|
116
|
+
IssueParser.parse_issue_file,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if milestone:
|
|
120
|
+
issues = [i for i in issues if i.milestone == milestone]
|
|
121
|
+
if status:
|
|
122
|
+
issues = [i for i in issues if i.status.value == status]
|
|
123
|
+
|
|
124
|
+
return issues
|
|
125
|
+
|
|
126
|
+
def list_all_including_archived(
|
|
127
|
+
self, milestone: str | None = None, status: str | None = None
|
|
128
|
+
) -> list[Issue]:
|
|
129
|
+
"""List all issues including archived (for sync operations).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
milestone: Optional milestone filter
|
|
133
|
+
status: Optional status filter
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of all Issue objects matching filters (from both active and archived)
|
|
137
|
+
"""
|
|
138
|
+
# Get issues from active directory
|
|
139
|
+
issues = FileEnumerationService.enumerate_and_parse(
|
|
140
|
+
self.issues_dir,
|
|
141
|
+
IssueParser.parse_issue_file,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Also get issues from archive directory if it exists
|
|
145
|
+
archive_dir = self.issues_dir.parent / "archive" / "issues"
|
|
146
|
+
if archive_dir.exists():
|
|
147
|
+
archived_issues = FileEnumerationService.enumerate_and_parse(
|
|
148
|
+
archive_dir,
|
|
149
|
+
IssueParser.parse_issue_file,
|
|
150
|
+
)
|
|
151
|
+
issues.extend(archived_issues)
|
|
152
|
+
|
|
153
|
+
if milestone:
|
|
154
|
+
issues = [i for i in issues if i.milestone == milestone]
|
|
155
|
+
if status:
|
|
156
|
+
issues = [i for i in issues if i.status.value == status]
|
|
157
|
+
|
|
158
|
+
return issues
|
|
159
|
+
|
|
160
|
+
def save(self, issue: Issue) -> None:
|
|
161
|
+
"""Save/create an issue.
|
|
162
|
+
|
|
163
|
+
Ensures single file location by cleaning up any stale copies in other directories.
|
|
164
|
+
This prevents duplicate files when issues are moved between milestones or when
|
|
165
|
+
the issue filename changes (e.g., due to title updates).
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
issue: Issue object to save
|
|
169
|
+
"""
|
|
170
|
+
archived = self._is_archived_issue(issue)
|
|
171
|
+
|
|
172
|
+
# Determine target directory based on milestone and archive state
|
|
173
|
+
target_dir = self._get_milestone_dir(issue.milestone, archived=archived)
|
|
174
|
+
|
|
175
|
+
# Create directory if it doesn't exist
|
|
176
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
# Check if file exists in other locations and remove it
|
|
179
|
+
# This handles issue moves between milestones
|
|
180
|
+
issue_path_target = target_dir / issue.filename
|
|
181
|
+
stale_files_removed = 0
|
|
182
|
+
|
|
183
|
+
# Search for any existing files with this issue ID in both active and archive
|
|
184
|
+
# trees so archive state changes do not leave duplicate copies behind.
|
|
185
|
+
issue_id_prefix = issue.id
|
|
186
|
+
for existing_file in self._find_issue_files_by_id(issue_id_prefix):
|
|
187
|
+
if existing_file != issue_path_target:
|
|
188
|
+
try:
|
|
189
|
+
existing_file.unlink()
|
|
190
|
+
stale_files_removed += 1
|
|
191
|
+
logger.debug(
|
|
192
|
+
"removed_stale_issue_file_by_id",
|
|
193
|
+
issue_id=issue.id,
|
|
194
|
+
old_filename=existing_file.name,
|
|
195
|
+
removed_path=str(existing_file),
|
|
196
|
+
target_path=str(issue_path_target),
|
|
197
|
+
)
|
|
198
|
+
except (OSError, PermissionError) as e:
|
|
199
|
+
logger.warning(
|
|
200
|
+
"failed_to_remove_stale_issue_file_by_id",
|
|
201
|
+
issue_id=issue.id,
|
|
202
|
+
old_filename=existing_file.name,
|
|
203
|
+
path=str(existing_file),
|
|
204
|
+
error=str(e),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Save file to proper location
|
|
208
|
+
IssueParser.save_issue_file(issue, issue_path_target)
|
|
209
|
+
logger.debug(
|
|
210
|
+
"issue_saved",
|
|
211
|
+
issue_id=issue.id,
|
|
212
|
+
issue_filename=issue.filename,
|
|
213
|
+
milestone=issue.milestone,
|
|
214
|
+
target_directory=str(target_dir),
|
|
215
|
+
stale_files_removed=stale_files_removed,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Set the file_path on the issue object so it reflects the saved location
|
|
219
|
+
issue.file_path = str(issue_path_target)
|
|
220
|
+
|
|
221
|
+
def update(self, issue_id: str, updates: dict) -> Issue | None:
|
|
222
|
+
"""Update specific fields of an issue.
|
|
223
|
+
|
|
224
|
+
When milestone changes, moves file to appropriate directory and removes
|
|
225
|
+
any stale copies from other locations. For non-milestone updates, saves
|
|
226
|
+
to current file location without moving.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
issue_id: Issue identifier
|
|
230
|
+
updates: Dictionary of field updates (may include milestone to trigger move)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Updated Issue object if found, None otherwise
|
|
234
|
+
"""
|
|
235
|
+
issue = self.get(issue_id)
|
|
236
|
+
if not issue:
|
|
237
|
+
logger.debug("issue_not_found_for_update", issue_id=issue_id)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
# Track old state before modification
|
|
241
|
+
old_milestone = issue.milestone
|
|
242
|
+
old_filename = issue.filename
|
|
243
|
+
|
|
244
|
+
logger.debug(
|
|
245
|
+
"updating_issue",
|
|
246
|
+
issue_id=issue_id,
|
|
247
|
+
old_milestone=old_milestone,
|
|
248
|
+
update_fields=list(updates.keys()),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Apply updates to issue object
|
|
252
|
+
for key, value in updates.items():
|
|
253
|
+
if hasattr(issue, key):
|
|
254
|
+
setattr(issue, key, value)
|
|
255
|
+
|
|
256
|
+
# Handle file system changes based on what was updated
|
|
257
|
+
if "milestone" in updates and old_milestone != issue.milestone:
|
|
258
|
+
self._handle_milestone_change(issue, old_milestone)
|
|
259
|
+
elif old_filename != issue.filename:
|
|
260
|
+
self._handle_filename_change(issue, old_filename)
|
|
261
|
+
|
|
262
|
+
# Save and return updated issue
|
|
263
|
+
self.save(issue)
|
|
264
|
+
return issue
|
|
265
|
+
|
|
266
|
+
def _handle_milestone_change(self, issue: Issue, old_milestone: str | None) -> None:
|
|
267
|
+
"""Handle file move when issue's milestone changes.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
issue: Updated issue object
|
|
271
|
+
old_milestone: Previous milestone value
|
|
272
|
+
"""
|
|
273
|
+
archived = self._is_archived_issue(issue)
|
|
274
|
+
old_path = self._get_issue_path(
|
|
275
|
+
old_milestone, issue.filename, archived=archived
|
|
276
|
+
)
|
|
277
|
+
new_path = self._get_issue_path(
|
|
278
|
+
issue.milestone, issue.filename, archived=archived
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Create destination directory if needed
|
|
282
|
+
new_path.parent.mkdir(parents=True, exist_ok=True)
|
|
283
|
+
|
|
284
|
+
# Move file to new location
|
|
285
|
+
if old_path.exists():
|
|
286
|
+
try:
|
|
287
|
+
shutil.move(str(old_path), str(new_path))
|
|
288
|
+
except (OSError, PermissionError):
|
|
289
|
+
# If move fails, save to new location directly
|
|
290
|
+
IssueParser.save_issue_file(issue, new_path)
|
|
291
|
+
else:
|
|
292
|
+
# File doesn't exist at old location, save to new location
|
|
293
|
+
IssueParser.save_issue_file(issue, new_path)
|
|
294
|
+
|
|
295
|
+
# Clean up stale copies from other directories
|
|
296
|
+
self._cleanup_stale_files(issue.filename, new_path.parent)
|
|
297
|
+
|
|
298
|
+
# Update the issue's file path
|
|
299
|
+
issue.file_path = str(new_path)
|
|
300
|
+
|
|
301
|
+
logger.info(
|
|
302
|
+
"issue_milestone_updated",
|
|
303
|
+
issue_id=issue.id,
|
|
304
|
+
old_milestone=old_milestone,
|
|
305
|
+
new_milestone=issue.milestone,
|
|
306
|
+
new_path=str(new_path),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _handle_filename_change(self, issue: Issue, old_filename: str) -> None:
|
|
310
|
+
"""Handle file removal when issue's filename changes.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
issue: Updated issue object
|
|
314
|
+
old_filename: Previous filename
|
|
315
|
+
"""
|
|
316
|
+
# Get current directory for issue
|
|
317
|
+
target_dir = self._get_milestone_dir(
|
|
318
|
+
issue.milestone, archived=self._is_archived_issue(issue)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Remove old file from target directory
|
|
322
|
+
old_path = target_dir / old_filename
|
|
323
|
+
if old_path.exists():
|
|
324
|
+
try:
|
|
325
|
+
old_path.unlink()
|
|
326
|
+
logger.debug(
|
|
327
|
+
"removed_old_filename_after_update",
|
|
328
|
+
issue_id=issue.id,
|
|
329
|
+
old_filename=old_filename,
|
|
330
|
+
)
|
|
331
|
+
except (OSError, PermissionError) as e:
|
|
332
|
+
logger.warning(
|
|
333
|
+
"failed_to_remove_old_filename",
|
|
334
|
+
issue_id=issue.id,
|
|
335
|
+
error=str(e),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Also check for stale copies in other directories
|
|
339
|
+
self._cleanup_stale_files(old_filename, target_dir)
|
|
340
|
+
|
|
341
|
+
def _get_issue_path(
|
|
342
|
+
self, milestone: str | None, filename: str, archived: bool = False
|
|
343
|
+
) -> Path:
|
|
344
|
+
"""Get the full path for an issue file.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
milestone: Issue milestone (None or "backlog" means backlog dir)
|
|
348
|
+
filename: Issue filename
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Full path to issue file
|
|
352
|
+
"""
|
|
353
|
+
return self._get_milestone_dir(milestone, archived=archived) / filename
|
|
354
|
+
|
|
355
|
+
def _get_milestone_dir(self, milestone: str | None, archived: bool = False) -> Path:
|
|
356
|
+
"""Get directory for a milestone.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
milestone: Milestone name (None or "backlog" means backlog)
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Directory path
|
|
363
|
+
"""
|
|
364
|
+
root_dir = self._get_issue_root(archived=archived)
|
|
365
|
+
if milestone and milestone != "backlog":
|
|
366
|
+
return root_dir / milestone
|
|
367
|
+
return root_dir / "backlog"
|
|
368
|
+
|
|
369
|
+
def _get_issue_root(self, archived: bool = False) -> Path:
|
|
370
|
+
"""Get the root directory for active or archived issues."""
|
|
371
|
+
if archived:
|
|
372
|
+
return self.issues_dir.parent / "archive" / "issues"
|
|
373
|
+
return self.issues_dir
|
|
374
|
+
|
|
375
|
+
def _find_issue_files_by_id(self, issue_id_prefix: str) -> list[Path]:
|
|
376
|
+
"""Find issue files across active and archive trees by ID prefix."""
|
|
377
|
+
issue_files: list[Path] = []
|
|
378
|
+
for root_dir in (
|
|
379
|
+
self._get_issue_root(archived=False),
|
|
380
|
+
self._get_issue_root(archived=True),
|
|
381
|
+
):
|
|
382
|
+
if not root_dir.exists():
|
|
383
|
+
continue
|
|
384
|
+
issue_files.extend(root_dir.rglob(f"{issue_id_prefix}-*.md"))
|
|
385
|
+
return issue_files
|
|
386
|
+
|
|
387
|
+
def _is_archived_issue(self, issue: Issue) -> bool:
|
|
388
|
+
"""Determine whether an issue should live under the archive tree."""
|
|
389
|
+
status = getattr(issue.status, "value", issue.status)
|
|
390
|
+
return bool(issue.archived or status == Status.ARCHIVED.value)
|
|
391
|
+
|
|
392
|
+
def _cleanup_stale_files(self, filename: str, exclude_dir: Path) -> None:
|
|
393
|
+
"""Remove stale copies of a file from other directories.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
filename: Filename to search for
|
|
397
|
+
exclude_dir: Directory to exclude from cleanup
|
|
398
|
+
"""
|
|
399
|
+
for root_dir in (
|
|
400
|
+
self._get_issue_root(archived=False),
|
|
401
|
+
self._get_issue_root(archived=True),
|
|
402
|
+
):
|
|
403
|
+
if not root_dir.exists():
|
|
404
|
+
continue
|
|
405
|
+
for subdir in root_dir.glob("*"):
|
|
406
|
+
if subdir.is_dir() and subdir != exclude_dir:
|
|
407
|
+
stale_path = subdir / filename
|
|
408
|
+
if stale_path.exists():
|
|
409
|
+
try:
|
|
410
|
+
stale_path.unlink()
|
|
411
|
+
except (OSError, PermissionError):
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
def delete(self, issue_id: str) -> bool:
|
|
415
|
+
"""Delete an issue.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
issue_id: Issue identifier
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
True if deleted, False if not found
|
|
422
|
+
"""
|
|
423
|
+
issue = self.get(issue_id)
|
|
424
|
+
if not issue:
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
issue_path = self._get_issue_path(
|
|
428
|
+
issue.milestone,
|
|
429
|
+
issue.filename,
|
|
430
|
+
archived=self._is_archived_issue(issue),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if issue_path.exists():
|
|
434
|
+
try:
|
|
435
|
+
issue_path.unlink()
|
|
436
|
+
except (OSError, PermissionError):
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
def delete_many(self, issue_ids: list[str]) -> int:
|
|
442
|
+
"""Delete multiple issues in a single batch operation.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
issue_ids: List of issue identifiers to delete
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Number of issues successfully deleted
|
|
449
|
+
"""
|
|
450
|
+
if not issue_ids:
|
|
451
|
+
return 0
|
|
452
|
+
|
|
453
|
+
deleted_count = 0
|
|
454
|
+
for issue_id in issue_ids:
|
|
455
|
+
if self.delete(issue_id):
|
|
456
|
+
deleted_count += 1
|
|
457
|
+
|
|
458
|
+
return deleted_count
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class YAMLMilestoneRepository(MilestoneRepository):
|
|
462
|
+
"""Milestone repository using YAML file storage.
|
|
463
|
+
|
|
464
|
+
This repository stores milestone files in a flat structure:
|
|
465
|
+
|
|
466
|
+
**File Organization:**
|
|
467
|
+
- All milestones stored in `.roadmap/milestones/`
|
|
468
|
+
- Each milestone is one markdown file
|
|
469
|
+
- No subdirectories (unlike issues)
|
|
470
|
+
- Archived milestones → `.roadmap/archive/milestones/`
|
|
471
|
+
|
|
472
|
+
**Relationship to Issues:**
|
|
473
|
+
- Milestone names are used as directory names in `.roadmap/issues/{milestone_name}`
|
|
474
|
+
- Deleting milestone doesn't delete associated issues (only the milestone metadata)
|
|
475
|
+
- Issues maintain reference to milestone by name
|
|
476
|
+
|
|
477
|
+
**Update Behavior:**
|
|
478
|
+
- Updates do NOT move files
|
|
479
|
+
- All updates saved to same file location
|
|
480
|
+
- No stale file cleanup needed (flat structure)
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
def __init__(self, db: StateManager, milestones_dir: Path):
|
|
484
|
+
"""Initialize milestone repository.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
db: State manager for metadata operations
|
|
488
|
+
milestones_dir: Path to milestones directory
|
|
489
|
+
"""
|
|
490
|
+
self.db = db
|
|
491
|
+
self.milestones_dir = milestones_dir
|
|
492
|
+
|
|
493
|
+
def get(self, milestone_id: str) -> Milestone | None:
|
|
494
|
+
"""Get a specific milestone by ID or name.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
milestone_id: Milestone identifier or name
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Milestone object if found, None otherwise
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def id_matcher(milestone: Milestone) -> bool:
|
|
504
|
+
return milestone.name.lower().startswith(milestone_id.lower())
|
|
505
|
+
|
|
506
|
+
milestones = FileEnumerationService.enumerate_with_filter(
|
|
507
|
+
self.milestones_dir,
|
|
508
|
+
MilestoneParser.parse_milestone_file,
|
|
509
|
+
id_matcher,
|
|
510
|
+
warn_on_parse_error=True,
|
|
511
|
+
)
|
|
512
|
+
return milestones[0] if milestones else None
|
|
513
|
+
|
|
514
|
+
def list(self) -> list[Milestone]:
|
|
515
|
+
"""List all milestones.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
List of all Milestone objects
|
|
519
|
+
"""
|
|
520
|
+
return FileEnumerationService.enumerate_and_parse(
|
|
521
|
+
self.milestones_dir,
|
|
522
|
+
MilestoneParser.parse_milestone_file,
|
|
523
|
+
warn_on_parse_error=True,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
def save(self, milestone: Milestone) -> None:
|
|
527
|
+
"""Save/create a milestone.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
milestone: Milestone object to save
|
|
531
|
+
"""
|
|
532
|
+
milestone_path = self.milestones_dir / milestone.filename
|
|
533
|
+
MilestoneParser.save_milestone_file(milestone, milestone_path)
|
|
534
|
+
|
|
535
|
+
def update(self, milestone_id: str, updates: dict) -> Milestone | None:
|
|
536
|
+
"""Update specific fields of a milestone.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
milestone_id: Milestone identifier
|
|
540
|
+
updates: Dictionary of field updates
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Updated Milestone object if found, None otherwise
|
|
544
|
+
"""
|
|
545
|
+
milestone = self.get(milestone_id)
|
|
546
|
+
if not milestone:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
# Update fields
|
|
550
|
+
for key, value in updates.items():
|
|
551
|
+
if hasattr(milestone, key):
|
|
552
|
+
setattr(milestone, key, value)
|
|
553
|
+
|
|
554
|
+
# Save back
|
|
555
|
+
self.save(milestone)
|
|
556
|
+
return milestone
|
|
557
|
+
|
|
558
|
+
def delete(self, milestone_id: str) -> bool:
|
|
559
|
+
"""Delete a milestone.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
milestone_id: Milestone identifier
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
True if deleted, False if not found
|
|
566
|
+
"""
|
|
567
|
+
milestone = self.get(milestone_id)
|
|
568
|
+
if not milestone:
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
milestone_path = self.milestones_dir / milestone.filename
|
|
572
|
+
if milestone_path.exists():
|
|
573
|
+
try:
|
|
574
|
+
milestone_path.unlink()
|
|
575
|
+
except (OSError, PermissionError):
|
|
576
|
+
return False
|
|
577
|
+
|
|
578
|
+
return True
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class YAMLProjectRepository(ProjectRepository):
|
|
582
|
+
"""Project repository using YAML file storage.
|
|
583
|
+
|
|
584
|
+
This repository stores project files in a flat structure:
|
|
585
|
+
|
|
586
|
+
**File Organization:**
|
|
587
|
+
- All projects stored in `.roadmap/projects/`
|
|
588
|
+
- Each project is one markdown file
|
|
589
|
+
- No subdirectories
|
|
590
|
+
- Archived projects → `.roadmap/archive/projects/`
|
|
591
|
+
|
|
592
|
+
**Update Behavior:**
|
|
593
|
+
- Updates do NOT move files
|
|
594
|
+
- All updates saved to same file location
|
|
595
|
+
- No stale file cleanup needed (flat structure)
|
|
596
|
+
|
|
597
|
+
**Note:** Projects are distinct from issues and milestones. They can reference
|
|
598
|
+
milestones but don't affect the issue file organization.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
def __init__(self, db: StateManager, projects_dir: Path):
|
|
602
|
+
"""Initialize project repository.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
db: State manager for metadata operations
|
|
606
|
+
projects_dir: Path to projects directory
|
|
607
|
+
"""
|
|
608
|
+
self.db = db
|
|
609
|
+
self.projects_dir = projects_dir
|
|
610
|
+
|
|
611
|
+
def get(self, project_id: str) -> Project | None:
|
|
612
|
+
"""Get a specific project by ID.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
project_id: Project identifier
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
Project object if found, None otherwise
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
def id_matcher(project: Project) -> bool:
|
|
622
|
+
return project.id.startswith(project_id)
|
|
623
|
+
|
|
624
|
+
projects = FileEnumerationService.enumerate_with_filter(
|
|
625
|
+
self.projects_dir,
|
|
626
|
+
ProjectParser.parse_project_file,
|
|
627
|
+
id_matcher,
|
|
628
|
+
)
|
|
629
|
+
return projects[0] if projects else None
|
|
630
|
+
|
|
631
|
+
def list(self) -> list[Project]:
|
|
632
|
+
"""List all projects.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
List of all Project objects
|
|
636
|
+
"""
|
|
637
|
+
projects = FileEnumerationService.enumerate_and_parse(
|
|
638
|
+
self.projects_dir,
|
|
639
|
+
ProjectParser.parse_project_file,
|
|
640
|
+
)
|
|
641
|
+
return sorted(projects, key=lambda x: x.created)
|
|
642
|
+
|
|
643
|
+
def save(self, project: Project) -> None:
|
|
644
|
+
"""Save/create a project.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
project: Project object to save
|
|
648
|
+
"""
|
|
649
|
+
project_path = self.projects_dir / project.filename
|
|
650
|
+
ProjectParser.save_project_file(project, project_path)
|
|
651
|
+
|
|
652
|
+
# Persist metadata to database (non-blocking)
|
|
653
|
+
try:
|
|
654
|
+
self.db.create_project(
|
|
655
|
+
{
|
|
656
|
+
"title": project.name,
|
|
657
|
+
"description": project.content,
|
|
658
|
+
"status": project.status.value,
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
except Exception as e:
|
|
662
|
+
logger.debug(
|
|
663
|
+
"failed_to_persist_project_to_database",
|
|
664
|
+
error=str(e),
|
|
665
|
+
severity="operational",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def update(self, project_id: str, updates: dict) -> Project | None:
|
|
669
|
+
"""Update specific fields of a project.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
project_id: Project identifier
|
|
673
|
+
updates: Dictionary of field updates
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
Updated Project object if found, None otherwise
|
|
677
|
+
"""
|
|
678
|
+
project = self.get(project_id)
|
|
679
|
+
if not project:
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
# Update fields
|
|
683
|
+
for key, value in updates.items():
|
|
684
|
+
if hasattr(project, key):
|
|
685
|
+
setattr(project, key, value)
|
|
686
|
+
|
|
687
|
+
# Save back
|
|
688
|
+
self.save(project)
|
|
689
|
+
return project
|
|
690
|
+
|
|
691
|
+
def delete(self, project_id: str) -> bool:
|
|
692
|
+
"""Delete a project.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
project_id: Project identifier
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
True if deleted, False if not found
|
|
699
|
+
"""
|
|
700
|
+
project = self.get(project_id)
|
|
701
|
+
if not project:
|
|
702
|
+
return False
|
|
703
|
+
|
|
704
|
+
project_path = self.projects_dir / project.filename
|
|
705
|
+
if project_path.exists():
|
|
706
|
+
try:
|
|
707
|
+
project_path.unlink()
|
|
708
|
+
except (OSError, PermissionError):
|
|
709
|
+
return False
|
|
710
|
+
|
|
711
|
+
return True
|