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,330 @@
|
|
|
1
|
+
"""Git history utilities for accessing file content and baselines.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to reconstruct baseline state from git history,
|
|
4
|
+
enabling three-way merge without a database. Key functions:
|
|
5
|
+
|
|
6
|
+
- get_file_at_timestamp: Get file content as it existed at a specific time
|
|
7
|
+
- find_commit_at_time: Find the git commit closest to a timestamp
|
|
8
|
+
- get_file_at_commit: Get file content at a specific commit SHA
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from roadmap.common.logging import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GitHistoryError(Exception):
|
|
20
|
+
"""Base exception for git history operations."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotAGitRepository(GitHistoryError):
|
|
26
|
+
"""Raised when not in a git repository."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileNotFound(GitHistoryError):
|
|
32
|
+
"""Raised when file is not found in git history."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run_git_command(args: list[str], cwd: str = ".") -> str:
|
|
38
|
+
"""Run a git command and return stdout.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
args: Git command arguments (e.g., ['log', '--oneline'])
|
|
42
|
+
cwd: Working directory for the command
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Command stdout as string
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
NotAGitRepository: If not in a git repository
|
|
49
|
+
GitHistoryError: If git command fails
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
["git"] + args,
|
|
54
|
+
cwd=cwd,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
error_msg = result.stderr.strip()
|
|
62
|
+
|
|
63
|
+
# Check for "not a git repository" error
|
|
64
|
+
if "fatal: not a git repository" in error_msg.lower():
|
|
65
|
+
raise NotAGitRepository(f"Not in a git repository: {error_msg}")
|
|
66
|
+
|
|
67
|
+
raise GitHistoryError(f"Git command failed: {error_msg}")
|
|
68
|
+
|
|
69
|
+
return result.stdout.strip()
|
|
70
|
+
|
|
71
|
+
except FileNotFoundError as e:
|
|
72
|
+
raise GitHistoryError("Git not found in PATH") from e
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def find_commit_at_time(
|
|
76
|
+
timestamp: datetime | str, file_path: str | None = None, cwd: str = "."
|
|
77
|
+
) -> str:
|
|
78
|
+
"""Find the git commit closest to a given timestamp.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
timestamp: Datetime or ISO 8601 string to search for
|
|
82
|
+
file_path: Optional - only find commits affecting this file
|
|
83
|
+
cwd: Working directory
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Commit SHA of the closest commit
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
NotAGitRepository: If not in a git repository
|
|
90
|
+
GitHistoryError: If no commits found
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(timestamp, str):
|
|
93
|
+
try:
|
|
94
|
+
timestamp = datetime.fromisoformat(timestamp)
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
raise GitHistoryError(f"Invalid timestamp format: {timestamp}") from e
|
|
97
|
+
|
|
98
|
+
timestamp_str = timestamp.isoformat()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
args = [
|
|
102
|
+
"log",
|
|
103
|
+
"--format=%H %aI", # SHA and timestamp
|
|
104
|
+
f"--until={timestamp_str}",
|
|
105
|
+
"-1", # Get most recent commit before timestamp
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
if file_path:
|
|
109
|
+
args.append("--")
|
|
110
|
+
args.append(file_path)
|
|
111
|
+
|
|
112
|
+
output = _run_git_command(args, cwd)
|
|
113
|
+
|
|
114
|
+
if not output:
|
|
115
|
+
# No commits found - try to get the very first commit
|
|
116
|
+
logger.warning(
|
|
117
|
+
"no_commits_before_timestamp",
|
|
118
|
+
timestamp=timestamp_str,
|
|
119
|
+
file_path=file_path,
|
|
120
|
+
severity="operational",
|
|
121
|
+
)
|
|
122
|
+
args_first = ["log", "--format=%H", "--reverse", "-1"]
|
|
123
|
+
if file_path:
|
|
124
|
+
args_first.extend(["--", file_path])
|
|
125
|
+
first_commit = _run_git_command(args_first, cwd)
|
|
126
|
+
if not first_commit:
|
|
127
|
+
raise GitHistoryError("No commits found in repository")
|
|
128
|
+
return first_commit
|
|
129
|
+
|
|
130
|
+
# Parse "SHA TIMESTAMP" format and return SHA
|
|
131
|
+
sha = output.split()[0]
|
|
132
|
+
logger.debug(
|
|
133
|
+
"found_commit_at_time",
|
|
134
|
+
timestamp=timestamp_str,
|
|
135
|
+
commit=sha[:8],
|
|
136
|
+
file_path=file_path,
|
|
137
|
+
)
|
|
138
|
+
return sha
|
|
139
|
+
|
|
140
|
+
except GitHistoryError:
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_file_at_commit(file_path: str, commit_sha: str, cwd: str = ".") -> str:
|
|
145
|
+
"""Get file content at a specific commit.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
file_path: Path to file (relative to repo root)
|
|
149
|
+
commit_sha: Git commit SHA
|
|
150
|
+
cwd: Working directory
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
File content as string
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
NotAGitRepository: If not in a git repository
|
|
157
|
+
FileNotFound: If file doesn't exist at commit
|
|
158
|
+
GitHistoryError: If git command fails
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
output = _run_git_command(["show", f"{commit_sha}:{file_path}"], cwd)
|
|
162
|
+
logger.debug(
|
|
163
|
+
"retrieved_file_at_commit",
|
|
164
|
+
file=file_path,
|
|
165
|
+
commit=commit_sha[:8],
|
|
166
|
+
)
|
|
167
|
+
return output
|
|
168
|
+
|
|
169
|
+
except GitHistoryError as e:
|
|
170
|
+
if "does not exist" in str(e).lower():
|
|
171
|
+
raise FileNotFound(
|
|
172
|
+
f"File {file_path} not found at commit {commit_sha}"
|
|
173
|
+
) from e
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_file_at_timestamp(
|
|
178
|
+
file_path: str, timestamp: datetime | str, cwd: str = "."
|
|
179
|
+
) -> str:
|
|
180
|
+
"""Get file content as it existed at a specific timestamp.
|
|
181
|
+
|
|
182
|
+
This function reconstructs the baseline by:
|
|
183
|
+
1. Finding the commit closest to the timestamp
|
|
184
|
+
2. Retrieving the file content at that commit
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
file_path: Path to file (relative to repo root)
|
|
188
|
+
timestamp: Datetime or ISO 8601 string
|
|
189
|
+
cwd: Working directory
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
File content as string
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
NotAGitRepository: If not in a git repository
|
|
196
|
+
FileNotFound: If file didn't exist at timestamp
|
|
197
|
+
GitHistoryError: If git operations fail
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
commit_sha = find_commit_at_time(timestamp, file_path, cwd)
|
|
201
|
+
content = get_file_at_commit(file_path, commit_sha, cwd)
|
|
202
|
+
logger.debug(
|
|
203
|
+
"file_retrieved_at_timestamp",
|
|
204
|
+
file=file_path,
|
|
205
|
+
timestamp=str(timestamp),
|
|
206
|
+
commit=commit_sha[:8],
|
|
207
|
+
)
|
|
208
|
+
return content
|
|
209
|
+
|
|
210
|
+
except (NotAGitRepository, FileNotFound, GitHistoryError):
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_file_at_head(file_path: str, cwd: str = ".") -> str:
|
|
215
|
+
"""Get current file content from HEAD.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
file_path: Path to file (relative to repo root)
|
|
219
|
+
cwd: Working directory
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Current file content
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
NotAGitRepository: If not in a git repository
|
|
226
|
+
FileNotFound: If file doesn't exist in HEAD
|
|
227
|
+
GitHistoryError: If git command fails
|
|
228
|
+
"""
|
|
229
|
+
return get_file_at_commit(file_path, "HEAD", cwd)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_last_modified_time(file_path: str, cwd: str = ".") -> datetime | None:
|
|
233
|
+
"""Get the timestamp of the last commit that modified a file.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
file_path: Path to file (relative to repo root)
|
|
237
|
+
cwd: Working directory
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Datetime of last modification, or None if file not found
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
NotAGitRepository: If not in a git repository
|
|
244
|
+
GitHistoryError: If git command fails
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
output = _run_git_command(["log", "-1", "--format=%aI", "--", file_path], cwd)
|
|
248
|
+
|
|
249
|
+
if not output:
|
|
250
|
+
logger.debug(
|
|
251
|
+
"file_not_in_history",
|
|
252
|
+
file=file_path,
|
|
253
|
+
)
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
return datetime.fromisoformat(output)
|
|
257
|
+
|
|
258
|
+
except GitHistoryError as e:
|
|
259
|
+
if "does not exist" in str(e).lower():
|
|
260
|
+
return None
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def is_git_repository(cwd: str = ".") -> bool:
|
|
265
|
+
"""Check if a directory is a git repository.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
cwd: Directory to check
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if directory is a git repository, False otherwise
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
_run_git_command(["rev-parse", "--git-dir"], cwd)
|
|
275
|
+
return True
|
|
276
|
+
except (NotAGitRepository, GitHistoryError):
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_repository_root(cwd: str = ".") -> str:
|
|
281
|
+
"""Get the root directory of the git repository.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
cwd: Working directory
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Path to repository root
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
NotAGitRepository: If not in a git repository
|
|
291
|
+
GitHistoryError: If git command fails
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
root = _run_git_command(["rev-parse", "--show-toplevel"], cwd)
|
|
295
|
+
logger.debug("found_repo_root", root=root)
|
|
296
|
+
return root
|
|
297
|
+
except GitHistoryError:
|
|
298
|
+
raise
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_changed_files_since_commit(ref: str = "HEAD~1", cwd: str = ".") -> set[str]:
|
|
302
|
+
"""Get list of files changed since a git reference.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
ref: Git reference (default: previous commit)
|
|
306
|
+
cwd: Working directory
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Set of file paths that changed
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
NotAGitRepository: If not in a git repository
|
|
313
|
+
GitHistoryError: If git command fails
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
output = _run_git_command(["diff", "--name-only", ref, "HEAD"], cwd)
|
|
317
|
+
|
|
318
|
+
if not output:
|
|
319
|
+
return set()
|
|
320
|
+
|
|
321
|
+
files = set(output.split("\n"))
|
|
322
|
+
logger.debug(
|
|
323
|
+
"found_changed_files",
|
|
324
|
+
ref=ref,
|
|
325
|
+
count=len(files),
|
|
326
|
+
)
|
|
327
|
+
return files
|
|
328
|
+
|
|
329
|
+
except GitHistoryError:
|
|
330
|
+
raise
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Parser package with modularized entity-specific parsers.
|
|
2
|
+
|
|
3
|
+
Organizes parser classes across semantic modules:
|
|
4
|
+
- frontmatter.py: YAML frontmatter parsing and serialization (70 LOC)
|
|
5
|
+
- issue.py: Issue entity parsing and serialization (160 LOC)
|
|
6
|
+
- milestone.py: Milestone entity parsing and serialization (130 LOC)
|
|
7
|
+
- project.py: Project entity parsing and serialization (100 LOC)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .frontmatter import FrontmatterParser
|
|
11
|
+
from .issue import IssueParser
|
|
12
|
+
from .milestone import MilestoneParser
|
|
13
|
+
from .project import ProjectParser
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"FrontmatterParser",
|
|
17
|
+
"IssueParser",
|
|
18
|
+
"MilestoneParser",
|
|
19
|
+
"ProjectParser",
|
|
20
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Frontmatter parser for markdown files with YAML headers."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from roadmap.common.utils.file_utils import ensure_directory_exists, file_exists_check
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FrontmatterParser:
|
|
14
|
+
"""Parser for markdown files with YAML frontmatter."""
|
|
15
|
+
|
|
16
|
+
FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def parse_file(cls, file_path: Path) -> tuple[dict[str, Any], str]:
|
|
20
|
+
"""Parse a markdown file and return frontmatter and content."""
|
|
21
|
+
if not file_exists_check(file_path):
|
|
22
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
23
|
+
|
|
24
|
+
content = file_path.read_text(encoding="utf-8")
|
|
25
|
+
return cls.parse_content(content)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def parse_content(cls, content: str) -> tuple[dict[str, Any], str]:
|
|
29
|
+
"""Parse markdown content and return frontmatter and body."""
|
|
30
|
+
match = cls.FRONTMATTER_PATTERN.match(content)
|
|
31
|
+
|
|
32
|
+
if not match:
|
|
33
|
+
# No frontmatter found, return empty dict and full content
|
|
34
|
+
return {}, content
|
|
35
|
+
|
|
36
|
+
frontmatter_str, markdown_content = match.groups()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
loaded = yaml.safe_load(frontmatter_str)
|
|
40
|
+
frontmatter = loaded if isinstance(loaded, dict) else {}
|
|
41
|
+
except yaml.YAMLError as e:
|
|
42
|
+
raise ValueError(f"Invalid YAML frontmatter: {e}") from e
|
|
43
|
+
|
|
44
|
+
return frontmatter, markdown_content.strip()
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def serialize_file(
|
|
48
|
+
cls, frontmatter: dict[str, Any], content: str, file_path: Path
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Write frontmatter and content to a markdown file."""
|
|
51
|
+
from roadmap.common.logging import get_logger, get_stack_trace
|
|
52
|
+
|
|
53
|
+
logger = get_logger(__name__)
|
|
54
|
+
|
|
55
|
+
logger.debug(
|
|
56
|
+
"serializing_file",
|
|
57
|
+
file_path=str(file_path),
|
|
58
|
+
stack=get_stack_trace(depth=3),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
ensure_directory_exists(file_path.parent)
|
|
62
|
+
|
|
63
|
+
# Convert datetime objects to ISO format strings
|
|
64
|
+
serializable_frontmatter = cls._prepare_frontmatter_for_yaml(frontmatter)
|
|
65
|
+
|
|
66
|
+
frontmatter_str = yaml.dump(
|
|
67
|
+
serializable_frontmatter, default_flow_style=False, sort_keys=False
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Strip trailing whitespace from frontmatter to prevent ruff errors
|
|
71
|
+
frontmatter_str_stripped = frontmatter_str.rstrip() if frontmatter_str else ""
|
|
72
|
+
|
|
73
|
+
# Ensure content doesn't have trailing whitespace
|
|
74
|
+
content_stripped = content.rstrip()
|
|
75
|
+
|
|
76
|
+
full_content = f"---\n{frontmatter_str_stripped}\n---\n\n{content_stripped}\n"
|
|
77
|
+
|
|
78
|
+
file_path.write_text(full_content, encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def _prepare_frontmatter_for_yaml(
|
|
82
|
+
cls, frontmatter: dict[str, Any]
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Prepare frontmatter for YAML serialization."""
|
|
85
|
+
prepared = {}
|
|
86
|
+
for key, value in frontmatter.items():
|
|
87
|
+
if isinstance(value, datetime):
|
|
88
|
+
prepared[key] = value.isoformat()
|
|
89
|
+
elif hasattr(value, "value"): # Handle enum values
|
|
90
|
+
prepared[key] = value.value
|
|
91
|
+
elif value is None:
|
|
92
|
+
prepared[key] = None
|
|
93
|
+
else:
|
|
94
|
+
prepared[key] = value
|
|
95
|
+
return prepared
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def extract_sync_metadata(
|
|
99
|
+
cls, frontmatter: dict[str, Any]
|
|
100
|
+
) -> dict[str, Any] | None:
|
|
101
|
+
"""Extract sync_metadata from frontmatter if present.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
frontmatter: Parsed frontmatter dictionary
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Sync metadata dictionary or None if not present
|
|
108
|
+
"""
|
|
109
|
+
return frontmatter.get("sync_metadata")
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def update_sync_metadata(
|
|
113
|
+
cls, frontmatter: dict[str, Any], sync_metadata: dict[str, Any] | None
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Update sync_metadata in frontmatter.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
frontmatter: Frontmatter dictionary to update in-place
|
|
119
|
+
sync_metadata: Sync metadata to set (converts datetime to ISO strings), or None to remove
|
|
120
|
+
"""
|
|
121
|
+
if sync_metadata:
|
|
122
|
+
# Prepare nested sync_metadata for YAML serialization
|
|
123
|
+
prepared = {}
|
|
124
|
+
for key, value in sync_metadata.items():
|
|
125
|
+
if isinstance(value, datetime):
|
|
126
|
+
prepared[key] = value.isoformat()
|
|
127
|
+
elif isinstance(value, dict):
|
|
128
|
+
# Handle nested dictionaries (e.g., remote_state)
|
|
129
|
+
prepared[key] = cls._prepare_dict_for_yaml(value)
|
|
130
|
+
elif hasattr(value, "value"): # Handle enum values
|
|
131
|
+
prepared[key] = value.value
|
|
132
|
+
else:
|
|
133
|
+
prepared[key] = value
|
|
134
|
+
frontmatter["sync_metadata"] = prepared
|
|
135
|
+
elif "sync_metadata" in frontmatter:
|
|
136
|
+
# Remove if None
|
|
137
|
+
del frontmatter["sync_metadata"]
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def _prepare_dict_for_yaml(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
141
|
+
"""Recursively prepare a dictionary for YAML serialization."""
|
|
142
|
+
prepared = {}
|
|
143
|
+
for key, value in data.items():
|
|
144
|
+
if isinstance(value, datetime):
|
|
145
|
+
prepared[key] = value.isoformat()
|
|
146
|
+
elif isinstance(value, dict):
|
|
147
|
+
prepared[key] = cls._prepare_dict_for_yaml(value)
|
|
148
|
+
elif hasattr(value, "value"): # Handle enum values
|
|
149
|
+
prepared[key] = value.value
|
|
150
|
+
else:
|
|
151
|
+
prepared[key] = value
|
|
152
|
+
return prepared
|