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,348 @@
|
|
|
1
|
+
"""File locking mechanism for roadmap YAML files."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import fcntl
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import AbstractContextManager
|
|
10
|
+
from datetime import UTC, datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from threading import Lock
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from structlog import get_logger
|
|
16
|
+
|
|
17
|
+
from roadmap.common.utils.file_utils import ensure_directory_exists
|
|
18
|
+
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileLockError(Exception):
|
|
23
|
+
"""Exception raised for file locking errors."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileLockTimeout(FileLockError):
|
|
29
|
+
"""Exception raised when file lock times out."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileLock:
|
|
35
|
+
"""File lock implementation using fcntl (Unix/Linux/macOS) or msvcrt (Windows)."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, file_path: Path, timeout: float = 30.0, check_interval: float = 0.1
|
|
39
|
+
):
|
|
40
|
+
"""Initialize FileLock.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
file_path: Path to file to lock.
|
|
44
|
+
timeout: Lock acquisition timeout in seconds.
|
|
45
|
+
check_interval: Interval in seconds between lock checks.
|
|
46
|
+
"""
|
|
47
|
+
self.file_path = file_path
|
|
48
|
+
self.lock_path = file_path.with_suffix(file_path.suffix + ".lock")
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.check_interval = check_interval
|
|
51
|
+
self.lock_file = None
|
|
52
|
+
self.acquired = False
|
|
53
|
+
|
|
54
|
+
# Lock metadata
|
|
55
|
+
self.lock_info = {
|
|
56
|
+
"pid": os.getpid(),
|
|
57
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
58
|
+
"file": str(file_path),
|
|
59
|
+
"host": os.uname().nodename if hasattr(os, "uname") else "unknown",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def __enter__(self):
|
|
63
|
+
"""Context manager entry."""
|
|
64
|
+
self.acquire()
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
|
68
|
+
"""Context manager exit."""
|
|
69
|
+
self.release()
|
|
70
|
+
|
|
71
|
+
def acquire(self) -> bool:
|
|
72
|
+
"""Acquire the file lock."""
|
|
73
|
+
if self.acquired:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
start_time = time.time()
|
|
77
|
+
|
|
78
|
+
while time.time() - start_time < self.timeout:
|
|
79
|
+
try:
|
|
80
|
+
# Create lock file
|
|
81
|
+
self.lock_file = open(self.lock_path, "w")
|
|
82
|
+
|
|
83
|
+
# Try to acquire exclusive lock
|
|
84
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
85
|
+
|
|
86
|
+
# Write lock metadata
|
|
87
|
+
json.dump(self.lock_info, self.lock_file, indent=2)
|
|
88
|
+
self.lock_file.flush()
|
|
89
|
+
|
|
90
|
+
self.acquired = True
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
except OSError as e:
|
|
94
|
+
if e.errno in (errno.EAGAIN, errno.EACCES):
|
|
95
|
+
# Lock is already held, wait and retry
|
|
96
|
+
if self.lock_file:
|
|
97
|
+
self.lock_file.close()
|
|
98
|
+
self.lock_file = None
|
|
99
|
+
time.sleep(self.check_interval)
|
|
100
|
+
continue
|
|
101
|
+
else:
|
|
102
|
+
# Other error
|
|
103
|
+
if self.lock_file:
|
|
104
|
+
self.lock_file.close()
|
|
105
|
+
self.lock_file = None
|
|
106
|
+
raise FileLockError(f"Failed to acquire lock: {e}") from e
|
|
107
|
+
|
|
108
|
+
# Timeout reached
|
|
109
|
+
if self.lock_file:
|
|
110
|
+
self.lock_file.close()
|
|
111
|
+
self.lock_file = None
|
|
112
|
+
raise FileLockTimeout(f"Failed to acquire lock within {self.timeout} seconds")
|
|
113
|
+
|
|
114
|
+
def release(self) -> bool:
|
|
115
|
+
"""Release the file lock."""
|
|
116
|
+
if not self.acquired:
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if self.lock_file:
|
|
121
|
+
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
|
|
122
|
+
self.lock_file.close()
|
|
123
|
+
self.lock_file = None
|
|
124
|
+
|
|
125
|
+
# Remove lock file
|
|
126
|
+
if self.lock_path.exists():
|
|
127
|
+
self.lock_path.unlink()
|
|
128
|
+
|
|
129
|
+
self.acquired = False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise FileLockError(f"Failed to release lock: {e}") from e
|
|
134
|
+
|
|
135
|
+
def is_locked(self) -> bool:
|
|
136
|
+
"""Check if the file is currently locked."""
|
|
137
|
+
if not self.lock_path.exists():
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Try to open and lock the file
|
|
142
|
+
with open(self.lock_path) as f:
|
|
143
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
144
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
145
|
+
return False # No lock held
|
|
146
|
+
except OSError as e:
|
|
147
|
+
logger.debug("lock_check_failed", error=str(e))
|
|
148
|
+
return True # Lock is held
|
|
149
|
+
|
|
150
|
+
def get_lock_info(self) -> dict[str, Any] | None:
|
|
151
|
+
"""Get information about the current lock holder."""
|
|
152
|
+
if not self.lock_path.exists():
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with open(self.lock_path) as f:
|
|
157
|
+
return json.load(f)
|
|
158
|
+
except (OSError, json.JSONDecodeError):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def force_unlock(self) -> bool:
|
|
162
|
+
"""Force unlock by removing the lock file (use with caution)."""
|
|
163
|
+
try:
|
|
164
|
+
if self.lock_path.exists():
|
|
165
|
+
self.lock_path.unlink()
|
|
166
|
+
return True
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error("force_unlock_failed", error=str(e), severity="system_error")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LockManager:
|
|
173
|
+
"""Manager for handling multiple file locks and lock policies."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, default_timeout: float = 30.0):
|
|
176
|
+
"""Initialize LockManager.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
default_timeout: Default timeout for lock acquisition in seconds.
|
|
180
|
+
"""
|
|
181
|
+
self.default_timeout = default_timeout
|
|
182
|
+
self.active_locks: dict[str, FileLock] = {}
|
|
183
|
+
self._global_lock = Lock() # Thread safety
|
|
184
|
+
|
|
185
|
+
def lock_file(
|
|
186
|
+
self, file_path: Path, timeout: float | None = None
|
|
187
|
+
) -> AbstractContextManager[FileLock]:
|
|
188
|
+
"""Get a context manager for file locking."""
|
|
189
|
+
timeout = timeout or self.default_timeout
|
|
190
|
+
file_str = str(file_path)
|
|
191
|
+
|
|
192
|
+
with self._global_lock:
|
|
193
|
+
if file_str in self.active_locks:
|
|
194
|
+
# Reuse existing lock
|
|
195
|
+
return self.active_locks[file_str]
|
|
196
|
+
|
|
197
|
+
# Create new lock
|
|
198
|
+
lock = FileLock(file_path, timeout)
|
|
199
|
+
self.active_locks[file_str] = lock
|
|
200
|
+
return lock
|
|
201
|
+
|
|
202
|
+
def is_file_locked(self, file_path: Path) -> bool:
|
|
203
|
+
"""Check if a file is currently locked."""
|
|
204
|
+
lock = FileLock(file_path)
|
|
205
|
+
return lock.is_locked()
|
|
206
|
+
|
|
207
|
+
def get_lock_info(self, file_path: Path) -> dict[str, Any] | None:
|
|
208
|
+
"""Get lock information for a file."""
|
|
209
|
+
lock = FileLock(file_path)
|
|
210
|
+
return lock.get_lock_info()
|
|
211
|
+
|
|
212
|
+
def force_unlock_file(self, file_path: Path) -> bool:
|
|
213
|
+
"""Force unlock a file (use with extreme caution)."""
|
|
214
|
+
lock = FileLock(file_path)
|
|
215
|
+
return lock.force_unlock()
|
|
216
|
+
|
|
217
|
+
def cleanup_stale_locks(self, max_age_hours: float = 24.0) -> int:
|
|
218
|
+
"""Clean up stale lock files older than max_age_hours."""
|
|
219
|
+
cleaned = 0
|
|
220
|
+
max_age = timedelta(hours=max_age_hours)
|
|
221
|
+
current_time = datetime.now(UTC)
|
|
222
|
+
|
|
223
|
+
# Find all .lock files
|
|
224
|
+
for lock_file in Path(".").rglob("*.lock"):
|
|
225
|
+
try:
|
|
226
|
+
# Check file age
|
|
227
|
+
file_time = datetime.fromtimestamp(lock_file.stat().st_mtime)
|
|
228
|
+
if current_time - file_time > max_age:
|
|
229
|
+
# Check if the lock is actually stale
|
|
230
|
+
lock = FileLock(lock_file.with_suffix(""))
|
|
231
|
+
if not lock.is_locked():
|
|
232
|
+
lock_file.unlink()
|
|
233
|
+
cleaned += 1
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.debug(
|
|
236
|
+
"stale_lock_cleanup_failed", lock_file=str(lock_file), error=str(e)
|
|
237
|
+
)
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
return cleaned
|
|
241
|
+
|
|
242
|
+
def get_all_locks(
|
|
243
|
+
self, search_dir: Path | None = None
|
|
244
|
+
) -> dict[str, dict[str, Any]]:
|
|
245
|
+
"""Get information about all current locks."""
|
|
246
|
+
locks = {}
|
|
247
|
+
search_path = search_dir or Path(".")
|
|
248
|
+
|
|
249
|
+
for lock_file in search_path.rglob("*.lock"):
|
|
250
|
+
try:
|
|
251
|
+
original_file = lock_file.with_suffix("")
|
|
252
|
+
lock = FileLock(original_file)
|
|
253
|
+
lock_info = lock.get_lock_info()
|
|
254
|
+
if lock_info:
|
|
255
|
+
locks[str(original_file)] = lock_info
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.debug(
|
|
258
|
+
"lock_info_retrieval_failed", lock_file=str(lock_file), error=str(e)
|
|
259
|
+
)
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
return locks
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class LockedFileOperations:
|
|
266
|
+
"""File operations with automatic locking."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, lock_manager: LockManager | None = None):
|
|
269
|
+
"""Initialize LockedFileOperations.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
lock_manager: LockManager instance. Defaults to new LockManager.
|
|
273
|
+
"""
|
|
274
|
+
self.lock_manager = lock_manager or LockManager()
|
|
275
|
+
|
|
276
|
+
def read_file_locked(self, file_path: Path, timeout: float | None = None) -> str:
|
|
277
|
+
"""Read a file with locking."""
|
|
278
|
+
with self.lock_manager.lock_file(file_path, timeout):
|
|
279
|
+
return file_path.read_text(encoding="utf-8")
|
|
280
|
+
|
|
281
|
+
def write_file_locked(
|
|
282
|
+
self,
|
|
283
|
+
file_path: Path,
|
|
284
|
+
content: str,
|
|
285
|
+
timeout: float | None = None,
|
|
286
|
+
backup: bool = True,
|
|
287
|
+
) -> bool:
|
|
288
|
+
"""Write a file with locking and optional backup."""
|
|
289
|
+
with self.lock_manager.lock_file(file_path, timeout):
|
|
290
|
+
try:
|
|
291
|
+
# Create backup if requested and file exists
|
|
292
|
+
if backup and file_path.exists():
|
|
293
|
+
backup_path = file_path.with_suffix(f".backup.{int(time.time())}")
|
|
294
|
+
backup_path.write_text(
|
|
295
|
+
file_path.read_text(encoding="utf-8"), encoding="utf-8"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Ensure directory exists
|
|
299
|
+
ensure_directory_exists(file_path.parent)
|
|
300
|
+
|
|
301
|
+
# Write to temporary file first for atomic operation
|
|
302
|
+
with tempfile.NamedTemporaryFile(
|
|
303
|
+
mode="w",
|
|
304
|
+
encoding="utf-8",
|
|
305
|
+
dir=file_path.parent,
|
|
306
|
+
prefix=f".{file_path.name}.",
|
|
307
|
+
suffix=".tmp",
|
|
308
|
+
delete=False,
|
|
309
|
+
) as tmp_file:
|
|
310
|
+
tmp_file.write(content)
|
|
311
|
+
tmp_path = Path(tmp_file.name)
|
|
312
|
+
|
|
313
|
+
# Atomic move
|
|
314
|
+
tmp_path.replace(file_path)
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
# Clean up temp file if it exists
|
|
319
|
+
if "tmp_path" in locals() and tmp_path.exists():
|
|
320
|
+
tmp_path.unlink()
|
|
321
|
+
raise FileLockError(f"Failed to write file: {e}") from e
|
|
322
|
+
|
|
323
|
+
def update_file_locked(
|
|
324
|
+
self, file_path: Path, updater_func, timeout: float | None = None
|
|
325
|
+
) -> Any:
|
|
326
|
+
"""Update a file using an updater function with locking."""
|
|
327
|
+
with self.lock_manager.lock_file(file_path, timeout):
|
|
328
|
+
# Read current content
|
|
329
|
+
if file_path.exists():
|
|
330
|
+
content = file_path.read_text(encoding="utf-8")
|
|
331
|
+
else:
|
|
332
|
+
content = ""
|
|
333
|
+
|
|
334
|
+
# Apply updater function
|
|
335
|
+
new_content = updater_func(content)
|
|
336
|
+
|
|
337
|
+
# Write back if changed
|
|
338
|
+
if new_content != content:
|
|
339
|
+
self.write_file_locked(
|
|
340
|
+
file_path, new_content, timeout=None, backup=True
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return new_content
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# Global instances
|
|
347
|
+
lock_manager = LockManager()
|
|
348
|
+
locked_file_ops = LockedFileOperations(lock_manager)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""YAML file parsing for .roadmap markdown files."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from roadmap.common.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileParser:
|
|
16
|
+
"""Handles parsing and hashing of markdown files with YAML frontmatter."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def calculate_file_hash(file_path: Path) -> str:
|
|
20
|
+
"""Calculate SHA-256 hash of file content."""
|
|
21
|
+
try:
|
|
22
|
+
with open(file_path, "rb") as f:
|
|
23
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.warning(
|
|
26
|
+
"failed_to_calculate_hash",
|
|
27
|
+
file_path=str(file_path),
|
|
28
|
+
error=str(e),
|
|
29
|
+
severity="operational",
|
|
30
|
+
)
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def parse_yaml_frontmatter(file_path: Path) -> dict[str, Any]:
|
|
35
|
+
"""Parse YAML frontmatter from markdown file.
|
|
36
|
+
|
|
37
|
+
Expects format:
|
|
38
|
+
---
|
|
39
|
+
key: value
|
|
40
|
+
---
|
|
41
|
+
# Markdown content
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
with open(file_path, encoding="utf-8") as f:
|
|
45
|
+
content = f.read()
|
|
46
|
+
|
|
47
|
+
# Check for frontmatter delimiters
|
|
48
|
+
if not content.startswith("---\n"):
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
# Find the end of frontmatter
|
|
52
|
+
try:
|
|
53
|
+
end_marker = content.index("\n---\n", 4)
|
|
54
|
+
frontmatter = content[4:end_marker]
|
|
55
|
+
loaded = yaml.safe_load(frontmatter)
|
|
56
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
57
|
+
except ValueError:
|
|
58
|
+
# No end marker found, treat entire file as YAML
|
|
59
|
+
loaded = yaml.safe_load(content)
|
|
60
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(
|
|
64
|
+
"failed_to_parse_yaml",
|
|
65
|
+
file_path=str(file_path),
|
|
66
|
+
error=str(e),
|
|
67
|
+
severity="data_error",
|
|
68
|
+
)
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def extract_file_metadata(file_path: Path) -> dict[str, Any]:
|
|
73
|
+
"""Extract file metadata without parsing YAML.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary with 'hash', 'size', and 'modified_time' keys
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
file_stat = file_path.stat()
|
|
80
|
+
return {
|
|
81
|
+
"hash": FileParser.calculate_file_hash(file_path),
|
|
82
|
+
"size": file_stat.st_size,
|
|
83
|
+
"modified_time": datetime.fromtimestamp(file_stat.st_mtime),
|
|
84
|
+
}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(
|
|
87
|
+
"failed_to_extract_metadata",
|
|
88
|
+
file_path=str(file_path),
|
|
89
|
+
error=str(e),
|
|
90
|
+
severity="data_error",
|
|
91
|
+
)
|
|
92
|
+
return {}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""File synchronization manager for syncing .roadmap files to database.
|
|
2
|
+
|
|
3
|
+
This module coordinates synchronization of markdown files in the .roadmap directory
|
|
4
|
+
with the SQLite database. It delegates to focused managers for parsing, sync state,
|
|
5
|
+
and entity-specific coordination.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from roadmap.adapters.persistence.file_parser import FileParser
|
|
12
|
+
from roadmap.adapters.persistence.sync_orchestrator import SyncOrchestrator
|
|
13
|
+
from roadmap.common.logging import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileSynchronizer:
|
|
19
|
+
"""Manages synchronization of .roadmap files to the database.
|
|
20
|
+
|
|
21
|
+
Delegates to specialized managers for parsing, sync state tracking,
|
|
22
|
+
and entity-specific synchronization coordination.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, get_connection, transaction_context):
|
|
26
|
+
"""Initialize the file synchronizer.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
get_connection: Callable that returns a database connection
|
|
30
|
+
transaction_context: Context manager for transactions
|
|
31
|
+
"""
|
|
32
|
+
self._orchestrator = SyncOrchestrator(get_connection, transaction_context)
|
|
33
|
+
self._parser = FileParser()
|
|
34
|
+
self._get_connection = get_connection
|
|
35
|
+
self._transaction = transaction_context
|
|
36
|
+
|
|
37
|
+
def get_file_sync_status(self, file_path: str) -> dict[str, Any] | None:
|
|
38
|
+
"""Get sync status for a file."""
|
|
39
|
+
conn = self._get_connection()
|
|
40
|
+
row = conn.execute(
|
|
41
|
+
"""
|
|
42
|
+
SELECT file_path, content_hash, file_size, last_modified
|
|
43
|
+
FROM file_sync_state WHERE file_path = ?
|
|
44
|
+
""",
|
|
45
|
+
(file_path,),
|
|
46
|
+
).fetchone()
|
|
47
|
+
return dict(row) if row else None
|
|
48
|
+
|
|
49
|
+
def update_file_sync_status(
|
|
50
|
+
self, file_path: str, content_hash: str, file_size: int, last_modified: Any
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Update sync status for a file."""
|
|
53
|
+
with self._transaction() as conn:
|
|
54
|
+
conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
INSERT OR REPLACE INTO file_sync_state
|
|
57
|
+
(file_path, content_hash, file_size, last_modified)
|
|
58
|
+
VALUES (?, ?, ?, ?)
|
|
59
|
+
""",
|
|
60
|
+
(file_path, content_hash, file_size, last_modified),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def has_file_changed(self, file_path: Path) -> bool:
|
|
64
|
+
"""Check if file has changed since last sync."""
|
|
65
|
+
try:
|
|
66
|
+
if not file_path.exists():
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
current_hash = self._parser.calculate_file_hash(file_path)
|
|
70
|
+
sync_status = self.get_file_sync_status(str(file_path))
|
|
71
|
+
|
|
72
|
+
if not sync_status:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
return current_hash != sync_status["content_hash"]
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(
|
|
79
|
+
"failed_to_check_file_changes",
|
|
80
|
+
file_path=str(file_path),
|
|
81
|
+
error=str(e),
|
|
82
|
+
severity="operational",
|
|
83
|
+
)
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def sync_directory_incremental(self, roadmap_dir: Path) -> dict[str, Any]:
|
|
87
|
+
"""Incrementally sync .roadmap directory to database.
|
|
88
|
+
|
|
89
|
+
Only syncs files that have changed since last sync.
|
|
90
|
+
"""
|
|
91
|
+
return self._orchestrator.sync_directory_incremental(roadmap_dir)
|
|
92
|
+
|
|
93
|
+
def full_rebuild_from_git(self, roadmap_dir: Path) -> dict[str, Any]:
|
|
94
|
+
"""Full rebuild of database from git files.
|
|
95
|
+
|
|
96
|
+
Clears all data and rebuilds from scratch.
|
|
97
|
+
"""
|
|
98
|
+
return self._orchestrator.full_rebuild_from_git(roadmap_dir)
|
|
99
|
+
|
|
100
|
+
def should_do_full_rebuild(self, roadmap_dir: Path, threshold: int = 50) -> bool:
|
|
101
|
+
"""Determine if full rebuild is needed vs incremental sync.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
roadmap_dir: Path to .roadmap directory
|
|
105
|
+
threshold: Percentage of files changed before triggering rebuild (default 50%)
|
|
106
|
+
"""
|
|
107
|
+
return self._orchestrator.should_do_full_rebuild(roadmap_dir, threshold)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Focused state manager adapters for StateManager.
|
|
2
|
+
|
|
3
|
+
These adapters wrap StateManager to provide focused, single-responsibility
|
|
4
|
+
interfaces. This allows code to depend on focused protocols instead of the
|
|
5
|
+
monolithic StateManager.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from roadmap.core.interfaces import (
|
|
11
|
+
IssueStateManager,
|
|
12
|
+
MilestoneStateManager,
|
|
13
|
+
ProjectStateManager,
|
|
14
|
+
QueryStateManager,
|
|
15
|
+
SyncStateManager,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FocusedProjectStateManager(ProjectStateManager):
|
|
20
|
+
"""Project-focused view of StateManager."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, state_manager):
|
|
23
|
+
"""Initialize with StateManager instance."""
|
|
24
|
+
self._state_manager = state_manager
|
|
25
|
+
|
|
26
|
+
def create_project(self, project_data: dict[str, Any]) -> str:
|
|
27
|
+
"""Create a new project."""
|
|
28
|
+
return self._state_manager.create_project(project_data)
|
|
29
|
+
|
|
30
|
+
def get_project(self, project_id: str) -> dict[str, Any] | None:
|
|
31
|
+
"""Get project by ID."""
|
|
32
|
+
return self._state_manager.get_project(project_id)
|
|
33
|
+
|
|
34
|
+
def list_projects(self) -> list[dict[str, Any]]:
|
|
35
|
+
"""List all projects."""
|
|
36
|
+
return self._state_manager.list_projects()
|
|
37
|
+
|
|
38
|
+
def update_project(self, project_id: str, updates: dict[str, Any]) -> bool:
|
|
39
|
+
"""Update project."""
|
|
40
|
+
return self._state_manager.update_project(project_id, updates)
|
|
41
|
+
|
|
42
|
+
def delete_project(self, project_id: str) -> bool:
|
|
43
|
+
"""Delete project."""
|
|
44
|
+
return self._state_manager.delete_project(project_id)
|
|
45
|
+
|
|
46
|
+
def mark_project_archived(self, project_id: str, archived: bool = True) -> bool:
|
|
47
|
+
"""Mark a project as archived or unarchived."""
|
|
48
|
+
return self._state_manager.mark_project_archived(project_id, archived)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FocusedMilestoneStateManager(MilestoneStateManager):
|
|
52
|
+
"""Milestone-focused view of StateManager."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, state_manager):
|
|
55
|
+
"""Initialize with StateManager instance."""
|
|
56
|
+
self._state_manager = state_manager
|
|
57
|
+
|
|
58
|
+
def create_milestone(self, milestone_data: dict[str, Any]) -> str:
|
|
59
|
+
"""Create a new milestone."""
|
|
60
|
+
return self._state_manager.create_milestone(milestone_data)
|
|
61
|
+
|
|
62
|
+
def get_milestone(self, milestone_id: str) -> dict[str, Any] | None:
|
|
63
|
+
"""Get milestone by ID."""
|
|
64
|
+
return self._state_manager.get_milestone(milestone_id)
|
|
65
|
+
|
|
66
|
+
def update_milestone(self, milestone_id: str, updates: dict[str, Any]) -> bool:
|
|
67
|
+
"""Update milestone."""
|
|
68
|
+
return self._state_manager.update_milestone(milestone_id, updates)
|
|
69
|
+
|
|
70
|
+
def mark_milestone_archived(self, milestone_id: str, archived: bool = True) -> bool:
|
|
71
|
+
"""Mark a milestone as archived or unarchived."""
|
|
72
|
+
return self._state_manager.mark_milestone_archived(milestone_id, archived)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FocusedIssueStateManager(IssueStateManager):
|
|
76
|
+
"""Issue-focused view of StateManager."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, state_manager):
|
|
79
|
+
"""Initialize with StateManager instance."""
|
|
80
|
+
self._state_manager = state_manager
|
|
81
|
+
|
|
82
|
+
def create_issue(self, issue_data: dict[str, Any]) -> str:
|
|
83
|
+
"""Create a new issue."""
|
|
84
|
+
return self._state_manager.create_issue(issue_data)
|
|
85
|
+
|
|
86
|
+
def get_issue(self, issue_id: str) -> dict[str, Any] | None:
|
|
87
|
+
"""Get issue by ID."""
|
|
88
|
+
return self._state_manager.get_issue(issue_id)
|
|
89
|
+
|
|
90
|
+
def update_issue(self, issue_id: str, updates: dict[str, Any]) -> bool:
|
|
91
|
+
"""Update issue."""
|
|
92
|
+
return self._state_manager.update_issue(issue_id, updates)
|
|
93
|
+
|
|
94
|
+
def delete_issue(self, issue_id: str) -> bool:
|
|
95
|
+
"""Delete issue."""
|
|
96
|
+
return self._state_manager.delete_issue(issue_id)
|
|
97
|
+
|
|
98
|
+
def mark_issue_archived(self, issue_id: str, archived: bool = True) -> bool:
|
|
99
|
+
"""Mark an issue as archived or unarchived."""
|
|
100
|
+
return self._state_manager.mark_issue_archived(issue_id, archived)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FocusedSyncStateManager(SyncStateManager):
|
|
104
|
+
"""Sync state-focused view of StateManager."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, state_manager):
|
|
107
|
+
"""Initialize with StateManager instance."""
|
|
108
|
+
self._state_manager = state_manager
|
|
109
|
+
|
|
110
|
+
def get_sync_state(self, key: str) -> str | None:
|
|
111
|
+
"""Get sync state value."""
|
|
112
|
+
return self._state_manager.get_sync_state(key)
|
|
113
|
+
|
|
114
|
+
def set_sync_state(self, key: str, value: str):
|
|
115
|
+
"""Set sync state value."""
|
|
116
|
+
self._state_manager.set_sync_state(key, value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FocusedQueryStateManager(QueryStateManager):
|
|
120
|
+
"""Query-focused view of StateManager."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, state_manager):
|
|
123
|
+
"""Initialize with StateManager instance."""
|
|
124
|
+
self._state_manager = state_manager
|
|
125
|
+
|
|
126
|
+
def get_all_issues(self) -> list[dict[str, Any]]:
|
|
127
|
+
"""Get all issues from database."""
|
|
128
|
+
return self._state_manager.get_all_issues()
|
|
129
|
+
|
|
130
|
+
def get_all_milestones(self) -> list[dict[str, Any]]:
|
|
131
|
+
"""Get all milestones from database."""
|
|
132
|
+
return self._state_manager.get_all_milestones()
|
|
133
|
+
|
|
134
|
+
def get_milestone_progress(self, milestone_name: str) -> dict[str, int]:
|
|
135
|
+
"""Get progress stats for a milestone."""
|
|
136
|
+
return self._state_manager.get_milestone_progress(milestone_name)
|
|
137
|
+
|
|
138
|
+
def get_issues_by_status(self) -> dict[str, int]:
|
|
139
|
+
"""Get issue counts by status."""
|
|
140
|
+
return self._state_manager.get_issues_by_status()
|