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,460 @@
|
|
|
1
|
+
"""Entity-level health scanning service.
|
|
2
|
+
|
|
3
|
+
Provides detailed diagnostics on individual entities (Issues, Milestones, Projects)
|
|
4
|
+
including validation of descriptions, comments, dependencies, and other metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from roadmap.common.constants import Status
|
|
10
|
+
from roadmap.common.logging import get_logger
|
|
11
|
+
from roadmap.common.observability.instrumentation import traced
|
|
12
|
+
from roadmap.core.domain.comment import Comment
|
|
13
|
+
from roadmap.core.domain.issue import Issue
|
|
14
|
+
from roadmap.core.domain.milestone import Milestone
|
|
15
|
+
from roadmap.core.domain.project import Project
|
|
16
|
+
|
|
17
|
+
# Re-export shared models for backward compatibility
|
|
18
|
+
from roadmap.core.services.health.health_models import (
|
|
19
|
+
EntityHealthReport,
|
|
20
|
+
EntityType,
|
|
21
|
+
HealthIssue,
|
|
22
|
+
HealthSeverity,
|
|
23
|
+
)
|
|
24
|
+
from roadmap.core.services.health.issue_health_scanner import IssueHealthScanner
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EntityHealthScanner:
|
|
30
|
+
"""Scans entities for health issues.
|
|
31
|
+
|
|
32
|
+
Checks for:
|
|
33
|
+
- Missing or empty descriptions
|
|
34
|
+
- Malformed or empty comments
|
|
35
|
+
- Circular dependencies
|
|
36
|
+
- Broken dependencies (referencing non-existent entities)
|
|
37
|
+
- Missing estimates (when required by milestone)
|
|
38
|
+
- Due date issues (overdue, unrealistic)
|
|
39
|
+
- Assignee issues (unassigned when expected, previous_assignee mismatch)
|
|
40
|
+
- Progress tracking issues (inconsistent progress state)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, core: Any = None):
|
|
44
|
+
"""Initialize the scanner.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
core: Optional Core object with access to entities. If not provided,
|
|
48
|
+
the scanner can still work on individual entities passed to methods.
|
|
49
|
+
"""
|
|
50
|
+
self.core = core
|
|
51
|
+
self._entity_cache: dict[str, Any] = {} # Cache of entity_id -> entity
|
|
52
|
+
|
|
53
|
+
@traced("scan_issue")
|
|
54
|
+
def scan_issue(self, issue: Issue) -> EntityHealthReport:
|
|
55
|
+
"""Delegate issue scanning to the extracted `IssueHealthScanner`.
|
|
56
|
+
|
|
57
|
+
This keeps the original public API while the implementation lives in
|
|
58
|
+
`issue_health_scanner.IssueHealthScanner` for easier maintenance.
|
|
59
|
+
"""
|
|
60
|
+
scanner = IssueHealthScanner(core=self.core)
|
|
61
|
+
return scanner.scan_issue(issue)
|
|
62
|
+
|
|
63
|
+
@traced("scan_milestone")
|
|
64
|
+
def scan_milestone(self, milestone: Milestone) -> EntityHealthReport:
|
|
65
|
+
"""Scan a single milestone for health problems.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
milestone: The milestone to scan
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
EntityHealthReport with all issues found
|
|
72
|
+
"""
|
|
73
|
+
report = EntityHealthReport(
|
|
74
|
+
entity_id=milestone.name,
|
|
75
|
+
entity_type=EntityType.MILESTONE,
|
|
76
|
+
entity_title=milestone.name,
|
|
77
|
+
status=milestone.status.value if milestone.status else "unknown",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Check description/content
|
|
81
|
+
if not milestone.content or not milestone.content.strip():
|
|
82
|
+
report.issues.append(
|
|
83
|
+
HealthIssue(
|
|
84
|
+
code="missing_content",
|
|
85
|
+
message="Milestone has no content",
|
|
86
|
+
severity=HealthSeverity.WARNING,
|
|
87
|
+
category="content",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Check dates
|
|
92
|
+
if milestone.created and milestone.due_date:
|
|
93
|
+
if milestone.created > milestone.due_date:
|
|
94
|
+
report.issues.append(
|
|
95
|
+
HealthIssue(
|
|
96
|
+
code="invalid_date_range",
|
|
97
|
+
message=f"Created date ({milestone.created.date()}) is after due date ({milestone.due_date.date()})",
|
|
98
|
+
severity=HealthSeverity.ERROR,
|
|
99
|
+
category="structure",
|
|
100
|
+
details={
|
|
101
|
+
"created_date": milestone.created.isoformat(),
|
|
102
|
+
"due_date": milestone.due_date.isoformat(),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Check progress
|
|
108
|
+
if (
|
|
109
|
+
milestone.calculated_progress is not None
|
|
110
|
+
and milestone.status == Status.CLOSED
|
|
111
|
+
):
|
|
112
|
+
if milestone.calculated_progress != 100:
|
|
113
|
+
report.issues.append(
|
|
114
|
+
HealthIssue(
|
|
115
|
+
code="inconsistent_completion",
|
|
116
|
+
message=f"Milestone marked Done but only {milestone.calculated_progress}% complete",
|
|
117
|
+
severity=HealthSeverity.WARNING,
|
|
118
|
+
category="structure",
|
|
119
|
+
details={"calculated_progress": milestone.calculated_progress},
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return report
|
|
124
|
+
|
|
125
|
+
@traced("scan_project")
|
|
126
|
+
def scan_project(self, project: Project) -> EntityHealthReport:
|
|
127
|
+
"""Scan a single project for health problems.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
project: The project to scan
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
EntityHealthReport with all issues found
|
|
134
|
+
"""
|
|
135
|
+
report = EntityHealthReport(
|
|
136
|
+
entity_id=project.id,
|
|
137
|
+
entity_type=EntityType.PROJECT,
|
|
138
|
+
entity_title=project.name,
|
|
139
|
+
status=project.status.value if project.status else "unknown",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Check description/content
|
|
143
|
+
if not project.content or not project.content.strip():
|
|
144
|
+
report.issues.append(
|
|
145
|
+
HealthIssue(
|
|
146
|
+
code="missing_content",
|
|
147
|
+
message="Project has no content",
|
|
148
|
+
severity=HealthSeverity.WARNING,
|
|
149
|
+
category="content",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check owner
|
|
154
|
+
if not project.owner or not project.owner.strip():
|
|
155
|
+
report.issues.append(
|
|
156
|
+
HealthIssue(
|
|
157
|
+
code="missing_owner",
|
|
158
|
+
message="Project has no owner assigned",
|
|
159
|
+
severity=HealthSeverity.WARNING,
|
|
160
|
+
category="structure",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return report
|
|
165
|
+
|
|
166
|
+
def scan_all(self) -> list[EntityHealthReport]:
|
|
167
|
+
"""Scan all issues, milestones, and projects.
|
|
168
|
+
|
|
169
|
+
Requires core to be initialized.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of EntityHealthReport objects for all entities
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
RuntimeError: If core is not initialized
|
|
176
|
+
"""
|
|
177
|
+
if not self.core:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"Core must be initialized to scan all entities. "
|
|
180
|
+
"Pass core to constructor or call scan_issue/milestone/project directly."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
reports = []
|
|
184
|
+
|
|
185
|
+
# Scan all issues
|
|
186
|
+
try:
|
|
187
|
+
all_issues = self._list_entities("issues", "issue_repository")
|
|
188
|
+
for issue in all_issues:
|
|
189
|
+
reports.append(self.scan_issue(issue))
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error("error_scanning_issues", error=str(e), severity="system_error")
|
|
192
|
+
|
|
193
|
+
# Scan all milestones
|
|
194
|
+
try:
|
|
195
|
+
all_milestones = self._list_entities("milestones", "milestone_repository")
|
|
196
|
+
for milestone in all_milestones:
|
|
197
|
+
reports.append(self.scan_milestone(milestone))
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(
|
|
200
|
+
"error_scanning_milestones", error=str(e), severity="system_error"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Scan all projects
|
|
204
|
+
try:
|
|
205
|
+
all_projects = self._list_entities("projects", "project_repository")
|
|
206
|
+
for project in all_projects:
|
|
207
|
+
reports.append(self.scan_project(project))
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(
|
|
210
|
+
"error_scanning_projects", error=str(e), severity="system_error"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return reports
|
|
214
|
+
|
|
215
|
+
def _list_entities(self, coordinator_attr: str, repository_attr: str) -> list[Any]:
|
|
216
|
+
"""List entities using coordinator API first, with legacy fallback.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
coordinator_attr: Coordinator attribute name on core (e.g., "issues")
|
|
220
|
+
repository_attr: Legacy repository attribute name on core
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of entities
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
AttributeError: If neither API is available
|
|
227
|
+
"""
|
|
228
|
+
coordinator = getattr(self.core, coordinator_attr, None)
|
|
229
|
+
if coordinator is not None and hasattr(coordinator, "list"):
|
|
230
|
+
return coordinator.list()
|
|
231
|
+
|
|
232
|
+
repository = getattr(self.core, repository_attr, None)
|
|
233
|
+
if repository is not None and hasattr(repository, "list"):
|
|
234
|
+
return repository.list()
|
|
235
|
+
|
|
236
|
+
raise AttributeError(
|
|
237
|
+
f"Core has neither '{coordinator_attr}.list()' nor '{repository_attr}.list()'"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Private methods for individual checks
|
|
241
|
+
|
|
242
|
+
def _check_issue_description(self, issue: Issue, report: EntityHealthReport):
|
|
243
|
+
"""Check issue description/content."""
|
|
244
|
+
if not issue.content or not issue.content.strip():
|
|
245
|
+
report.issues.append(
|
|
246
|
+
HealthIssue(
|
|
247
|
+
code="missing_description",
|
|
248
|
+
message="Issue has no description or content",
|
|
249
|
+
severity=HealthSeverity.INFO,
|
|
250
|
+
category="content",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _check_issue_comments(self, issue: Issue, report: EntityHealthReport):
|
|
255
|
+
"""Check issue comments for problems."""
|
|
256
|
+
if not issue.comments:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
errors = self._validate_comment_thread(issue.comments)
|
|
260
|
+
for error_msg in errors:
|
|
261
|
+
# Parse error message to determine severity and code
|
|
262
|
+
if "Duplicate" in error_msg:
|
|
263
|
+
code = "duplicate_comment_id"
|
|
264
|
+
severity = HealthSeverity.ERROR
|
|
265
|
+
elif "circular" in error_msg.lower():
|
|
266
|
+
code = "circular_comment_reference"
|
|
267
|
+
severity = HealthSeverity.ERROR
|
|
268
|
+
elif "datetime" in error_msg:
|
|
269
|
+
code = "invalid_comment_datetime"
|
|
270
|
+
severity = HealthSeverity.ERROR
|
|
271
|
+
elif "Empty" in error_msg:
|
|
272
|
+
code = "empty_comment_body"
|
|
273
|
+
severity = HealthSeverity.WARNING
|
|
274
|
+
elif "author" in error_msg.lower():
|
|
275
|
+
code = "missing_comment_author"
|
|
276
|
+
severity = HealthSeverity.WARNING
|
|
277
|
+
else:
|
|
278
|
+
code = "invalid_comment"
|
|
279
|
+
severity = HealthSeverity.WARNING
|
|
280
|
+
|
|
281
|
+
report.issues.append(
|
|
282
|
+
HealthIssue(
|
|
283
|
+
code=code,
|
|
284
|
+
message=error_msg,
|
|
285
|
+
severity=severity,
|
|
286
|
+
category="content",
|
|
287
|
+
details={"comment_count": len(issue.comments)},
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _check_issue_estimates(self, issue: Issue, report: EntityHealthReport):
|
|
292
|
+
"""Check issue estimation."""
|
|
293
|
+
# Only warn about missing estimates for in-progress or done issues
|
|
294
|
+
if issue.status not in (Status.IN_PROGRESS, Status.CLOSED):
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if issue.estimated_hours is None:
|
|
298
|
+
report.issues.append(
|
|
299
|
+
HealthIssue(
|
|
300
|
+
code="missing_estimate",
|
|
301
|
+
message=f"Issue is {issue.status.value} but has no time estimate",
|
|
302
|
+
severity=HealthSeverity.INFO,
|
|
303
|
+
category="structure",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
elif issue.estimated_hours <= 0:
|
|
307
|
+
report.issues.append(
|
|
308
|
+
HealthIssue(
|
|
309
|
+
code="invalid_estimate",
|
|
310
|
+
message=f"Estimated hours must be positive, got {issue.estimated_hours}",
|
|
311
|
+
severity=HealthSeverity.WARNING,
|
|
312
|
+
category="structure",
|
|
313
|
+
details={"estimated_hours": issue.estimated_hours},
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _check_issue_dates(self, issue: Issue, report: EntityHealthReport):
|
|
318
|
+
"""Check issue date consistency."""
|
|
319
|
+
if issue.actual_start_date and issue.actual_end_date:
|
|
320
|
+
if issue.actual_start_date > issue.actual_end_date:
|
|
321
|
+
report.issues.append(
|
|
322
|
+
HealthIssue(
|
|
323
|
+
code="invalid_date_range",
|
|
324
|
+
message="Start date is after end date",
|
|
325
|
+
severity=HealthSeverity.ERROR,
|
|
326
|
+
category="structure",
|
|
327
|
+
details={
|
|
328
|
+
"actual_start_date": issue.actual_start_date.isoformat(),
|
|
329
|
+
"actual_end_date": issue.actual_end_date.isoformat(),
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if issue.due_date and issue.actual_end_date:
|
|
335
|
+
if issue.actual_end_date > issue.due_date:
|
|
336
|
+
report.issues.append(
|
|
337
|
+
HealthIssue(
|
|
338
|
+
code="missed_due_date",
|
|
339
|
+
message=f"Issue completed after due date ({issue.due_date.date()})",
|
|
340
|
+
severity=HealthSeverity.INFO,
|
|
341
|
+
category="structure",
|
|
342
|
+
details={
|
|
343
|
+
"due_date": issue.due_date.isoformat(),
|
|
344
|
+
"completed_date": issue.actual_end_date.isoformat(),
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _check_issue_assignee(self, issue: Issue, report: EntityHealthReport):
|
|
350
|
+
"""Check issue assignee consistency."""
|
|
351
|
+
if issue.previous_assignee and not issue.handoff_date:
|
|
352
|
+
report.issues.append(
|
|
353
|
+
HealthIssue(
|
|
354
|
+
code="missing_handoff_date",
|
|
355
|
+
message=f"Issue was handed off from {issue.previous_assignee} but no handoff date recorded",
|
|
356
|
+
severity=HealthSeverity.WARNING,
|
|
357
|
+
category="structure",
|
|
358
|
+
details={"previous_assignee": issue.previous_assignee},
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _check_issue_progress(self, issue: Issue, report: EntityHealthReport):
|
|
363
|
+
"""Check issue progress tracking."""
|
|
364
|
+
if issue.progress_percentage is not None:
|
|
365
|
+
if not (0 <= issue.progress_percentage <= 100):
|
|
366
|
+
report.issues.append(
|
|
367
|
+
HealthIssue(
|
|
368
|
+
code="invalid_progress_percentage",
|
|
369
|
+
message=f"Progress must be 0-100, got {issue.progress_percentage}",
|
|
370
|
+
severity=HealthSeverity.ERROR,
|
|
371
|
+
category="structure",
|
|
372
|
+
details={"progress_percentage": issue.progress_percentage},
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Check progress vs status consistency
|
|
377
|
+
if issue.status == Status.CLOSED and issue.progress_percentage != 100:
|
|
378
|
+
report.issues.append(
|
|
379
|
+
HealthIssue(
|
|
380
|
+
code="inconsistent_completion",
|
|
381
|
+
message=f"Issue marked Done but only {issue.progress_percentage or 0}% complete",
|
|
382
|
+
severity=HealthSeverity.WARNING,
|
|
383
|
+
category="structure",
|
|
384
|
+
details={"progress_percentage": issue.progress_percentage},
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def _check_issue_status_consistency(self, issue: Issue, report: EntityHealthReport):
|
|
389
|
+
"""Check overall status consistency."""
|
|
390
|
+
if issue.status == Status.CLOSED and not issue.actual_end_date:
|
|
391
|
+
report.issues.append(
|
|
392
|
+
HealthIssue(
|
|
393
|
+
code="missing_completion_date",
|
|
394
|
+
message="Issue marked Done but has no actual end date recorded",
|
|
395
|
+
severity=HealthSeverity.WARNING,
|
|
396
|
+
category="structure",
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if issue.actual_start_date and issue.status == Status.TODO:
|
|
401
|
+
report.issues.append(
|
|
402
|
+
HealthIssue(
|
|
403
|
+
code="inconsistent_status",
|
|
404
|
+
message="Issue has a start date but status is still TODO",
|
|
405
|
+
severity=HealthSeverity.WARNING,
|
|
406
|
+
category="structure",
|
|
407
|
+
details={"actual_start_date": issue.actual_start_date.isoformat()},
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def _validate_comment_thread(comments: list[Comment]) -> list[str]:
|
|
413
|
+
"""Validate comment threads for errors.
|
|
414
|
+
|
|
415
|
+
Returns list of error messages found.
|
|
416
|
+
"""
|
|
417
|
+
errors = []
|
|
418
|
+
comment_ids = set()
|
|
419
|
+
|
|
420
|
+
for comment in comments:
|
|
421
|
+
# Check for duplicate IDs
|
|
422
|
+
if comment.id in comment_ids:
|
|
423
|
+
errors.append(f"Duplicate comment ID: {comment.id}")
|
|
424
|
+
comment_ids.add(comment.id)
|
|
425
|
+
|
|
426
|
+
# Check for empty body
|
|
427
|
+
if not comment.body or not comment.body.strip():
|
|
428
|
+
errors.append(f"Comment {comment.id}: Empty comment body")
|
|
429
|
+
|
|
430
|
+
# Check for empty author
|
|
431
|
+
if not comment.author or not comment.author.strip():
|
|
432
|
+
errors.append(f"Comment {comment.id}: Missing comment author")
|
|
433
|
+
|
|
434
|
+
# Check for invalid datetime fields
|
|
435
|
+
if not isinstance(comment.created_at, type(comment.created_at)):
|
|
436
|
+
errors.append(
|
|
437
|
+
f"Comment {comment.id}: created_at is not a valid datetime"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Check for circular references in reply chains
|
|
441
|
+
for comment in comments:
|
|
442
|
+
if comment.in_reply_to:
|
|
443
|
+
visited = set()
|
|
444
|
+
current_id = comment.in_reply_to
|
|
445
|
+
while current_id:
|
|
446
|
+
if current_id in visited:
|
|
447
|
+
errors.append(
|
|
448
|
+
f"Comment {comment.id}: Circular reference in reply chain"
|
|
449
|
+
)
|
|
450
|
+
break
|
|
451
|
+
visited.add(current_id)
|
|
452
|
+
# Find the comment this replies to
|
|
453
|
+
current_comment = next(
|
|
454
|
+
(c for c in comments if c.id == current_id), None
|
|
455
|
+
)
|
|
456
|
+
current_id = (
|
|
457
|
+
current_comment.in_reply_to if current_comment else None
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return errors
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Service for repairing malformed YAML in issue files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from roadmap.common.logging import get_logger
|
|
9
|
+
from roadmap.common.logging.error_logging import (
|
|
10
|
+
log_error_with_context,
|
|
11
|
+
)
|
|
12
|
+
from roadmap.common.observability.instrumentation import traced
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileRepairResult:
|
|
18
|
+
"""Result of file repair operation."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize FileRepairResult."""
|
|
22
|
+
self.fixed_files: list[str] = []
|
|
23
|
+
self.errors: list[str] = []
|
|
24
|
+
|
|
25
|
+
def add_fixed(self, file_rel: str) -> None:
|
|
26
|
+
"""Record a fixed file."""
|
|
27
|
+
self.fixed_files.append(file_rel)
|
|
28
|
+
|
|
29
|
+
def add_error(self, file_rel: str) -> None:
|
|
30
|
+
"""Record a file that couldn't be fixed."""
|
|
31
|
+
self.errors.append(file_rel)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileRepairService:
|
|
35
|
+
"""Repairs common malformed YAML issues in issue files."""
|
|
36
|
+
|
|
37
|
+
@traced("repair_files")
|
|
38
|
+
def repair_files(
|
|
39
|
+
self, issues_dir: Path, malformed_files: list[str], dry_run: bool = False
|
|
40
|
+
) -> FileRepairResult:
|
|
41
|
+
"""Fix common malformed YAML issues in issue files.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
issues_dir: Directory containing issue files
|
|
45
|
+
malformed_files: List of relative paths to malformed files
|
|
46
|
+
dry_run: If True, don't write changes to disk
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
FileRepairResult with fixed_files and errors lists
|
|
50
|
+
"""
|
|
51
|
+
result = FileRepairResult()
|
|
52
|
+
|
|
53
|
+
for file_rel in malformed_files:
|
|
54
|
+
file_path = issues_dir / file_rel
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
content = file_path.read_text(encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
# Extract frontmatter
|
|
60
|
+
if not content.startswith("---"):
|
|
61
|
+
result.add_error(file_rel)
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
parts = content.split("---", 2)
|
|
65
|
+
if len(parts) < 3:
|
|
66
|
+
result.add_error(file_rel)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
frontmatter_str, markdown_content = parts[1], parts[2]
|
|
70
|
+
|
|
71
|
+
# Parse and fix common issues
|
|
72
|
+
try:
|
|
73
|
+
frontmatter = yaml.safe_load(frontmatter_str)
|
|
74
|
+
if not isinstance(frontmatter, dict):
|
|
75
|
+
result.add_error(file_rel)
|
|
76
|
+
continue
|
|
77
|
+
except yaml.YAMLError:
|
|
78
|
+
result.add_error(file_rel)
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Fix git_commits and git_branches
|
|
82
|
+
self._normalize_git_data(frontmatter)
|
|
83
|
+
|
|
84
|
+
# Reconstruct the file
|
|
85
|
+
fixed_frontmatter = yaml.dump(
|
|
86
|
+
frontmatter, default_flow_style=False, sort_keys=False
|
|
87
|
+
)
|
|
88
|
+
fixed_content = f"---\n{fixed_frontmatter}---\n{markdown_content}"
|
|
89
|
+
|
|
90
|
+
if not dry_run:
|
|
91
|
+
try:
|
|
92
|
+
file_path.write_text(fixed_content, encoding="utf-8")
|
|
93
|
+
except Exception as write_error:
|
|
94
|
+
log_error_with_context(
|
|
95
|
+
write_error,
|
|
96
|
+
operation="write_repaired_file",
|
|
97
|
+
entity_type="File",
|
|
98
|
+
entity_id=file_rel,
|
|
99
|
+
)
|
|
100
|
+
result.add_error(file_rel)
|
|
101
|
+
logger.warning(
|
|
102
|
+
"Failed to write repaired file",
|
|
103
|
+
file=file_rel,
|
|
104
|
+
error=str(write_error),
|
|
105
|
+
severity="operational",
|
|
106
|
+
)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
result.add_fixed(file_rel)
|
|
110
|
+
logger.info("Fixed malformed file", file=file_rel)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
log_error_with_context(
|
|
114
|
+
e,
|
|
115
|
+
operation="repair_file",
|
|
116
|
+
entity_type="File",
|
|
117
|
+
entity_id=file_rel,
|
|
118
|
+
)
|
|
119
|
+
result.add_error(file_rel)
|
|
120
|
+
logger.warning(
|
|
121
|
+
"Failed to fix file",
|
|
122
|
+
file=file_rel,
|
|
123
|
+
error=str(e),
|
|
124
|
+
severity="operational",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _fix_git_commits(frontmatter: dict[str, Any]) -> None:
|
|
132
|
+
"""Fix git_commits list format.
|
|
133
|
+
|
|
134
|
+
Converts list of strings to list of dicts with 'hash' key.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
frontmatter: Frontmatter dict to fix
|
|
138
|
+
"""
|
|
139
|
+
if "git_commits" not in frontmatter or not isinstance(
|
|
140
|
+
frontmatter["git_commits"], list
|
|
141
|
+
):
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
fixed_commits = []
|
|
145
|
+
for commit in frontmatter["git_commits"]:
|
|
146
|
+
if isinstance(commit, str):
|
|
147
|
+
# Convert string commit hash to dict
|
|
148
|
+
fixed_commits.append({"hash": commit})
|
|
149
|
+
else:
|
|
150
|
+
fixed_commits.append(commit)
|
|
151
|
+
frontmatter["git_commits"] = fixed_commits
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _fix_git_branches(frontmatter: dict[str, Any]) -> None:
|
|
155
|
+
"""Fix git_branches list format.
|
|
156
|
+
|
|
157
|
+
Converts list of dicts to list of strings.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
frontmatter: Frontmatter dict to fix
|
|
161
|
+
"""
|
|
162
|
+
if "git_branches" not in frontmatter or not isinstance(
|
|
163
|
+
frontmatter["git_branches"], list
|
|
164
|
+
):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
fixed_branches = []
|
|
168
|
+
for branch in frontmatter["git_branches"]:
|
|
169
|
+
if isinstance(branch, dict):
|
|
170
|
+
# Convert dict to string - prefer 'name' field
|
|
171
|
+
if "name" in branch:
|
|
172
|
+
fixed_branches.append(branch["name"])
|
|
173
|
+
elif isinstance(branch, str):
|
|
174
|
+
fixed_branches.append(branch)
|
|
175
|
+
else:
|
|
176
|
+
fixed_branches.append(str(branch))
|
|
177
|
+
elif isinstance(branch, str):
|
|
178
|
+
fixed_branches.append(branch)
|
|
179
|
+
else:
|
|
180
|
+
# Convert any other type to string
|
|
181
|
+
fixed_branches.append(str(branch))
|
|
182
|
+
frontmatter["git_branches"] = fixed_branches
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _normalize_git_data(frontmatter: dict[str, Any]) -> None:
|
|
186
|
+
"""Normalize git_commits and git_branches in frontmatter.
|
|
187
|
+
|
|
188
|
+
Fixes:
|
|
189
|
+
- git_commits: list of strings → list of dicts with 'hash' key
|
|
190
|
+
- git_branches: list of dicts → list of strings
|
|
191
|
+
"""
|
|
192
|
+
FileRepairService._fix_git_commits(frontmatter)
|
|
193
|
+
FileRepairService._fix_git_branches(frontmatter)
|