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,245 @@
|
|
|
1
|
+
"""Parser for issue markdown files."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from roadmap.common.datetime_parser import parse_datetime
|
|
9
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
10
|
+
from roadmap.core.domain import Issue, IssueType, Priority, Status
|
|
11
|
+
|
|
12
|
+
from ..persistence import enhanced_persistence
|
|
13
|
+
from .frontmatter import FrontmatterParser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IssueParser:
|
|
17
|
+
"""Parser specifically for issue markdown files."""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def _parse_datetime_field(frontmatter: dict, field_name: str) -> None:
|
|
21
|
+
"""Parse a datetime field in frontmatter if present and string."""
|
|
22
|
+
if field_name in frontmatter and isinstance(frontmatter[field_name], str):
|
|
23
|
+
frontmatter[field_name] = parse_datetime(frontmatter[field_name], "file")
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _parse_enum_field(
|
|
27
|
+
frontmatter: dict, field_name: str, enum_class, file_path: Path
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Parse enum field with validation."""
|
|
30
|
+
if field_name not in frontmatter:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
frontmatter[field_name] = enum_class(frontmatter[field_name])
|
|
35
|
+
except ValueError as e:
|
|
36
|
+
valid_values = [v.value for v in enum_class]
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Invalid {field_name} '{frontmatter[field_name]}' in {file_path}. "
|
|
39
|
+
f"Valid values are: {', '.join(valid_values)}"
|
|
40
|
+
) from e
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _ensure_list_fields(frontmatter: dict) -> None:
|
|
44
|
+
"""Ensure dependency fields are lists."""
|
|
45
|
+
if "depends_on" not in frontmatter:
|
|
46
|
+
frontmatter["depends_on"] = []
|
|
47
|
+
if "blocks" not in frontmatter:
|
|
48
|
+
frontmatter["blocks"] = []
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def parse_issue_file(cls, file_path: Path) -> Issue:
|
|
52
|
+
"""Parse an issue markdown file and return an Issue object."""
|
|
53
|
+
frontmatter, content = FrontmatterParser.parse_file(file_path)
|
|
54
|
+
|
|
55
|
+
# Parse date fields
|
|
56
|
+
cls._parse_datetime_field(frontmatter, "created")
|
|
57
|
+
cls._parse_datetime_field(frontmatter, "updated")
|
|
58
|
+
cls._parse_datetime_field(frontmatter, "actual_start_date")
|
|
59
|
+
cls._parse_datetime_field(frontmatter, "actual_end_date")
|
|
60
|
+
cls._parse_datetime_field(frontmatter, "handoff_date")
|
|
61
|
+
|
|
62
|
+
# Parse enum fields with validation
|
|
63
|
+
cls._parse_enum_field(frontmatter, "priority", Priority, file_path)
|
|
64
|
+
cls._parse_enum_field(frontmatter, "status", Status, file_path)
|
|
65
|
+
cls._parse_enum_field(frontmatter, "issue_type", IssueType, file_path)
|
|
66
|
+
|
|
67
|
+
# Ensure list fields exist
|
|
68
|
+
cls._ensure_list_fields(frontmatter)
|
|
69
|
+
|
|
70
|
+
# Map legacy "description" field to "content" if present
|
|
71
|
+
if "description" in frontmatter and "content" not in frontmatter:
|
|
72
|
+
frontmatter["content"] = frontmatter.pop("description")
|
|
73
|
+
else:
|
|
74
|
+
# Otherwise use the body content
|
|
75
|
+
frontmatter["content"] = content
|
|
76
|
+
|
|
77
|
+
# If headline is not provided, use first line of content as fallback
|
|
78
|
+
if "headline" not in frontmatter or not frontmatter["headline"]:
|
|
79
|
+
first_line = content.split("\n")[0] if content else ""
|
|
80
|
+
frontmatter["headline"] = first_line[:100] # Limit to 100 chars
|
|
81
|
+
|
|
82
|
+
return Issue(**frontmatter)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def save_issue_file(
|
|
86
|
+
cls, issue: Issue, file_path: Path, sync_metadata: dict | None = None
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Save an Issue object to a markdown file.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
issue: Issue object to save
|
|
92
|
+
file_path: Path to write the file to
|
|
93
|
+
sync_metadata: Optional sync metadata to include in frontmatter
|
|
94
|
+
"""
|
|
95
|
+
frontmatter = issue.model_dump(exclude={"content", "file_path"})
|
|
96
|
+
|
|
97
|
+
# Add sync_metadata if provided
|
|
98
|
+
if sync_metadata:
|
|
99
|
+
FrontmatterParser.update_sync_metadata(frontmatter, sync_metadata)
|
|
100
|
+
|
|
101
|
+
FrontmatterParser.serialize_file(frontmatter, issue.content, file_path)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def load_sync_metadata(cls, file_path: Path) -> dict | None:
|
|
105
|
+
"""Load sync_metadata from an issue file without parsing the full issue.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
file_path: Path to the issue file
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Sync metadata dictionary or None if not present
|
|
112
|
+
"""
|
|
113
|
+
frontmatter, _ = FrontmatterParser.parse_file(file_path)
|
|
114
|
+
return FrontmatterParser.extract_sync_metadata(frontmatter)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def update_issue_sync_metadata(cls, file_path: Path, sync_metadata: dict) -> None:
|
|
118
|
+
"""Update only the sync_metadata in an issue file without parsing the full Issue.
|
|
119
|
+
|
|
120
|
+
This is useful for updating sync state without deserializing and re-serializing
|
|
121
|
+
the entire Issue object.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
file_path: Path to the issue file
|
|
125
|
+
sync_metadata: Sync metadata to set
|
|
126
|
+
"""
|
|
127
|
+
frontmatter, content = FrontmatterParser.parse_file(file_path)
|
|
128
|
+
FrontmatterParser.update_sync_metadata(frontmatter, sync_metadata)
|
|
129
|
+
FrontmatterParser.serialize_file(frontmatter, content, file_path)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def _validate_enum_field(
|
|
133
|
+
cls, field_name: str, value: str | None, enum_class
|
|
134
|
+
) -> tuple[bool, Any, str | None]:
|
|
135
|
+
"""Validate and convert enum field. Returns (success, value, error_message)."""
|
|
136
|
+
if not value:
|
|
137
|
+
return True, None, None
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
return True, enum_class(value), None
|
|
141
|
+
except ValueError:
|
|
142
|
+
valid_values = [e.value for e in enum_class]
|
|
143
|
+
error_msg = (
|
|
144
|
+
f"Invalid {field_name}: {value}. Valid: {', '.join(valid_values)}"
|
|
145
|
+
)
|
|
146
|
+
return False, None, error_msg
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _extract_issue_dates(
|
|
150
|
+
cls, data: dict
|
|
151
|
+
) -> tuple[datetime, datetime, datetime | None]:
|
|
152
|
+
"""Extract and parse date fields from data."""
|
|
153
|
+
now = now_utc()
|
|
154
|
+
created = cls._parse_datetime(data.get("created")) or now
|
|
155
|
+
updated = cls._parse_datetime(data.get("updated")) or now
|
|
156
|
+
due_date = cls._parse_datetime(data.get("due_date"))
|
|
157
|
+
return created, updated, due_date
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def _build_issue_from_data(
|
|
161
|
+
cls, data: dict, file_path: Path
|
|
162
|
+
) -> tuple[bool, Issue | None, str | None]:
|
|
163
|
+
"""Build Issue object from validated data. Returns (success, issue, error_message)."""
|
|
164
|
+
# Validate priority
|
|
165
|
+
success, priority, error = cls._validate_enum_field(
|
|
166
|
+
"priority", data.get("priority"), Priority
|
|
167
|
+
)
|
|
168
|
+
if not success:
|
|
169
|
+
return False, None, f"{error} in {file_path}"
|
|
170
|
+
priority = priority or Priority.MEDIUM
|
|
171
|
+
|
|
172
|
+
# Validate status
|
|
173
|
+
success, status, error = cls._validate_enum_field(
|
|
174
|
+
"status", data.get("status"), Status
|
|
175
|
+
)
|
|
176
|
+
if not success:
|
|
177
|
+
return False, None, f"{error} in {file_path}"
|
|
178
|
+
status = status or Status.TODO
|
|
179
|
+
|
|
180
|
+
# Validate issue_type
|
|
181
|
+
success, _, error = cls._validate_enum_field(
|
|
182
|
+
"issue_type", data.get("issue_type"), IssueType
|
|
183
|
+
)
|
|
184
|
+
if not success:
|
|
185
|
+
return False, None, f"{error} in {file_path}"
|
|
186
|
+
|
|
187
|
+
# Extract dates
|
|
188
|
+
created, updated, due_date = cls._extract_issue_dates(data)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
issue = Issue(
|
|
192
|
+
id=data.get("id") or str(uuid.uuid4())[:8],
|
|
193
|
+
title=data.get("title") or "Untitled Issue",
|
|
194
|
+
priority=priority,
|
|
195
|
+
status=status,
|
|
196
|
+
assignee=data.get("assignee"),
|
|
197
|
+
milestone=data.get("milestone"),
|
|
198
|
+
labels=data.get("labels", []),
|
|
199
|
+
created=created,
|
|
200
|
+
updated=updated,
|
|
201
|
+
due_date=due_date,
|
|
202
|
+
content=data.get("content", ""),
|
|
203
|
+
)
|
|
204
|
+
return True, issue, None
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return False, None, f"Error creating Issue: {e}"
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def parse_issue_file_safe(
|
|
210
|
+
cls, file_path: Path
|
|
211
|
+
) -> tuple[bool, Issue | None, str | None]:
|
|
212
|
+
"""Safely parse an issue file with enhanced validation and recovery.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
(success, issue, error_message)
|
|
216
|
+
"""
|
|
217
|
+
is_valid, result = enhanced_persistence.safe_load_with_validation(
|
|
218
|
+
file_path, "issue"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if not is_valid:
|
|
222
|
+
# result is a string error message
|
|
223
|
+
return False, None, str(result)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# Convert the validated data to an Issue
|
|
227
|
+
# At this point, result must be a dict since is_valid is True
|
|
228
|
+
data = result if isinstance(result, dict) else {}
|
|
229
|
+
return cls._build_issue_from_data(data, file_path)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
return False, None, f"Error processing issue file: {e}"
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def save_issue_file_safe(cls, issue: Issue, file_path: Path) -> tuple[bool, str]:
|
|
235
|
+
"""Safely save an issue file with automatic backup."""
|
|
236
|
+
try:
|
|
237
|
+
cls.save_issue_file(issue, file_path)
|
|
238
|
+
return True, "Issue saved successfully"
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return False, f"Error saving issue: {e}"
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def _parse_datetime(cls, value: Any) -> datetime | None:
|
|
244
|
+
"""Parse datetime from various formats with timezone awareness."""
|
|
245
|
+
return parse_datetime(value, source_type="file", assumed_timezone="UTC")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Parser for milestone markdown files."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roadmap.common.datetime_parser import parse_datetime
|
|
8
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
9
|
+
from roadmap.core.domain import Milestone, MilestoneStatus
|
|
10
|
+
|
|
11
|
+
from ..persistence import enhanced_persistence
|
|
12
|
+
from .frontmatter import FrontmatterParser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MilestoneParser:
|
|
16
|
+
"""Parser specifically for milestone markdown files."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def parse_milestone_file(cls, file_path: Path) -> Milestone:
|
|
20
|
+
"""Parse a milestone markdown file and return a Milestone object."""
|
|
21
|
+
frontmatter, content = FrontmatterParser.parse_file(file_path)
|
|
22
|
+
|
|
23
|
+
# Convert string dates back to datetime objects
|
|
24
|
+
for date_field in ["created", "updated", "due_date"]:
|
|
25
|
+
if date_field in frontmatter and isinstance(frontmatter[date_field], str):
|
|
26
|
+
frontmatter[date_field] = parse_datetime(
|
|
27
|
+
frontmatter[date_field], "file"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Backward-compatibility: some historical files may be missing `name`.
|
|
31
|
+
# Use filename stem so list/view commands remain consistent with disk state.
|
|
32
|
+
if not frontmatter.get("name"):
|
|
33
|
+
frontmatter["name"] = file_path.stem
|
|
34
|
+
|
|
35
|
+
# Convert string enums back to enum objects with validation
|
|
36
|
+
if "status" in frontmatter:
|
|
37
|
+
try:
|
|
38
|
+
frontmatter["status"] = MilestoneStatus(frontmatter["status"])
|
|
39
|
+
except ValueError as e:
|
|
40
|
+
valid_statuses = [s.value for s in MilestoneStatus]
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Invalid status '{frontmatter['status']}' in {file_path}. "
|
|
43
|
+
f"Valid status values are: {', '.join(valid_statuses)}"
|
|
44
|
+
) from e
|
|
45
|
+
|
|
46
|
+
# Map legacy "description" field to "content" if present
|
|
47
|
+
if "description" in frontmatter and "content" not in frontmatter:
|
|
48
|
+
frontmatter["content"] = frontmatter.pop("description")
|
|
49
|
+
else:
|
|
50
|
+
# Otherwise use the body content
|
|
51
|
+
frontmatter["content"] = content
|
|
52
|
+
|
|
53
|
+
# If headline is not provided, use first line of content as fallback
|
|
54
|
+
if "headline" not in frontmatter or not frontmatter["headline"]:
|
|
55
|
+
first_line = content.split("\n")[0] if content else ""
|
|
56
|
+
frontmatter["headline"] = first_line[:100] # Limit to 100 chars
|
|
57
|
+
|
|
58
|
+
return Milestone(**frontmatter)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def save_milestone_file(cls, milestone: Milestone, file_path: Path) -> None:
|
|
62
|
+
"""Save a Milestone object to a markdown file."""
|
|
63
|
+
frontmatter = milestone.model_dump(exclude={"content", "file_path"})
|
|
64
|
+
FrontmatterParser.serialize_file(frontmatter, milestone.content, file_path)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def _build_milestone_from_data(
|
|
68
|
+
cls, data: dict, file_path: Path
|
|
69
|
+
) -> tuple[bool, Milestone | None, str | None]:
|
|
70
|
+
"""Build Milestone object from parsed data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
data: Parsed milestone data
|
|
74
|
+
file_path: Path to milestone file (for error reporting)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
(success, milestone, error_message)
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Handle datetime fields - set defaults if None
|
|
81
|
+
now = now_utc()
|
|
82
|
+
created = cls._parse_datetime(data.get("created")) or now
|
|
83
|
+
updated = cls._parse_datetime(data.get("updated")) or now
|
|
84
|
+
due_date = cls._parse_datetime(data.get("due_date"))
|
|
85
|
+
|
|
86
|
+
milestone = Milestone(
|
|
87
|
+
name=data.get("name") or "Untitled Milestone",
|
|
88
|
+
content=data.get("description", ""),
|
|
89
|
+
status=MilestoneStatus(data.get("status"))
|
|
90
|
+
if data.get("status")
|
|
91
|
+
else MilestoneStatus.OPEN,
|
|
92
|
+
created=created,
|
|
93
|
+
updated=updated,
|
|
94
|
+
due_date=due_date,
|
|
95
|
+
)
|
|
96
|
+
return True, milestone, None
|
|
97
|
+
except ValueError as e:
|
|
98
|
+
# Check if it's an enum validation error
|
|
99
|
+
if "status" in str(e):
|
|
100
|
+
valid_statuses = [s.value for s in MilestoneStatus]
|
|
101
|
+
return (
|
|
102
|
+
False,
|
|
103
|
+
None,
|
|
104
|
+
f"Invalid status in {file_path}: {e}. Valid values: {', '.join(valid_statuses)}",
|
|
105
|
+
)
|
|
106
|
+
return False, None, f"Error creating Milestone object: {e}"
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return False, None, f"Error creating Milestone object: {e}"
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def parse_milestone_file_safe(
|
|
112
|
+
cls, file_path: Path
|
|
113
|
+
) -> tuple[bool, Milestone | None, str | None]:
|
|
114
|
+
"""Safely parse a milestone file with enhanced validation and recovery.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
(success, milestone, error_message)
|
|
118
|
+
"""
|
|
119
|
+
is_valid, result = enhanced_persistence.safe_load_with_validation(
|
|
120
|
+
file_path, "milestone"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not is_valid:
|
|
124
|
+
# result is a string error message
|
|
125
|
+
return False, None, str(result)
|
|
126
|
+
|
|
127
|
+
# Convert the validated data to a Milestone
|
|
128
|
+
# At this point, result must be a dict since is_valid is True
|
|
129
|
+
data = result if isinstance(result, dict) else {}
|
|
130
|
+
return cls._build_milestone_from_data(data, file_path)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def save_milestone_file_safe(
|
|
134
|
+
cls, milestone: Milestone, file_path: Path
|
|
135
|
+
) -> tuple[bool, str]:
|
|
136
|
+
"""Safely save a milestone file with automatic backup."""
|
|
137
|
+
try:
|
|
138
|
+
cls.save_milestone_file(milestone, file_path)
|
|
139
|
+
return True, "Milestone saved successfully"
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return False, f"Error saving milestone: {e}"
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def _parse_datetime(cls, value: Any) -> datetime | None:
|
|
145
|
+
"""Parse datetime from various formats with timezone awareness."""
|
|
146
|
+
return parse_datetime(value, source_type="file", assumed_timezone="UTC")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Parser for project markdown files."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roadmap.common.datetime_parser import parse_datetime
|
|
8
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
9
|
+
from roadmap.core.domain import Priority, Project, ProjectStatus
|
|
10
|
+
|
|
11
|
+
from .frontmatter import FrontmatterParser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectParser:
|
|
15
|
+
"""Parser for project markdown files."""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def parse_project_file(cls, file_path: Path) -> Project:
|
|
19
|
+
"""Parse a project file and return a Project instance."""
|
|
20
|
+
frontmatter, content = FrontmatterParser.parse_file(file_path)
|
|
21
|
+
|
|
22
|
+
# Extract ID from frontmatter, or from filename if not present
|
|
23
|
+
project_id = frontmatter.get("id", "")
|
|
24
|
+
if not project_id:
|
|
25
|
+
# Extract ID from filename (format: {id}-{name}.md)
|
|
26
|
+
filename_stem = file_path.stem
|
|
27
|
+
if "-" in filename_stem:
|
|
28
|
+
project_id = filename_stem.split("-", 1)[0]
|
|
29
|
+
|
|
30
|
+
# Convert frontmatter to Project model
|
|
31
|
+
project = Project(
|
|
32
|
+
id=project_id,
|
|
33
|
+
name=frontmatter.get("name", ""),
|
|
34
|
+
headline=frontmatter.get("headline", ""),
|
|
35
|
+
status=ProjectStatus(frontmatter.get("status", "planning")),
|
|
36
|
+
priority=Priority(frontmatter.get("priority", "medium")),
|
|
37
|
+
owner=frontmatter.get("owner"),
|
|
38
|
+
start_date=cls._parse_datetime(frontmatter.get("start_date")),
|
|
39
|
+
target_end_date=cls._parse_datetime(frontmatter.get("target_end_date")),
|
|
40
|
+
actual_end_date=cls._parse_datetime(frontmatter.get("actual_end_date")),
|
|
41
|
+
created=cls._parse_datetime(frontmatter.get("created")) or now_utc(),
|
|
42
|
+
updated=cls._parse_datetime(frontmatter.get("updated")) or now_utc(),
|
|
43
|
+
milestones=frontmatter.get("milestones", []),
|
|
44
|
+
estimated_hours=frontmatter.get("estimated_hours"),
|
|
45
|
+
actual_hours=frontmatter.get("actual_hours"),
|
|
46
|
+
content=content,
|
|
47
|
+
calculated_progress=frontmatter.get("calculated_progress"),
|
|
48
|
+
last_progress_update=cls._parse_datetime(
|
|
49
|
+
frontmatter.get("last_progress_update")
|
|
50
|
+
),
|
|
51
|
+
projected_end_date=cls._parse_datetime(
|
|
52
|
+
frontmatter.get("projected_end_date")
|
|
53
|
+
),
|
|
54
|
+
schedule_variance=frontmatter.get("schedule_variance"),
|
|
55
|
+
completion_velocity=frontmatter.get("completion_velocity"),
|
|
56
|
+
risk_level=frontmatter.get("risk_level", "low"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return project
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def save_project_file(cls, project: Project, file_path: Path) -> None:
|
|
63
|
+
"""Save a project to a markdown file."""
|
|
64
|
+
# Create frontmatter from project data
|
|
65
|
+
frontmatter = {
|
|
66
|
+
"id": project.id,
|
|
67
|
+
"name": project.name,
|
|
68
|
+
"headline": project.headline,
|
|
69
|
+
"status": project.status.value,
|
|
70
|
+
"priority": project.priority.value,
|
|
71
|
+
"owner": project.owner,
|
|
72
|
+
"start_date": project.start_date.isoformat()
|
|
73
|
+
if project.start_date
|
|
74
|
+
else None,
|
|
75
|
+
"target_end_date": project.target_end_date.isoformat()
|
|
76
|
+
if project.target_end_date
|
|
77
|
+
else None,
|
|
78
|
+
"actual_end_date": project.actual_end_date.isoformat()
|
|
79
|
+
if project.actual_end_date
|
|
80
|
+
else None,
|
|
81
|
+
"created": project.created.isoformat(),
|
|
82
|
+
"updated": project.updated.isoformat(),
|
|
83
|
+
"milestones": project.milestones,
|
|
84
|
+
"estimated_hours": project.estimated_hours,
|
|
85
|
+
"actual_hours": project.actual_hours,
|
|
86
|
+
"calculated_progress": project.calculated_progress,
|
|
87
|
+
"last_progress_update": project.last_progress_update.isoformat()
|
|
88
|
+
if project.last_progress_update
|
|
89
|
+
else None,
|
|
90
|
+
"projected_end_date": project.projected_end_date.isoformat()
|
|
91
|
+
if project.projected_end_date
|
|
92
|
+
else None,
|
|
93
|
+
"schedule_variance": project.schedule_variance,
|
|
94
|
+
"completion_velocity": project.completion_velocity,
|
|
95
|
+
"risk_level": project.risk_level.value,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Remove None values
|
|
99
|
+
frontmatter = {k: v for k, v in frontmatter.items() if v is not None}
|
|
100
|
+
|
|
101
|
+
FrontmatterParser.serialize_file(frontmatter, project.content, file_path)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _parse_datetime(cls, value: Any) -> datetime | None:
|
|
105
|
+
"""Parse datetime from various formats with timezone awareness."""
|
|
106
|
+
return parse_datetime(value, source_type="file", assumed_timezone="UTC")
|