From fe8978f7163784a69df262743a558a1d734f91d0 Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 12 May 2026 20:06:16 +0300 Subject: [PATCH] semantics --- .axiom/runtime/belief_events.jsonl | 400 + .axiom/semantic_index/graph.duckdb | Bin 37236736 -> 49033216 bytes .axiom/semantic_index/index.json | 28787 ++++++++++------ backend/_convert_defs.py | 409 + backend/src/__init__.py | 6 +- backend/src/api/__init__.py | 6 +- backend/src/api/auth.py | 88 +- backend/src/api/routes/__init__.py | 48 +- backend/src/api/routes/__tests__/conftest.py | 7 +- .../routes/__tests__/test_assistant_api.py | 163 +- .../routes/__tests__/test_assistant_authz.py | 176 +- .../__tests__/test_clean_release_api.py | 52 +- .../test_clean_release_legacy_compat.py | 39 +- .../test_clean_release_source_policy.py | 30 +- .../__tests__/test_clean_release_v2_api.py | 35 +- .../test_clean_release_v2_release_api.py | 35 +- .../__tests__/test_connections_routes.py | 27 +- .../api/routes/__tests__/test_dashboards.py | 205 +- .../__tests__/test_dataset_review_api.py | 230 +- .../src/api/routes/__tests__/test_datasets.py | 90 +- .../src/api/routes/__tests__/test_git_api.py | 108 +- .../routes/__tests__/test_git_status_route.py | 180 +- .../routes/__tests__/test_migration_routes.py | 29 +- .../api/routes/__tests__/test_profile_api.py | 120 +- .../api/routes/__tests__/test_reports_api.py | 73 +- .../__tests__/test_reports_detail_api.py | 57 +- .../test_reports_openapi_conformance.py | 57 +- .../api/routes/__tests__/test_tasks_logs.py | 44 +- backend/src/api/routes/admin.py | 145 +- backend/src/api/routes/assistant/__init__.py | 20 +- .../src/api/routes/assistant/_admin_routes.py | 65 +- .../api/routes/assistant/_command_parser.py | 34 +- .../api/routes/assistant/_dataset_review.py | 77 +- .../assistant/_dataset_review_dispatch.py | 39 +- backend/src/api/routes/assistant/_dispatch.py | 68 +- backend/src/api/routes/assistant/_history.py | 169 +- .../src/api/routes/assistant/_llm_planner.py | 63 +- .../routes/assistant/_llm_planner_intent.py | 37 +- .../src/api/routes/assistant/_resolvers.py | 125 +- backend/src/api/routes/assistant/_routes.py | 82 +- backend/src/api/routes/assistant/_schemas.py | 99 +- backend/src/api/routes/clean_release.py | 138 +- backend/src/api/routes/clean_release_v2.py | 123 +- backend/src/api/routes/connections.py | 78 +- backend/src/api/routes/dashboards/__init__.py | 26 +- .../api/routes/dashboards/_action_routes.py | 50 +- .../api/routes/dashboards/_detail_routes.py | 68 +- backend/src/api/routes/dashboards/_helpers.py | 80 +- .../api/routes/dashboards/_listing_routes.py | 37 +- .../src/api/routes/dashboards/_projection.py | 72 +- backend/src/api/routes/dashboards/_router.py | 9 +- backend/src/api/routes/dashboards/_schemas.py | 117 +- backend/src/api/routes/dataset_review.py | 14 +- .../dataset_review_pkg/_dependencies.py | 368 +- .../api/routes/dataset_review_pkg/_routes.py | 161 +- backend/src/api/routes/datasets.py | 163 +- backend/src/api/routes/environments.py | 79 +- backend/src/api/routes/git/__init__.py | 12 +- backend/src/api/routes/git/_config_routes.py | 45 +- backend/src/api/routes/git/_deps.py | 12 +- .../src/api/routes/git/_environment_routes.py | 17 +- backend/src/api/routes/git/_gitea_routes.py | 38 +- backend/src/api/routes/git/_helpers.py | 104 +- backend/src/api/routes/git/_merge_routes.py | 49 +- .../api/routes/git/_repo_lifecycle_routes.py | 37 +- .../api/routes/git/_repo_operations_routes.py | 70 +- backend/src/api/routes/git/_repo_routes.py | 54 +- backend/src/api/routes/git/_router.py | 10 +- backend/src/api/routes/git_schemas.py | 177 +- backend/src/api/routes/health.py | 36 +- backend/src/api/routes/llm.py | 154 +- backend/src/api/routes/mappings.py | 60 +- backend/src/api/routes/migration.py | 163 +- backend/src/api/routes/plugins.py | 32 +- backend/src/api/routes/profile.py | 66 +- backend/src/api/routes/reports.py | 71 +- backend/src/api/routes/settings.py | 257 +- backend/src/api/routes/storage.py | 73 +- backend/src/api/routes/tasks.py | 79 +- backend/src/app.py | 192 +- backend/src/core/__init__.py | 6 +- .../__tests__/test_config_manager_compat.py | 72 +- .../src/core/__tests__/test_native_filters.py | 190 +- .../test_superset_preview_pipeline.py | 110 +- .../__tests__/test_superset_profile_lookup.py | 79 +- .../__tests__/test_throttled_scheduler.py | 57 +- backend/src/core/async_superset_client.py | 168 +- backend/src/core/auth/__init__.py | 6 +- backend/src/core/auth/__tests__/test_auth.py | 93 +- backend/src/core/auth/config.py | 34 +- backend/src/core/auth/jwt.py | 40 +- backend/src/core/auth/logger.py | 26 +- backend/src/core/auth/oauth.py | 52 +- backend/src/core/auth/repository.py | 128 +- backend/src/core/auth/security.py | 38 +- backend/src/core/config_manager.py | 181 +- backend/src/core/config_models.py | 59 +- backend/src/core/cot_logger.py | 104 +- backend/src/core/database.py | 274 +- backend/src/core/database.py.bak | 737 + backend/src/core/encryption_key.py | 34 +- .../src/core/logger/__tests__/test_logger.py | 139 +- backend/src/core/mapping_service.py | 73 +- backend/src/core/middleware/__init__.py | 8 +- backend/src/core/middleware/trace.py | 29 +- backend/src/core/migration/__init__.py | 4 +- backend/src/core/migration/archive_parser.py | 62 +- .../core/migration/dry_run_orchestrator.py | 112 +- backend/src/core/migration/risk_assessor.py | 102 +- backend/src/core/migration_engine.py | 92 +- backend/src/core/plugin_base.py | 284 +- backend/src/core/plugin_loader.py | 84 +- backend/src/core/scheduler.py | 114 +- backend/src/core/scheduler.py.bak | 215 + backend/src/core/superset_client/__init__.py | 55 +- backend/src/core/superset_client/_base.py | 130 +- backend/src/core/superset_client/_charts.py | 45 +- .../core/superset_client/_dashboards_crud.py | 74 +- .../superset_client/_dashboards_filters.py | 76 +- .../core/superset_client/_dashboards_list.py | 60 +- .../src/core/superset_client/_databases.py | 56 +- backend/src/core/superset_client/_datasets.py | 69 +- .../core/superset_client/_datasets_preview.py | 41 +- .../_datasets_preview_filters.py | 38 +- .../core/superset_client/_user_projection.py | 41 +- backend/src/core/superset_profile_lookup.py | 75 +- backend/src/core/task_manager/__init__.py | 4 +- .../task_manager/__tests__/test_context.py | 22 +- .../__tests__/test_task_logger.py | 56 +- backend/src/core/task_manager/cleanup.py | 59 +- backend/src/core/task_manager/context.py | 120 +- backend/src/core/task_manager/manager.py | 404 +- backend/src/core/task_manager/models.py | 79 +- backend/src/core/task_manager/persistence.py | 244 +- backend/src/core/task_manager/task_logger.py | 112 +- backend/src/core/utils/__init__.py | 6 +- backend/src/core/utils/async_network.py | 184 +- backend/src/core/utils/dataset_mapper.py | 71 +- backend/src/core/utils/fileio.py | 162 +- backend/src/core/utils/matching.py | 23 +- backend/src/core/utils/network.py | 243 +- .../utils/superset_compilation_adapter.py | 164 +- .../superset_context_extractor/__init__.py | 51 +- .../utils/superset_context_extractor/_base.py | 115 +- .../superset_context_extractor/_filters.py | 30 +- .../superset_context_extractor/_parsing.py | 51 +- .../utils/superset_context_extractor/_pii.py | 35 +- .../superset_context_extractor/_recovery.py | 49 +- .../superset_context_extractor/_templates.py | 70 +- backend/src/dependencies.py | 161 +- .../models/__tests__/test_clean_release.py | 72 +- backend/src/models/__tests__/test_models.py | 23 +- .../models/__tests__/test_report_models.py | 11 +- backend/src/models/assistant.py | 53 +- backend/src/models/auth.py | 82 +- backend/src/models/auth.py.bak | 134 + backend/src/models/clean_release.py | 194 +- backend/src/models/config.py | 42 +- backend/src/models/connection.py | 21 +- backend/src/models/dashboard.py | 26 +- backend/src/models/dataset_review.py | 34 +- .../src/models/dataset_review_pkg/__init__.py | 10 +- .../_clarification_models.py | 51 +- .../src/models/dataset_review_pkg/_enums.py | 235 +- .../dataset_review_pkg/_execution_models.py | 49 +- .../dataset_review_pkg/_filter_models.py | 31 +- .../dataset_review_pkg/_finding_models.py | 22 +- .../dataset_review_pkg/_mapping_models.py | 24 +- .../dataset_review_pkg/_profile_models.py | 22 +- .../dataset_review_pkg/_semantic_models.py | 46 +- .../dataset_review_pkg/_session_models.py | 59 +- backend/src/models/filter_state.py | 57 +- backend/src/models/git.py | 25 +- backend/src/models/llm.py | 30 +- backend/src/models/mapping.py | 59 +- backend/src/models/profile.py | 27 +- backend/src/models/report.py | 96 +- backend/src/models/storage.py | 25 +- backend/src/models/task.py | 32 +- backend/src/plugins/__init__.py | 6 +- backend/src/plugins/backup.py | 401 +- backend/src/plugins/debug.py | 113 +- backend/src/plugins/git/__init__.py | 6 +- backend/src/plugins/git/llm_extension.py | 24 +- backend/src/plugins/git_plugin.py | 107 +- backend/src/plugins/llm_analysis/__init__.py | 4 +- .../__tests__/test_client_headers.py | 22 +- .../__tests__/test_screenshot_service.py | 94 +- .../llm_analysis/__tests__/test_service.py | 34 +- backend/src/plugins/llm_analysis/models.py | 58 +- backend/src/plugins/llm_analysis/plugin.py | 70 +- backend/src/plugins/llm_analysis/scheduler.py | 24 +- backend/src/plugins/llm_analysis/service.py | 204 +- backend/src/plugins/mapper.py | 93 +- backend/src/plugins/migration.py | 118 +- backend/src/plugins/search.py | 99 +- backend/src/plugins/storage/plugin.py | 169 +- .../plugins/translate/__tests__/__init__.py | 6 +- .../test_clickhouse_insert_integration.py | 30 +- .../translate/__tests__/test_dictionary.py | 173 +- .../translate/__tests__/test_orchestrator.py | 26 +- .../translate/__tests__/test_preview.py | 114 +- .../translate/__tests__/test_sql_generator.py | 48 +- .../test_settings_and_health_schemas.py | 33 +- backend/src/schemas/auth.py | 89 +- backend/src/schemas/dataset_review.py | 14 +- .../schemas/dataset_review_pkg/_composites.py | 69 +- .../src/schemas/dataset_review_pkg/_dtos.py | 74 +- backend/src/schemas/health.py | 24 +- backend/src/schemas/profile.py | 86 +- backend/src/schemas/settings.py | 42 +- backend/src/scripts/__init__.py | 6 +- backend/src/scripts/clean_release_cli.py | 144 +- backend/src/scripts/clean_release_tui.py | 42 +- backend/src/scripts/create_admin.py | 28 +- backend/src/scripts/init_auth_db.py | 33 +- .../src/scripts/migrate_sqlite_to_postgres.py | 79 +- backend/src/scripts/seed_permissions.py | 48 +- .../src/scripts/seed_superset_load_test.py | 98 +- .../test_dataset_dashboard_relations.py | 8 +- backend/src/services/__init__.py | 7 +- .../__tests__/test_encryption_manager.py | 96 +- .../services/__tests__/test_health_service.py | 33 +- .../__tests__/test_llm_plugin_persistence.py | 111 +- .../__tests__/test_llm_prompt_templates.py | 80 +- .../services/__tests__/test_llm_provider.py | 102 +- .../__tests__/test_rbac_permission_catalog.py | 62 +- .../__tests__/test_resource_service.py | 88 +- backend/src/services/auth_service.py | 121 +- .../src/services/clean_release/__init__.py | 17 +- .../__tests__/test_audit_service.py | 36 +- .../__tests__/test_compliance_orchestrator.py | 46 +- .../__tests__/test_manifest_builder.py | 34 +- .../__tests__/test_policy_engine.py | 56 +- .../__tests__/test_preparation_service.py | 70 +- .../__tests__/test_report_builder.py | 62 +- .../__tests__/test_source_isolation.py | 36 +- .../clean_release/__tests__/test_stages.py | 44 +- .../clean_release/approval_service.py | 50 +- .../clean_release/artifact_catalog_loader.py | 10 +- .../clean_release/candidate_service.py | 40 +- .../compliance_execution_service.py | 84 +- .../clean_release/compliance_orchestrator.py | 94 +- .../clean_release/demo_data_service.py | 44 +- backend/src/services/clean_release/dto.py | 11 +- backend/src/services/clean_release/enums.py | 11 +- .../src/services/clean_release/exceptions.py | 11 +- backend/src/services/clean_release/facade.py | 11 +- .../clean_release/manifest_builder.py | 34 +- .../clean_release/manifest_service.py | 36 +- backend/src/services/clean_release/mappers.py | 11 +- .../services/clean_release/policy_engine.py | 32 +- .../policy_resolution_service.py | 38 +- .../clean_release/preparation_service.py | 28 +- .../clean_release/publication_service.py | 50 +- .../services/clean_release/report_builder.py | 16 +- .../clean_release/repositories/__init__.py | 9 +- .../repositories/approval_repository.py | 11 +- .../repositories/artifact_repository.py | 11 +- .../repositories/audit_repository.py | 11 +- .../repositories/candidate_repository.py | 11 +- .../repositories/compliance_repository.py | 11 +- .../repositories/manifest_repository.py | 91 +- .../repositories/policy_repository.py | 11 +- .../repositories/publication_repository.py | 11 +- .../repositories/report_repository.py | 11 +- .../src/services/clean_release/repository.py | 20 +- .../clean_release/source_isolation.py | 14 +- .../services/clean_release/stages/__init__.py | 56 +- .../src/services/clean_release/stages/base.py | 54 +- .../clean_release/stages/data_purity.py | 26 +- .../stages/internal_sources_only.py | 26 +- .../stages/manifest_consistency.py | 26 +- .../stages/no_external_endpoints.py | 26 +- .../src/services/dataset_review/__init__.py | 11 +- .../dataset_review/clarification_engine.py | 128 +- .../clarification_pkg/_helpers.py | 69 +- .../services/dataset_review/event_logger.py | 85 +- .../services/dataset_review/orchestrator.py | 161 +- .../orchestrator_pkg/_commands.py | 55 +- .../orchestrator_pkg/_helpers.py | 85 +- .../__tests__/test_session_repository.py | 108 +- .../repositories/repository_pkg/_mutations.py | 69 +- .../repositories/session_repository.py | 161 +- .../dataset_review/semantic_resolver.py | 164 +- backend/src/services/git/__init__.py | 37 +- backend/src/services/git/_base.py | 131 +- backend/src/services/git/_branch.py | 63 +- backend/src/services/git/_gitea.py | 131 +- backend/src/services/git/_merge.py | 79 +- backend/src/services/git/_remote_providers.py | 67 +- backend/src/services/git/_status.py | 55 +- backend/src/services/git/_sync.py | 41 +- backend/src/services/git/_url.py | 107 +- backend/src/services/git_service.py | 7 +- backend/src/services/health_service.py | 136 +- backend/src/services/llm_prompt_templates.py | 87 +- backend/src/services/llm_provider.py | 197 +- backend/src/services/mapping_service.py | 66 +- .../src/services/notifications/__init__.py | 8 +- .../__tests__/test_notification_service.py | 9 +- .../src/services/notifications/providers.py | 74 +- backend/src/services/notifications/service.py | 148 +- backend/src/services/profile_service.py | 356 +- .../src/services/rbac_permission_catalog.py | 103 +- backend/src/services/reports/__init__.py | 6 +- .../__tests__/test_report_normalizer.py | 38 +- .../reports/__tests__/test_report_service.py | 17 +- .../reports/__tests__/test_type_profiles.py | 40 +- backend/src/services/reports/normalizer.py | 66 +- .../src/services/reports/report_service.py | 163 +- backend/src/services/reports/type_profiles.py | 39 +- backend/src/services/resource_service.py | 173 +- frontend/src/components/DashboardGrid.svelte | 6 +- frontend/src/components/DynamicForm.svelte | 6 +- frontend/src/components/EnvSelector.svelte | 6 +- frontend/src/components/Footer.svelte | 6 +- frontend/src/components/MappingTable.svelte | 6 +- .../src/components/MissingMappingModal.svelte | 6 +- frontend/src/components/Navbar.svelte | 6 +- frontend/src/components/PasswordPrompt.svelte | 6 +- .../components/RepositoryDashboardGrid.svelte | 6 +- .../StartupEnvironmentWizard.svelte | 6 +- frontend/src/components/TaskHistory.svelte | 6 +- frontend/src/components/TaskList.svelte | 6 +- frontend/src/components/TaskLogViewer.svelte | 32 +- frontend/src/components/TaskRunner.svelte | 6 +- frontend/src/components/Toast.svelte | 6 +- .../src/components/auth/ProtectedRoute.svelte | 9 +- .../src/components/backups/BackupList.svelte | 6 +- .../components/backups/BackupManager.svelte | 6 +- .../src/components/git/BranchSelector.svelte | 6 +- .../src/components/git/CommitHistory.svelte | 6 +- .../src/components/git/CommitModal.svelte | 6 +- .../components/git/ConflictResolver.svelte | 6 +- .../src/components/git/DeploymentModal.svelte | 6 +- frontend/src/components/git/GitManager.svelte | 6 +- frontend/src/components/llm/DocPreview.svelte | 6 +- .../src/components/llm/ProviderConfig.svelte | 6 +- .../components/llm/ValidationReport.svelte | 6 +- .../src/components/storage/FileList.svelte | 6 +- .../src/components/storage/FileUpload.svelte | 6 +- .../src/components/tasks/LogEntryRow.svelte | 6 +- .../src/components/tasks/LogFilterBar.svelte | 6 +- .../src/components/tasks/TaskLogPanel.svelte | 6 +- .../components/tasks/TaskResultPanel.svelte | 8 + .../components/tools/ConnectionForm.svelte | 6 +- .../components/tools/ConnectionList.svelte | 6 +- .../src/components/tools/DebugTool.svelte | 6 +- .../src/components/tools/MapperTool.svelte | 6 +- frontend/src/lib/Counter.svelte | 6 +- frontend/src/lib/api.js | 9 + frontend/src/lib/api/assistant.js | 5 + frontend/src/lib/api/datasetReview.js | 5 + frontend/src/lib/api/reports.js | 8 + frontend/src/lib/api/translate.js | 5 + frontend/src/lib/auth/permissions.js | 5 + frontend/src/lib/auth/store.ts | 10 +- .../assistant/AssistantChatPanel.svelte | 6 +- .../AssistantClarificationCard.svelte | 6 +- .../dataset-review/CompiledSQLPreview.svelte | 6 +- .../ExecutionMappingReview.svelte | 6 +- .../LaunchConfirmationPanel.svelte | 6 +- .../dataset-review/SemanticLayerReview.svelte | 6 +- .../dataset-review/SourceIntakePanel.svelte | 6 +- .../ValidationFindingsPanel.svelte | 6 +- .../lib/components/health/HealthMatrix.svelte | 6 +- .../lib/components/health/PolicyForm.svelte | 6 +- .../lib/components/layout/Breadcrumbs.svelte | 10 +- .../src/lib/components/layout/Sidebar.svelte | 16 +- .../lib/components/layout/TaskDrawer.svelte | 17 +- .../lib/components/layout/TopNavbar.svelte | 20 +- .../lib/components/reports/ReportCard.svelte | 6 +- .../reports/ReportDetailPanel.svelte | 6 +- .../lib/components/reports/ReportsList.svelte | 6 +- .../translate/BulkCorrectionSidebar.svelte | 6 +- .../translate/ScheduleConfig.svelte | 6 +- .../translate/TermCorrectionPopup.svelte | 6 +- .../TranslationMetricsDashboard.svelte | 6 +- .../translate/TranslationPreview.svelte | 6 +- .../translate/TranslationRunProgress.svelte | 6 +- .../translate/TranslationRunResult.svelte | 6 +- frontend/src/lib/i18n/index.ts | 6 + frontend/src/lib/stores.js | 7 +- .../stores/__tests__/assistantChat.test.js | 5 + .../src/lib/stores/__tests__/mocks/stores.js | 3 + .../src/lib/stores/__tests__/setupTests.js | 7 + .../src/lib/stores/__tests__/test_activity.js | 6 + .../__tests__/test_datasetReviewSession.js | 5 + .../src/lib/stores/__tests__/test_sidebar.js | 5 + .../lib/stores/__tests__/test_taskDrawer.js | 5 + frontend/src/lib/stores/activity.js | 4 + frontend/src/lib/stores/assistantChat.js | 7 + .../src/lib/stores/datasetReviewSession.js | 12 + frontend/src/lib/stores/environmentContext.js | 9 + frontend/src/lib/stores/health.js | 8 + frontend/src/lib/stores/sidebar.js | 7 + frontend/src/lib/stores/taskDrawer.js | 7 + frontend/src/lib/toasts.js | 7 +- frontend/src/lib/ui/Button.svelte | 8 +- frontend/src/lib/ui/Card.svelte | 8 +- frontend/src/lib/ui/Icon.svelte | 6 +- frontend/src/lib/ui/Input.svelte | 6 +- frontend/src/lib/ui/LanguageSwitcher.svelte | 6 +- frontend/src/lib/ui/PageHeader.svelte | 6 +- frontend/src/lib/ui/Select.svelte | 6 +- frontend/src/lib/utils.js | 4 + frontend/src/lib/utils/debounce.js | 4 + frontend/src/pages/Dashboard.svelte | 6 +- frontend/src/pages/Settings.svelte | 550 +- frontend/src/routes/+error.svelte | 20 +- frontend/src/routes/+layout.svelte | 51 +- frontend/src/routes/+page.svelte | 38 +- frontend/src/routes/admin/roles/+page.svelte | 6 +- .../src/routes/admin/settings/+page.svelte | 6 +- .../routes/admin/settings/llm/+page.svelte | 6 +- frontend/src/routes/admin/users/+page.svelte | 6 +- frontend/src/routes/dashboards/+page.svelte | 18 +- .../src/routes/dashboards/[id]/+page.svelte | 20 +- .../components/DashboardGitManager.svelte | 6 +- .../[id]/components/DashboardHeader.svelte | 14 +- .../DashboardLinkedResources.svelte | 6 +- .../components/DashboardTaskHistory.svelte | 6 +- .../src/routes/dashboards/health/+page.svelte | 22 +- frontend/src/routes/datasets/+page.svelte | 16 +- .../src/routes/datasets/[id]/+page.svelte | 13 +- .../src/routes/datasets/review/+page.svelte | 338 +- .../routes/datasets/review/[id]/+page.svelte | 45 +- frontend/src/routes/git/+page.svelte | 39 +- frontend/src/routes/login/+page.svelte | 44 +- frontend/src/routes/migration/+page.svelte | 59 +- .../routes/migration/mappings/+page.svelte | 10 +- frontend/src/routes/profile/+page.svelte | 920 +- frontend/src/routes/reports/+page.svelte | 55 +- .../routes/reports/llm/[taskId]/+page.svelte | 6 +- frontend/src/routes/settings/+page.svelte | 6 +- .../routes/settings/EnvironmentsTab.svelte | 6 +- .../routes/settings/automation/+page.svelte | 6 +- .../routes/settings/connections/+page.svelte | 6 +- frontend/src/routes/settings/git/+page.svelte | 6 +- .../settings/notifications/+page.svelte | 6 +- frontend/src/routes/storage/+page.svelte | 18 +- .../src/routes/storage/backups/+page.svelte | 6 +- .../src/routes/storage/repos/+page.svelte | 12 +- .../src/routes/tools/backups/+page.svelte | 6 +- frontend/src/routes/tools/debug/+page.svelte | 6 +- frontend/src/routes/tools/mapper/+page.svelte | 6 +- .../src/routes/tools/storage/+page.svelte | 6 +- frontend/src/routes/translate/+page.svelte | 12 +- .../src/routes/translate/[id]/+page.svelte | 15 +- .../translate/dictionaries/+page.svelte | 6 +- .../translate/dictionaries/[id]/+page.svelte | 6 +- .../src/routes/translate/history/+page.svelte | 6 +- frontend/src/services/adminService.js | 6 + frontend/src/services/connectionService.js | 6 +- frontend/src/services/gitService.js | 6 + frontend/src/services/storageService.js | 6 + frontend/src/services/taskService.js | 8 +- frontend/src/services/toolsService.js | 6 +- frontend/src/types/backup.ts | 3 + frontend/src/types/dashboard.ts | 3 + 461 files changed, 31360 insertions(+), 25040 deletions(-) create mode 100644 backend/_convert_defs.py create mode 100644 backend/src/core/database.py.bak create mode 100644 backend/src/core/scheduler.py.bak create mode 100644 backend/src/models/auth.py.bak diff --git a/.axiom/runtime/belief_events.jsonl b/.axiom/runtime/belief_events.jsonl index 1f4e0779..125f6c02 100644 --- a/.axiom/runtime/belief_events.jsonl +++ b/.axiom/runtime/belief_events.jsonl @@ -24,3 +24,403 @@ {"timestamp":1778595227.436,"event_type":"belief_explore","component":"Axiom:Services:Contract:Rebuild:SemanticIndex","data":{"depth":1,"extra":{"attempted_count":21885},"message":"Embedding refresh skipped because no provider is configured."}} {"recorded_at":"2026-05-12T14:13:47.492602315Z","anchor_id":"Axiom:Services:Contract:Rebuild:SemanticIndex","marker":"reflect","message":"Semantic index snapshot persisted.","depth":1,"extra":{"contract_count":3052,"index_path":"/home/busya/dev/ss-tools/.axiom/semantic_index/index.json"}} {"timestamp":1778595227.492,"event_type":"belief_reflect","component":"Axiom:Services:Contract:Rebuild:SemanticIndex","data":{"depth":1,"extra":{"contract_count":3052,"index_path":"/home/busya/dev/ss-tools/.axiom/semantic_index/index.json"},"message":"Semantic index snapshot persisted."}} +{"recorded_at":"2026-05-12T16:37:24.276803768Z","anchor_id":"Axiom:Services:Contract:Rebuild:SemanticIndex","marker":"reason","message":"Rebuilding the semantic index snapshot for the workspace.","depth":1,"extra":{"rebuild_mode":"full","refresh_embeddings":true}} +{"timestamp":1778603844.276,"event_type":"belief_reason","component":"Axiom:Services:Contract:Rebuild:SemanticIndex","data":{"depth":1,"extra":{"rebuild_mode":"full","refresh_embeddings":true},"message":"Rebuilding the semantic index snapshot for the workspace."}} +{"recorded_at":"2026-05-12T16:38:51.288318891Z","anchor_id":"Axiom:Services:Contract:Rebuild:SemanticIndex","marker":"explore","message":"Embedding refresh skipped because no provider is configured.","depth":1,"extra":{"attempted_count":21885}} +{"timestamp":1778603931.288,"event_type":"belief_explore","component":"Axiom:Services:Contract:Rebuild:SemanticIndex","data":{"depth":1,"extra":{"attempted_count":21885},"message":"Embedding refresh skipped because no provider is configured."}} +{"recorded_at":"2026-05-12T16:38:51.351644666Z","anchor_id":"Axiom:Services:Contract:Rebuild:SemanticIndex","marker":"reflect","message":"Semantic index snapshot persisted.","depth":1,"extra":{"contract_count":3052,"index_path":"/home/busya/dev/ss-tools/.axiom/semantic_index/index.json"}} +{"timestamp":1778603931.351,"event_type":"belief_reflect","component":"Axiom:Services:Contract:Rebuild:SemanticIndex","data":{"depth":1,"extra":{"contract_count":3052,"index_path":"/home/busya/dev/ss-tools/.axiom/semantic_index/index.json"},"message":"Semantic index snapshot persisted."}} +{"timestamp":1778603996.841,"event_type":"semantic_index_reindex","component":"semantic_index","data":{"contract_count":3051}} +{"timestamp":1778604131.002,"event_type":"semantic_index_reindex","component":"semantic_index","data":{"contract_count":3057}} +{"recorded_at":"2026-05-12T16:49:01.010716173Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"ps aux | grep -i mcp","timeout_seconds":10}} +{"timestamp":1778604541.01,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"ps aux | grep -i mcp","timeout_seconds":10},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:49:01.160504061Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778604541.16,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} +{"recorded_at":"2026-05-12T16:49:01.189254358Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"echo \"hello\"","timeout_seconds":5}} +{"timestamp":1778604541.189,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"echo \"hello\"","timeout_seconds":5},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:49:01.338029667Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778604541.338,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} +{"recorded_at":"2026-05-12T16:50:10.424571219Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"ps aux | grep -i axiom 2>/dev/null; echo \"---\"; echo $MCP_SERVER_HOST 2>/dev/null; echo \"---\"; which axiom-mcp 2>/dev/null || echo \"no axiom-mcp\"","timeout_seconds":10}} +{"timestamp":1778604610.424,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"ps aux | grep -i axiom 2>/dev/null; echo \"---\"; echo $MCP_SERVER_HOST 2>/dev/null; echo \"---\"; which axiom-mcp 2>/dev/null || echo \"no axiom-mcp\"","timeout_seconds":10},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:50:10.574784009Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778604610.574,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} +{"recorded_at":"2026-05-12T16:50:28.263938599Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"regex"}} +{"timestamp":1778604628.263,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"regex"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:50:28.264003330Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604628.264,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:50:28.264299623Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"65785024-ec82-434a-8544-6d40a02c2b7d"}} +{"timestamp":1778604628.264,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"65785024-ec82-434a-8544-6d40a02c2b7d"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:50:28.276334908Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"65785024-ec82-434a-8544-6d40a02c2b7d","path":"backend/src/app.py"}} +{"timestamp":1778604628.276,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"65785024-ec82-434a-8544-6d40a02c2b7d","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:50:45.946333898Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604645.946,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:50:45.946401554Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604645.946,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:50:45.946730047Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"06e2c8db-bbb9-497a-87ad-dddebf1fad2c"}} +{"timestamp":1778604645.946,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"06e2c8db-bbb9-497a-87ad-dddebf1fad2c"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:50:45.958128073Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"06e2c8db-bbb9-497a-87ad-dddebf1fad2c","path":"backend/src/app.py"}} +{"timestamp":1778604645.958,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"06e2c8db-bbb9-497a-87ad-dddebf1fad2c","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:50:48.082354701Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604648.082,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:50:48.082405596Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604648.082,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:50:48.082627019Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb59e5bf-c6c7-4bba-bf24-2266327cfe7a"}} +{"timestamp":1778604648.082,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb59e5bf-c6c7-4bba-bf24-2266327cfe7a"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:50:48.093848636Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"eb59e5bf-c6c7-4bba-bf24-2266327cfe7a","path":"backend/src/app.py"}} +{"timestamp":1778604648.093,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"eb59e5bf-c6c7-4bba-bf24-2266327cfe7a","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:50:50.399073364Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604650.399,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:50:50.399177278Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604650.399,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:50:50.399558459Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"fa0a2b10-2dbd-4b22-ae67-4bafc42a0fc0"}} +{"timestamp":1778604650.399,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"fa0a2b10-2dbd-4b22-ae67-4bafc42a0fc0"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:50:50.413249222Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"fa0a2b10-2dbd-4b22-ae67-4bafc42a0fc0","path":"backend/src/app.py"}} +{"timestamp":1778604650.413,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"fa0a2b10-2dbd-4b22-ae67-4bafc42a0fc0","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:07.160162203Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604667.16,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:07.160347649Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604667.16,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:07.160663809Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"9325a48a-9cf6-4225-99d0-dfd29169a66e"}} +{"timestamp":1778604667.16,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"9325a48a-9cf6-4225-99d0-dfd29169a66e"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:07.174546841Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"9325a48a-9cf6-4225-99d0-dfd29169a66e","path":"backend/src/app.py"}} +{"timestamp":1778604667.174,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"9325a48a-9cf6-4225-99d0-dfd29169a66e","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:09.017768805Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604669.017,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:09.017822786Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604669.017,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:09.018118047Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"728441ad-e1f5-4b96-892e-ea0e2b9d9e70"}} +{"timestamp":1778604669.018,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"728441ad-e1f5-4b96-892e-ea0e2b9d9e70"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:09.032403259Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"728441ad-e1f5-4b96-892e-ea0e2b9d9e70","path":"backend/src/app.py"}} +{"timestamp":1778604669.032,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"728441ad-e1f5-4b96-892e-ea0e2b9d9e70","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:11.130635305Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604671.13,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:11.130703442Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604671.13,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:11.131016747Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"11d826ae-c9f0-4025-9127-18c86636eacc"}} +{"timestamp":1778604671.131,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"11d826ae-c9f0-4025-9127-18c86636eacc"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:11.145296589Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"11d826ae-c9f0-4025-9127-18c86636eacc","path":"backend/src/app.py"}} +{"timestamp":1778604671.145,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"11d826ae-c9f0-4025-9127-18c86636eacc","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:24.171456704Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604684.171,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:24.171510965Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604684.171,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:24.171774968Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"4d2f1f24-bb1e-4693-8532-de5848cb9f3b"}} +{"timestamp":1778604684.171,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"4d2f1f24-bb1e-4693-8532-de5848cb9f3b"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:24.184197775Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"4d2f1f24-bb1e-4693-8532-de5848cb9f3b","path":"backend/src/app.py"}} +{"timestamp":1778604684.184,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"4d2f1f24-bb1e-4693-8532-de5848cb9f3b","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:26.311304741Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604686.311,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:26.311358762Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604686.311,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:26.311667518Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"fff0710b-00ca-4b1b-9fec-a250b3ada0e7"}} +{"timestamp":1778604686.311,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"fff0710b-00ca-4b1b-9fec-a250b3ada0e7"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:26.323285544Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"fff0710b-00ca-4b1b-9fec-a250b3ada0e7","path":"backend/src/app.py"}} +{"timestamp":1778604686.323,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"fff0710b-00ca-4b1b-9fec-a250b3ada0e7","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:28.154730810Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604688.154,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:28.154796933Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604688.154,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:28.155061166Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"68323671-1977-4ca7-bea5-a9cadab9198a"}} +{"timestamp":1778604688.155,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"68323671-1977-4ca7-bea5-a9cadab9198a"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:28.165942036Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"68323671-1977-4ca7-bea5-a9cadab9198a","path":"backend/src/app.py"}} +{"timestamp":1778604688.165,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"68323671-1977-4ca7-bea5-a9cadab9198a","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:45.732948139Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604705.732,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:45.733007790Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604705.733,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:45.733398699Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"563497ee-fde9-4b96-b4a1-97243f269a6e"}} +{"timestamp":1778604705.733,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"563497ee-fde9-4b96-b4a1-97243f269a6e"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:45.744387331Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"563497ee-fde9-4b96-b4a1-97243f269a6e","path":"backend/src/dependencies.py"}} +{"timestamp":1778604705.744,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"563497ee-fde9-4b96-b4a1-97243f269a6e","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:47.886058267Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604707.886,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:47.886115153Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604707.886,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:47.886329543Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"aca81d53-f737-4125-901e-e2915bf2bedd"}} +{"timestamp":1778604707.886,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"aca81d53-f737-4125-901e-e2915bf2bedd"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:47.896759782Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"aca81d53-f737-4125-901e-e2915bf2bedd","path":"backend/src/dependencies.py"}} +{"timestamp":1778604707.896,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"aca81d53-f737-4125-901e-e2915bf2bedd","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:51:49.937910254Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604709.937,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:51:49.937983060Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604709.937,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:51:49.938273111Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"0f380f8a-5272-447f-8083-224c4cfb0c6e"}} +{"timestamp":1778604709.938,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"0f380f8a-5272-447f-8083-224c4cfb0c6e"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:51:49.947629617Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"0f380f8a-5272-447f-8083-224c4cfb0c6e","path":"backend/src/dependencies.py"}} +{"timestamp":1778604709.947,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"0f380f8a-5272-447f-8083-224c4cfb0c6e","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:03.039867528Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604723.039,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:03.040001888Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604723.04,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:03.040357772Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e8395936-66c4-4b23-a0ed-c20fe6d489dd"}} +{"timestamp":1778604723.04,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e8395936-66c4-4b23-a0ed-c20fe6d489dd"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:03.050256289Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"e8395936-66c4-4b23-a0ed-c20fe6d489dd","path":"backend/src/dependencies.py"}} +{"timestamp":1778604723.05,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"e8395936-66c4-4b23-a0ed-c20fe6d489dd","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:05.058326559Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604725.058,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:05.058381031Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604725.058,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:05.058623854Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"c18157b1-7222-4037-8646-cdf1cb2f5f06"}} +{"timestamp":1778604725.058,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"c18157b1-7222-4037-8646-cdf1cb2f5f06"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:05.070080518Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"c18157b1-7222-4037-8646-cdf1cb2f5f06","path":"backend/src/dependencies.py"}} +{"timestamp":1778604725.07,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"c18157b1-7222-4037-8646-cdf1cb2f5f06","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:07.146302047Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604727.146,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:07.146346550Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604727.146,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:07.146573884Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"f4390f2b-8dd6-4c1d-9d7b-79902d25438f"}} +{"timestamp":1778604727.146,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"f4390f2b-8dd6-4c1d-9d7b-79902d25438f"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:07.157448853Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"f4390f2b-8dd6-4c1d-9d7b-79902d25438f","path":"backend/src/dependencies.py"}} +{"timestamp":1778604727.157,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"f4390f2b-8dd6-4c1d-9d7b-79902d25438f","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:19.773281871Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604739.773,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:19.773340711Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604739.773,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:19.773655899Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"ce0b5ecc-677c-4e21-8330-cb135d7befd7"}} +{"timestamp":1778604739.773,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"ce0b5ecc-677c-4e21-8330-cb135d7befd7"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:19.784629823Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"ce0b5ecc-677c-4e21-8330-cb135d7befd7","path":"backend/src/dependencies.py"}} +{"timestamp":1778604739.784,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"ce0b5ecc-677c-4e21-8330-cb135d7befd7","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:21.611387024Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604741.611,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:21.611443409Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604741.611,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:21.611691612Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e824fbc2-7138-4992-9d6d-29cc34ab8c4d"}} +{"timestamp":1778604741.611,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e824fbc2-7138-4992-9d6d-29cc34ab8c4d"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:21.621468081Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"e824fbc2-7138-4992-9d6d-29cc34ab8c4d","path":"backend/src/dependencies.py"}} +{"timestamp":1778604741.621,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"e824fbc2-7138-4992-9d6d-29cc34ab8c4d","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:23.165213278Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604743.165,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:23.165257260Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604743.165,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:23.165482460Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"f5b840e5-2380-4970-b4c1-53bf0463620f"}} +{"timestamp":1778604743.165,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"f5b840e5-2380-4970-b4c1-53bf0463620f"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:23.175295267Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"f5b840e5-2380-4970-b4c1-53bf0463620f","path":"backend/src/dependencies.py"}} +{"timestamp":1778604743.175,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"f5b840e5-2380-4970-b4c1-53bf0463620f","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:36.630616688Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604756.63,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:36.630670698Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604756.63,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:36.630991888Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"012167b2-8dd6-41bd-8eed-24750ae21818"}} +{"timestamp":1778604756.631,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"012167b2-8dd6-41bd-8eed-24750ae21818"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:36.641349721Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"012167b2-8dd6-41bd-8eed-24750ae21818","path":"backend/src/dependencies.py"}} +{"timestamp":1778604756.641,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"012167b2-8dd6-41bd-8eed-24750ae21818","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:52:39.368795341Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"}} +{"timestamp":1778604759.368,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/dependencies.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:52:39.368861705Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"}} +{"timestamp":1778604759.368,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/dependencies.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:52:39.369244650Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"43ec1aea-21fc-40f0-9a78-a6f9b0a347d1"}} +{"timestamp":1778604759.369,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"43ec1aea-21fc-40f0-9a78-a6f9b0a347d1"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:52:39.379785184Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"43ec1aea-21fc-40f0-9a78-a6f9b0a347d1","path":"backend/src/dependencies.py"}} +{"timestamp":1778604759.379,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"43ec1aea-21fc-40f0-9a78-a6f9b0a347d1","path":"backend/src/dependencies.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:09.015760724Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604789.015,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:09.015815556Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604789.015,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:09.016170709Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b5e50fbb-06d0-4a22-8421-1d9bacc3acec"}} +{"timestamp":1778604789.016,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b5e50fbb-06d0-4a22-8421-1d9bacc3acec"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:09.020116630Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"b5e50fbb-06d0-4a22-8421-1d9bacc3acec","path":"backend/src/models/auth.py"}} +{"timestamp":1778604789.02,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"b5e50fbb-06d0-4a22-8421-1d9bacc3acec","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:11.089963262Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604791.089,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:11.090020168Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604791.09,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:11.090273982Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b14774f6-3c88-4f7e-8ba8-c635a59a77da"}} +{"timestamp":1778604791.09,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b14774f6-3c88-4f7e-8ba8-c635a59a77da"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:11.093886761Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"b14774f6-3c88-4f7e-8ba8-c635a59a77da","path":"backend/src/models/auth.py"}} +{"timestamp":1778604791.093,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"b14774f6-3c88-4f7e-8ba8-c635a59a77da","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:12.292831434Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604792.292,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:12.292886306Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604792.292,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:12.293207395Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"84b487bf-0e7c-4db8-8f1f-05b1f2ccd4b8"}} +{"timestamp":1778604792.293,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"84b487bf-0e7c-4db8-8f1f-05b1f2ccd4b8"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:12.297815632Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"84b487bf-0e7c-4db8-8f1f-05b1f2ccd4b8","path":"backend/src/models/auth.py"}} +{"timestamp":1778604792.297,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"84b487bf-0e7c-4db8-8f1f-05b1f2ccd4b8","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:14.741148799Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604794.741,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:14.741198041Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604794.741,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:14.741450241Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"0bab913c-fd8d-40ce-af82-46efb7ced55d"}} +{"timestamp":1778604794.741,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"0bab913c-fd8d-40ce-af82-46efb7ced55d"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:14.744303515Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"0bab913c-fd8d-40ce-af82-46efb7ced55d","path":"backend/src/models/auth.py"}} +{"timestamp":1778604794.744,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"0bab913c-fd8d-40ce-af82-46efb7ced55d","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:16.391899577Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604796.391,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:16.391952996Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604796.391,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:16.392233079Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"18c1b4ba-723c-41b9-8bd2-e4a15cd16704"}} +{"timestamp":1778604796.392,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"18c1b4ba-723c-41b9-8bd2-e4a15cd16704"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:16.395330728Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"18c1b4ba-723c-41b9-8bd2-e4a15cd16704","path":"backend/src/models/auth.py"}} +{"timestamp":1778604796.395,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"18c1b4ba-723c-41b9-8bd2-e4a15cd16704","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:18.457016005Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604798.457,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:18.457080204Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604798.457,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:18.457356650Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb2f9318-cb57-470b-a496-3e697667b8f4"}} +{"timestamp":1778604798.457,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb2f9318-cb57-470b-a496-3e697667b8f4"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:18.460383367Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"eb2f9318-cb57-470b-a496-3e697667b8f4","path":"backend/src/models/auth.py"}} +{"timestamp":1778604798.46,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"eb2f9318-cb57-470b-a496-3e697667b8f4","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:32.360678759Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604812.36,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:32.360733101Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604812.36,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:32.361058578Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"4d6b5428-510f-404b-8794-6ff1c9c203fa"}} +{"timestamp":1778604812.361,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"4d6b5428-510f-404b-8794-6ff1c9c203fa"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:32.364144655Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"4d6b5428-510f-404b-8794-6ff1c9c203fa","path":"backend/src/models/auth.py"}} +{"timestamp":1778604812.364,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"4d6b5428-510f-404b-8794-6ff1c9c203fa","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:34.067946955Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604814.067,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:34.068009641Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604814.068,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:34.068365646Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"59b77a17-a7d3-4c6f-98e3-663a578e6b44"}} +{"timestamp":1778604814.068,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"59b77a17-a7d3-4c6f-98e3-663a578e6b44"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:34.074524765Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"59b77a17-a7d3-4c6f-98e3-663a578e6b44","path":"backend/src/models/auth.py"}} +{"timestamp":1778604814.074,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"59b77a17-a7d3-4c6f-98e3-663a578e6b44","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:35.832580171Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604815.832,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:35.832699263Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604815.832,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:35.833073602Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"ff804079-607a-41d8-bf52-d292b55af697"}} +{"timestamp":1778604815.833,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"ff804079-607a-41d8-bf52-d292b55af697"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:35.836484475Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"ff804079-607a-41d8-bf52-d292b55af697","path":"backend/src/models/auth.py"}} +{"timestamp":1778604815.836,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"ff804079-607a-41d8-bf52-d292b55af697","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:37.425281864Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"}} +{"timestamp":1778604817.425,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/auth.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:37.425323692Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"}} +{"timestamp":1778604817.425,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/auth.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:37.425553561Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"52a5a398-06a2-458f-bb29-5b6d8d7ad6d0"}} +{"timestamp":1778604817.425,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"52a5a398-06a2-458f-bb29-5b6d8d7ad6d0"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:37.428768348Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"52a5a398-06a2-458f-bb29-5b6d8d7ad6d0","path":"backend/src/models/auth.py"}} +{"timestamp":1778604817.428,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"52a5a398-06a2-458f-bb29-5b6d8d7ad6d0","path":"backend/src/models/auth.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:55.858635074Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"}} +{"timestamp":1778604835.858,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:55.858697420Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"}} +{"timestamp":1778604835.858,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:55.859098018Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e268a461-ffb9-4c14-928b-a249c72a0ae5"}} +{"timestamp":1778604835.859,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e268a461-ffb9-4c14-928b-a249c72a0ae5"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:55.862259816Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"e268a461-ffb9-4c14-928b-a249c72a0ae5","path":"backend/src/models/config.py"}} +{"timestamp":1778604835.862,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"e268a461-ffb9-4c14-928b-a249c72a0ae5","path":"backend/src/models/config.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:53:58.402896238Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"}} +{"timestamp":1778604838.402,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:53:58.402945791Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"}} +{"timestamp":1778604838.402,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:53:58.403217427Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2d4aa66f-c9df-402f-b9c6-5305276be250"}} +{"timestamp":1778604838.403,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2d4aa66f-c9df-402f-b9c6-5305276be250"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:53:58.406242941Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"2d4aa66f-c9df-402f-b9c6-5305276be250","path":"backend/src/models/config.py"}} +{"timestamp":1778604838.406,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"2d4aa66f-c9df-402f-b9c6-5305276be250","path":"backend/src/models/config.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:01.065684434Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"}} +{"timestamp":1778604841.065,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/config.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:01.065741521Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"}} +{"timestamp":1778604841.065,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/config.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:01.066004672Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"279ae318-8fe2-456b-ace4-3cbbcb6e40c6"}} +{"timestamp":1778604841.066,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"279ae318-8fe2-456b-ace4-3cbbcb6e40c6"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:01.069003997Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"279ae318-8fe2-456b-ace4-3cbbcb6e40c6","path":"backend/src/models/config.py"}} +{"timestamp":1778604841.069,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"279ae318-8fe2-456b-ace4-3cbbcb6e40c6","path":"backend/src/models/config.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:13.205341445Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"}} +{"timestamp":1778604853.205,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:13.205396187Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"}} +{"timestamp":1778604853.205,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:13.206004652Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"545ac444-65f2-432f-ae70-d1fb4b2ea06c"}} +{"timestamp":1778604853.206,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"545ac444-65f2-432f-ae70-d1fb4b2ea06c"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:13.208849158Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"545ac444-65f2-432f-ae70-d1fb4b2ea06c","path":"backend/src/models/task.py"}} +{"timestamp":1778604853.208,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"545ac444-65f2-432f-ae70-d1fb4b2ea06c","path":"backend/src/models/task.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:14.718460177Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"}} +{"timestamp":1778604854.718,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:14.718512515Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"}} +{"timestamp":1778604854.718,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:14.718816372Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"8ab11ac4-7964-40c8-bc90-3701595c9a0f"}} +{"timestamp":1778604854.718,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"8ab11ac4-7964-40c8-bc90-3701595c9a0f"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:14.721995532Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"8ab11ac4-7964-40c8-bc90-3701595c9a0f","path":"backend/src/models/task.py"}} +{"timestamp":1778604854.722,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"8ab11ac4-7964-40c8-bc90-3701595c9a0f","path":"backend/src/models/task.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:16.715868431Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"}} +{"timestamp":1778604856.715,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/models/task.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:16.715924135Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"}} +{"timestamp":1778604856.715,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/models/task.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:16.716220839Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"d5bfdcd7-260c-460c-98ac-942ebd5818e2"}} +{"timestamp":1778604856.716,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"d5bfdcd7-260c-460c-98ac-942ebd5818e2"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:16.719072809Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"d5bfdcd7-260c-460c-98ac-942ebd5818e2","path":"backend/src/models/task.py"}} +{"timestamp":1778604856.719,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"d5bfdcd7-260c-460c-98ac-942ebd5818e2","path":"backend/src/models/task.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:38.470804961Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/resource_service.py","target_mode":"exact_text"}} +{"timestamp":1778604878.47,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/resource_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:38.470866496Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/resource_service.py"}} +{"timestamp":1778604878.47,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/resource_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:38.471225115Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"867247d0-5bd9-49f8-aaac-d7d28c91ea54"}} +{"timestamp":1778604878.471,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"867247d0-5bd9-49f8-aaac-d7d28c91ea54"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:38.476361316Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"867247d0-5bd9-49f8-aaac-d7d28c91ea54","path":"backend/src/services/resource_service.py"}} +{"timestamp":1778604878.476,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"867247d0-5bd9-49f8-aaac-d7d28c91ea54","path":"backend/src/services/resource_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:54:40.452529044Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/resource_service.py","target_mode":"exact_text"}} +{"timestamp":1778604880.452,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/resource_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:54:40.452584748Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/resource_service.py"}} +{"timestamp":1778604880.452,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/resource_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:54:40.452903914Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"01badf77-d9a3-4b22-8956-9be0362275be"}} +{"timestamp":1778604880.452,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"01badf77-d9a3-4b22-8956-9be0362275be"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:54:40.459027667Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"01badf77-d9a3-4b22-8956-9be0362275be","path":"backend/src/services/resource_service.py"}} +{"timestamp":1778604880.459,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"01badf77-d9a3-4b22-8956-9be0362275be","path":"backend/src/services/resource_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:55:07.506699968Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"}} +{"timestamp":1778604907.506,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:55:07.506758437Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"}} +{"timestamp":1778604907.506,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:55:07.507085137Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b0eff5db-cbdf-4224-bd86-5b3509a772e1"}} +{"timestamp":1778604907.507,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"b0eff5db-cbdf-4224-bd86-5b3509a772e1"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:55:07.512046391Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"b0eff5db-cbdf-4224-bd86-5b3509a772e1","path":"backend/src/services/mapping_service.py"}} +{"timestamp":1778604907.512,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"b0eff5db-cbdf-4224-bd86-5b3509a772e1","path":"backend/src/services/mapping_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:55:10.187234635Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"}} +{"timestamp":1778604910.187,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:55:10.187322018Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"}} +{"timestamp":1778604910.187,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:55:10.187658425Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"45e204c0-6492-4991-aeeb-3f6f68ebf106"}} +{"timestamp":1778604910.187,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"45e204c0-6492-4991-aeeb-3f6f68ebf106"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:55:10.193429420Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"45e204c0-6492-4991-aeeb-3f6f68ebf106","path":"backend/src/services/mapping_service.py"}} +{"timestamp":1778604910.193,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"45e204c0-6492-4991-aeeb-3f6f68ebf106","path":"backend/src/services/mapping_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:55:24.645284159Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"}} +{"timestamp":1778604924.645,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/mapping_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:55:24.645429670Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"}} +{"timestamp":1778604924.645,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/mapping_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:55:24.645886282Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"aa239f4d-eaca-4a19-99c0-1f69582d331c"}} +{"timestamp":1778604924.645,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"aa239f4d-eaca-4a19-99c0-1f69582d331c"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:55:24.650650279Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"aa239f4d-eaca-4a19-99c0-1f69582d331c","path":"backend/src/services/mapping_service.py"}} +{"timestamp":1778604924.65,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"aa239f4d-eaca-4a19-99c0-1f69582d331c","path":"backend/src/services/mapping_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:55:45.446283411Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"}} +{"timestamp":1778604945.446,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:55:45.446330208Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"}} +{"timestamp":1778604945.446,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:55:45.446603227Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e11ba4bf-89b1-4237-9cc4-6c140eda1b82"}} +{"timestamp":1778604945.446,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"e11ba4bf-89b1-4237-9cc4-6c140eda1b82"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:55:45.452183356Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"e11ba4bf-89b1-4237-9cc4-6c140eda1b82","path":"backend/src/services/health_service.py"}} +{"timestamp":1778604945.452,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"e11ba4bf-89b1-4237-9cc4-6c140eda1b82","path":"backend/src/services/health_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:55:48.230781626Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"}} +{"timestamp":1778604948.23,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:55:48.230827832Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"}} +{"timestamp":1778604948.23,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:55:48.231079842Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb59c48b-564c-4445-8704-ce888e645a50"}} +{"timestamp":1778604948.231,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"eb59c48b-564c-4445-8704-ce888e645a50"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:55:48.236130013Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"eb59c48b-564c-4445-8704-ce888e645a50","path":"backend/src/services/health_service.py"}} +{"timestamp":1778604948.236,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"eb59c48b-564c-4445-8704-ce888e645a50","path":"backend/src/services/health_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:56:03.133596195Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"}} +{"timestamp":1778604963.133,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/services/health_service.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:56:03.133657920Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"}} +{"timestamp":1778604963.133,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/services/health_service.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:56:03.133968059Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2c5d87cc-94ce-4dec-89f6-7ed0aac2e607"}} +{"timestamp":1778604963.133,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2c5d87cc-94ce-4dec-89f6-7ed0aac2e607"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:56:03.140759026Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"2c5d87cc-94ce-4dec-89f6-7ed0aac2e607","path":"backend/src/services/health_service.py"}} +{"timestamp":1778604963.14,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"2c5d87cc-94ce-4dec-89f6-7ed0aac2e607","path":"backend/src/services/health_service.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:56:04.721815113Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reason","message":"Applying a workspace file patch after producing a preview diff.","depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"}} +{"timestamp":1778604964.721,"event_type":"belief_reason","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"path":"backend/src/app.py","target_mode":"exact_text"},"message":"Applying a workspace file patch after producing a preview diff."}} +{"recorded_at":"2026-05-12T16:56:04.721886637Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reason","message":"Capturing rollback state before a workspace mutation.","depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"}} +{"timestamp":1778604964.721,"event_type":"belief_reason","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"path_count":1,"reason":"patch backend/src/app.py"},"message":"Capturing rollback state before a workspace mutation."}} +{"recorded_at":"2026-05-12T16:56:04.722435541Z","anchor_id":"Axiom:Services:Checkpoint:CreateCheckpoint","marker":"reflect","message":"Checkpoint manifest persisted for future rollback.","depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2adbd64e-bdff-47f9-a706-23fca89d023c"}} +{"timestamp":1778604964.722,"event_type":"belief_reflect","component":"Axiom:Services:Checkpoint:CreateCheckpoint","data":{"depth":2,"extra":{"affected_paths":1,"checkpoint_id":"2adbd64e-bdff-47f9-a706-23fca89d023c"},"message":"Checkpoint manifest persisted for future rollback."}} +{"recorded_at":"2026-05-12T16:56:04.734974440Z","anchor_id":"Axiom:Services:Artifact:PatchWorkspaceFile","marker":"reflect","message":"Workspace file patch committed and checkpointed.","depth":1,"extra":{"checkpoint_id":"2adbd64e-bdff-47f9-a706-23fca89d023c","path":"backend/src/app.py"}} +{"timestamp":1778604964.735,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:PatchWorkspaceFile","data":{"depth":1,"extra":{"checkpoint_id":"2adbd64e-bdff-47f9-a706-23fca89d023c","path":"backend/src/app.py"},"message":"Workspace file patch committed and checkpointed."}} +{"recorded_at":"2026-05-12T16:56:33.905482123Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport re, os, json\n\n# Count anchors and check structure in key files\nfiles = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n 'backend/src/services/__init__.py',\n]\n\ntotal_defs = 0\ntotal_closes = 0\nc5_without_rationale = 0\nc1_with_extra = 0\nc3_with_prepost = 0\n\nfor f in files:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n content = fh.read()\n defs = re.findall(r'# \\[DEF:\\w+:\\w+\\]', content)\n closes = re.findall(r'# \\[/DEF:\\w+:\\w+\\]', content)\n total_defs += len(defs)\n total_closes += len(closes)\n \n # Check for C5 without RATIONALE\n c5_blocks = re.findall(r'# @COMPLEXITY: 5.*?(?=# \\[/DEF)', content, re.DOTALL)\n for block in c5_blocks:\n if '@RATIONALE' not in block:\n c5_without_rationale += 1\n \n # Check for C1 with forbidden tags\n c1_blocks = re.findall(r'# @COMPLEXITY: 1.*?(?=# \\[/DEF)', content, re.DOTALL)\n for block in c1_blocks:\n if '@PURPOSE' in block or '@BRIEF' in block or '@RELATION' in block:\n c1_with_extra += 1\n\nprint(json.dumps({\n 'total_def_anchors': total_defs,\n 'total_close_anchors': total_closes,\n 'mismatched': total_defs - total_closes,\n 'remaining_c5_no_rationale': c5_without_rationale,\n 'remaining_c1_with_extra': c1_with_extra,\n}, indent=2))\n\"\n","timeout_seconds":10}} +{"timestamp":1778604993.905,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport re, os, json\n\n# Count anchors and check structure in key files\nfiles = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n 'backend/src/services/__init__.py',\n]\n\ntotal_defs = 0\ntotal_closes = 0\nc5_without_rationale = 0\nc1_with_extra = 0\nc3_with_prepost = 0\n\nfor f in files:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n content = fh.read()\n defs = re.findall(r'# \\[DEF:\\w+:\\w+\\]', content)\n closes = re.findall(r'# \\[/DEF:\\w+:\\w+\\]', content)\n total_defs += len(defs)\n total_closes += len(closes)\n \n # Check for C5 without RATIONALE\n c5_blocks = re.findall(r'# @COMPLEXITY: 5.*?(?=# \\[/DEF)', content, re.DOTALL)\n for block in c5_blocks:\n if '@RATIONALE' not in block:\n c5_without_rationale += 1\n \n # Check for C1 with forbidden tags\n c1_blocks = re.findall(r'# @COMPLEXITY: 1.*?(?=# \\[/DEF)', content, re.DOTALL)\n for block in c1_blocks:\n if '@PURPOSE' in block or '@BRIEF' in block or '@RELATION' in block:\n c1_with_extra += 1\n\nprint(json.dumps({\n 'total_def_anchors': total_defs,\n 'total_close_anchors': total_closes,\n 'mismatched': total_defs - total_closes,\n 'remaining_c5_no_rationale': c5_without_rationale,\n 'remaining_c1_with_extra': c1_with_extra,\n}, indent=2))\n\"\n","timeout_seconds":10},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:56:34.053115878Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778604994.053,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} +{"recorded_at":"2026-05-12T16:56:41.528315203Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport re, os\n\n# Check region-format files too and measure file sizes\nextra_files = [\n 'backend/src/plugins/translate/plugin.py',\n 'backend/src/plugins/translate/orchestrator.py',\n 'backend/src/models/translate.py',\n]\n\nall_files_master = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n 'backend/src/services/__init__.py',\n 'backend/src/__init__.py',\n 'backend/src/models/__init__.py',\n 'backend/src/plugins/translate/plugin.py',\n 'backend/src/plugins/translate/orchestrator.py',\n 'backend/src/models/translate.py',\n]\n\nprint('=== File Size Check (< 400 LOC rule) ===')\nfor f in all_files_master:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n lines = len(fh.readlines())\n status = 'OK' if lines <= 400 else 'OVER'\n print(f'{status:4s} {lines:3d}L {f}')\n\nprint()\nprint('=== Region-format file check ===')\nfor f in extra_files:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n content = fh.read()\n opens = len(re.findall(r'#region\\s+\\w+', content))\n closes = len(re.findall(r'#endregion\\s+\\w+', content))\n match = 'OK' if opens == closes else 'MISMATCH'\n print(f'{match:8s} opens={opens} closes={closes} {f}')\n\"\n","timeout_seconds":10}} +{"timestamp":1778605001.528,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport re, os\n\n# Check region-format files too and measure file sizes\nextra_files = [\n 'backend/src/plugins/translate/plugin.py',\n 'backend/src/plugins/translate/orchestrator.py',\n 'backend/src/models/translate.py',\n]\n\nall_files_master = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n 'backend/src/services/__init__.py',\n 'backend/src/__init__.py',\n 'backend/src/models/__init__.py',\n 'backend/src/plugins/translate/plugin.py',\n 'backend/src/plugins/translate/orchestrator.py',\n 'backend/src/models/translate.py',\n]\n\nprint('=== File Size Check (< 400 LOC rule) ===')\nfor f in all_files_master:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n lines = len(fh.readlines())\n status = 'OK' if lines <= 400 else 'OVER'\n print(f'{status:4s} {lines:3d}L {f}')\n\nprint()\nprint('=== Region-format file check ===')\nfor f in extra_files:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if not os.path.exists(path):\n continue\n with open(path) as fh:\n content = fh.read()\n opens = len(re.findall(r'#region\\s+\\w+', content))\n closes = len(re.findall(r'#endregion\\s+\\w+', content))\n match = 'OK' if opens == closes else 'MISMATCH'\n print(f'{match:8s} opens={opens} closes={closes} {f}')\n\"\n","timeout_seconds":10},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:56:41.676534610Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778605001.676,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} +{"recorded_at":"2026-05-12T16:56:55.415592910Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reason","message":"Executing a read-only workspace command inside the project root.","depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport os\n\n# Summary stats\nfiles_fixed = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n]\n\ntotal_lines_before = 0\ntotal_lines_after = 0\nfor f in files_fixed:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if os.path.exists(path):\n with open(path) as fh:\n total_lines_after += len(fh.readlines())\n\nprint(f'Files audited: {len(files_fixed)}')\nprint(f'Total lines (fixed files): {total_lines_after}')\nprint()\nprint('=== File sizes post-fix ===')\nfor f in files_fixed:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if os.path.exists(path):\n with open(path) as fh:\n lines = len(fh.readlines())\n ok = 'OK' if lines <= 400 else 'OVER'\n print(f' {ok:4s} {lines:3d}L {f}')\n\"\n","timeout_seconds":10}} +{"timestamp":1778605015.415,"event_type":"belief_reason","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"command":"cd /home/busya/dev/ss-tools && python3 -c \"\nimport os\n\n# Summary stats\nfiles_fixed = [\n 'backend/src/app.py',\n 'backend/src/dependencies.py',\n 'backend/src/models/auth.py',\n 'backend/src/models/config.py',\n 'backend/src/models/task.py',\n 'backend/src/services/resource_service.py',\n 'backend/src/services/mapping_service.py',\n 'backend/src/services/health_service.py',\n]\n\ntotal_lines_before = 0\ntotal_lines_after = 0\nfor f in files_fixed:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if os.path.exists(path):\n with open(path) as fh:\n total_lines_after += len(fh.readlines())\n\nprint(f'Files audited: {len(files_fixed)}')\nprint(f'Total lines (fixed files): {total_lines_after}')\nprint()\nprint('=== File sizes post-fix ===')\nfor f in files_fixed:\n path = os.path.join('/home/busya/dev/ss-tools', f)\n if os.path.exists(path):\n with open(path) as fh:\n lines = len(fh.readlines())\n ok = 'OK' if lines <= 400 else 'OVER'\n print(f' {ok:4s} {lines:3d}L {f}')\n\"\n","timeout_seconds":10},"message":"Executing a read-only workspace command inside the project root."}} +{"recorded_at":"2026-05-12T16:56:55.567948460Z","anchor_id":"Axiom:Services:Artifact:RunWorkspaceCommand","marker":"reflect","message":"Workspace command completed and output was bounded for transport.","depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false}} +{"timestamp":1778605015.567,"event_type":"belief_reflect","component":"Axiom:Services:Artifact:RunWorkspaceCommand","data":{"depth":1,"extra":{"exit_code":0,"stderr_truncated":false,"stdout_truncated":false},"message":"Workspace command completed and output was bounded for transport."}} diff --git a/.axiom/semantic_index/graph.duckdb b/.axiom/semantic_index/graph.duckdb index f7b78731883bc962b4b831ade33781fb38df5c69..85d521b44e7b69c0082acd6133c147cf7c338092 100644 GIT binary patch delta 591552 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

5J@;RPRC)7#-aF=^m|s4DmUo=@2) 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

6xNH4)S#yHOz#II*`|em5KOQO754E5 zQUw#^7wJZBKX1n!OUijd9c&M(Kk&_z46Ql|N(?*-0KCTe;Uw&cEravQ?Cbs03n9JsC+H5cE34^6WNhlseu@}Y z(ror;Us_GSqL%jY97Pb1(TX-m_dc_}IN<%Taq>RE7-2sn$LF!3!3CMo&3Mu(E{*dy zOwxou5KNAhpY83FjplLDxHVJ{y3`XHm#i)Q3W$tO2z^Jyj?xuZugmQ9va1;kVkXs5 zVB3L{-}`MDo2u>Z;gi!8-w4{0uSK-8%uH;tTd`LmJ!z>zcOO7B+C{3HIsI!HpI6bP zga}i_;ry{g0v!h09Xy4HBQYVPU~8gPd48N*F=A9lbxmgQ8>!mv0v{!g8o8>A*|6e=_n?> zVL6+%dXzq;hYfT^9wtftVSgpoh^qrF(-X~-9-hbZ4)1Dohl zu3uYfq%6~2A0=U3++yTM(0W8Ro8g`$YF#R ziUOkGoSbZBR(EELujeeX%?$eoQs*RG4u|zZw==tnU`mpZ9*kVBh3~rnfalHb3EOk0 zct6u5M38Fgl=}V(oEYgn)gM>|;*Oi-bm0~3Ii>#Qwx7d=KtBOM7?zQrSMT?IJ3@2; zs|d)T?=Q%G>c%mr^98 z6r&vPSw03SrV>ABxq8}t$%D^!eHD+|?NdGnL^mGkAV}#r#dl;-BSJLrQ4h5Yve`NM1p z4@#k`9K>DQ9n(z0)kv@y!KEH%yr_kK3f&8F`=-aJp$USWvkiN-L4CAfDeFS3+VpjT z9JnRHRKTX@)z4)@)YDr|jlL1Gjdr#C1DP8E2YDjks5xEU>g}eDSK@{c&{N#z?S$o_ zo1WNg2`#xL69=bH623b3h|jVMN;bwK;NURTC9UEP{|FGBhECy+4&a?LW5;*_wQ8vs z)So?}FTDtK-=E(o4tkHH*{Z=CwWUxRMKqB;TfgjUy))dYJ;SjT=&XLh?_2;4cIX;=Q)4`9+vX8Z`jZr;ko{PkP}9NB9p9* zulII#5yWDV_5yo5zs&$DJi)26$vBD0KF>c05DyLrKNE>>;RAC)fV2zuv!cTJ{yyq7 z&{fCTp?KPX>9tpge!UU--APeU|8tM?2V1YDnf0PWaf~GYChu$%oANplJ)AnZ@0B+L z%6&j<`wndthz70}{uD&JVKVa;{|KF$vO&k>qM5gPyOj+?X(d` zB2;c=dh(g(A0hSfaSq}jJ(M`wW&7p+bAbI_(yi|#Fd7TA@kl1*t~@)2H%)F^F+N_J z=M$=9m16VV12D;5?s&ef{PQ6hj48(QZ*$Tg8EA6sC`DW4y}m<=`!wfp7)s!*>wVsC zna`qH;n+>aFTRj#wQlXo;hGYHhb*F%%lv(JhEyT(u%erM#X<6At|6&<@dN(Bs>ICq z+-l36lzqSnK)9cP7{H4br+9_GZ{Ai~bLgk=V8pv#5@o+b6K8|;xc>t=&tK-!Zqol zls>o!1CAJ^zwnVKfU4K(mJPl1As-1jbjDmKkRG=tA*`z)7ZXl^D+um4N1gnzgSQo4 z3ewkcWZ3P}2EQCkW2~-Gd#URt3y*sL6OHX>?Cu9(jOP5<%%{B{Gy`CMj(aMdnsGd?uVyOH!Wj9`^k1&wKmiBss<>yebYLQ*_YaYK9Dc zK^0f|qQ9R$aIrY0ew}9GtC*T$|8``56URL7icaU|xE2Kug?-b(6v_@RIyHl! z6U|Ot_!4{iRbX=PS*${s2!4-?TyLJ;y$=AI-24Aa2 zJ$|a?TfH4aP$Z~YVeYHGLMpl~gk;E(YEqtH#cy^xqDgBbX%P{RF`pcL>|Ud8hNc}J~H7Pml3BfH|I*K}yK zQY1)IY2+;TT~7B)VwIl9DuP&u)+VIKEB(XiF&Bh`XM`Y_mhbdIq&-6Jg{(+N0E6J| zs@D;0>1s}@fAUpe$$}5GRL05)(RaO1igp4lqDw|?Q}O%Wk%{!w%XXi@!Uz4nb@Jxw zT#1FTUnW<(jOM5=GnDQp4*?CwP*F zxXb)|BJ*I$u+35@*R!08!H;`Eu6vflkm?`LeV-X4K5_K=~Z??HbM_$hU3zv7eLQS@i^L_DR3 z{CyNbwZW+3KJC{Z0jR^Op-% z!&cIN)VVi9!6SpIaTZ6dl=N}Z*X`MVHlFQ)7lRg=(y~^395vB9ac-GA$*ZnDL!`I* zmSyeTAmv|qp+9p@ddADvW%e1I=EaSj9D=ft*0cZ5=f2V`-!>6W;B(Mcj@TFcQP}CYitB6`8f^jTG)Bu$#6grS#Uo7-H+p~PBvZz& z)5fO4jb1sO`G>!s)RkVf^UFNh>8Fu?W}0+71FvC8>u}2IBphTG|Ma)yl4!9GLEg&U zePBpX(r)vSc)H!FROa8YYezwK)KlE=@>6jtXim-NE@(9~2S(x&Dl^16mj2`KV+pjQ1qFO?8%u9?uo7>h^J(9l3!?RtmiX+JIqgUjlxdN09(-4XR_hwn2`J2 z1*O(3@DOr{P(_>kl?P}2p?8dd? z`8q$lJHLU4P4}0VSNkIEo)WT9H^IX+GmHH8TI3FbN+UVxI1=txE8Zo-Azf6YJZp9*f;Drs;|f2=cc3weNFWd-pwpPJfg+utITDm`rd=6S|r$f z4-efsXhu%_(Mr9si{Oc9j%^H9>Wfpv4htS32bH-!+JXI=%V`Nr-9!$gTl;zt6Il)z zHt0;4Mt)E4XKZ35z568`@WAETs0O)*iSY83WcGCwF}-7*Ro`*aR^986bTO5m=k_O} z!cZcUGEHCDRe#<@$#Jzgp&+!05U!hQ}xIMc(uS<(+SX{6`r{yvTi!{{#2mRZzX zP7}G1Ju^fqi-IGQx&A)bupKm~q3l}a8L=bjbw~WjVAv1wZNfD-$~ut85!?K`0Y%Yj z6U4D1;WeAz=o1}UmDusFgNp+9G7$~7mZ7o3e{yZ;7u;iO^t9)YIe ze_2*;zeCdMxiIR3pXQ-wSb&1aLt;yDfA6W2umS05avH~Zem6?M+pI`uwjMr~U*-S~ zG=q)u%+J#q25Es7Vb5fMZ8k~=dS}LAHt@)n_h++JILO<9L3I*;`GdVZyow?#C_>8@ zqe1)+Bw+An2ElNHHyCk2PA*8d(^LLkvx0BERsE^pQ6* z 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

Enter your login and password below

\n \n \n \n \n \n \"\"\"\n )\n\n assert result is True\n\n\n# [/DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n\n\n# [DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Locator helper must not reject a selector collection just because its first element is hidden.\n# @PRE: First matched element is hidden and second matched element is visible.\n# @POST: Helper returns the second visible candidate.\n@pytest.mark.anyio\nasync def test_find_first_visible_locator_skips_hidden_first_match():\n class _FakeElement:\n def __init__(self, visible, label):\n self._visible = visible\n self.label = label\n\n async def is_visible(self):\n return self._visible\n\n class _FakeLocator:\n def __init__(self, elements):\n self._elements = elements\n\n async def count(self):\n return len(self._elements)\n\n def nth(self, index):\n return self._elements[index]\n\n service = ScreenshotService(env=type(\"Env\", (), {})())\n hidden_then_visible = _FakeLocator(\n [\n _FakeElement(False, \"hidden\"),\n _FakeElement(True, \"visible\"),\n ]\n )\n\n result = await service._find_first_visible_locator([hidden_then_visible])\n\n assert result.label == \"visible\"\n\n\n# [/DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n\n\n# [DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must submit hidden fields and credentials through the context request cookie jar.\n# @PRE: Login DOM exposes csrf hidden field and request context returns authenticated HTML.\n# @POST: Helper returns True and request payload contains csrf_token plus credentials plus request options.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_uses_browser_context_request():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/welcome/\"\n\n async def text(self):\n return \"Welcome\"\n\n class _FakeRequest:\n def __init__(self):\n self.calls = []\n\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n self.calls.append(\n {\n \"url\": url,\n \"form\": dict(form or {}),\n \"headers\": dict(headers or {}),\n \"timeout\": timeout,\n \"fail_on_status_code\": fail_on_status_code,\n \"max_redirects\": max_redirects,\n }\n )\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator(\n [\n _FakeInput(\"csrf_token\", \"csrf-123\"),\n _FakeInput(\"next\", \"/superset/welcome/\"),\n ]\n )\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n page = _FakePage()\n\n result = await service._submit_login_via_form_post(\n page, \"https://example.test/login/\"\n )\n\n assert result is True\n assert page.context.request.calls == [\n {\n \"url\": \"https://example.test/login/\",\n \"form\": {\n \"csrf_token\": \"csrf-123\",\n \"next\": \"/superset/welcome/\",\n \"username\": \"admin\",\n \"password\": \"secret\",\n },\n \"headers\": {\n \"Origin\": \"https://example.test\",\n \"Referer\": \"https://example.test/login/\",\n },\n \"timeout\": 10000,\n \"fail_on_status_code\": False,\n \"max_redirects\": 0,\n }\n ]\n\n\n# [/DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n\n\n# [DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must treat non-login 302 redirect as success without waiting for redirect target.\n# @PRE: Request response is 302 with Location outside login path.\n# @POST: Helper returns True.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_accepts_authenticated_redirect():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 302\n url = \"https://example.test/login/\"\n headers = {\"location\": \"/superset/welcome/\"}\n\n async def text(self):\n return \"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is True\n\n\n# [/DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n\n\n# [DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must fail when POST response still contains login form content.\n# @PRE: Login DOM exposes csrf hidden field and request response renders login markup.\n# @POST: Helper returns False.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_rejects_login_markup_response():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/login/\"\n\n async def text(self):\n return \"\"\"\n \n \n Enter your login and password below\n Username:\n Password:\n Sign in\n \n \n \"\"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is False\n\n\n# [/DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n\n\n# [DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Pages with unstable primary wait must retry with fallback wait strategy.\n# @PRE: First page.goto call raises; second succeeds.\n# @POST: Helper returns second response and attempts both wait modes in order.\n@pytest.mark.anyio\nasync def test_goto_resilient_falls_back_from_domcontentloaded_to_load():\n class _FakePage:\n def __init__(self):\n self.calls = []\n\n async def goto(self, url, wait_until, timeout):\n self.calls.append((url, wait_until, timeout))\n if wait_until == \"domcontentloaded\":\n raise RuntimeError(\"primary wait failed\")\n return {\"ok\": True, \"url\": url, \"wait_until\": wait_until}\n\n page = _FakePage()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n response = await service._goto_resilient(\n page,\n \"https://example.test/dashboard\",\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=1234,\n )\n\n assert response[\"ok\"] is True\n assert page.calls == [\n (\"https://example.test/dashboard\", \"domcontentloaded\", 1234),\n (\"https://example.test/dashboard\", \"load\", 1234),\n ]\n\n\n# [/DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n# [/DEF:TestScreenshotService:Module]\n" }, @@ -61458,7 +63690,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.", "SEMANTICS": [ "tests", @@ -61477,15 +63709,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -61498,6 +63721,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -61622,7 +63846,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Define Pydantic models for LLM Analysis plugin.", "SEMANTICS": [ @@ -61667,7 +63891,17 @@ "PURPOSE": "Enum for supported LLM providers." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LLMProviderType:Class]\n# @PURPOSE: Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n# [/DEF:LLMProviderType:Class]\n" }, @@ -61683,7 +63917,17 @@ "PURPOSE": "Request model to fetch available models from a provider." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FetchModelsRequest:Class]\n# @PURPOSE: Request model to fetch available models from a provider.\nclass FetchModelsRequest(BaseModel):\n base_url: str = Field(description=\"Provider base URL (e.g. https://api.openai.com/v1)\")\n api_key: Optional[str] = Field(None, description=\"Optional API key for authenticated providers\")\n provider_type: LLMProviderType = Field(LLMProviderType.OPENAI, description=\"Provider type for auth headers\")\n provider_id: Optional[str] = Field(None, description=\"Existing provider ID — resolves api_key from DB if not provided directly\")\n# [/DEF:FetchModelsRequest:Class]\n" }, @@ -61699,7 +63943,17 @@ "PURPOSE": "Response with available model IDs from a provider." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FetchModelsResponse:Class]\n# @PURPOSE: Response with available model IDs from a provider.\nclass FetchModelsResponse(BaseModel):\n models: List[str] = Field(description=\"List of available model IDs\")\n total: int = Field(description=\"Total number of models found\")\n# [/DEF:FetchModelsResponse:Class]\n" }, @@ -61715,7 +63969,17 @@ "PURPOSE": "Configuration for an LLM provider." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LLMProviderConfig:Class]\n# @PURPOSE: Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: Optional[str] = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: Optional[str] = None\n default_model: str\n is_active: bool = True\n# [/DEF:LLMProviderConfig:Class]\n" }, @@ -61731,7 +63995,17 @@ "PURPOSE": "Enum for dashboard validation status." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationStatus:Class]\n# @PURPOSE: Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# [/DEF:ValidationStatus:Class]\n" }, @@ -61747,7 +64021,17 @@ "PURPOSE": "Model for a single issue detected during validation." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DetectedIssue:Class]\n# @PURPOSE: Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: Optional[str] = None\n# [/DEF:DetectedIssue:Class]\n" }, @@ -61763,7 +64047,17 @@ "PURPOSE": "Model for dashboard validation result." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationResult:Class]\n# @PURPOSE: Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: Optional[str] = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: Optional[str] = None\n issues: List[DetectedIssue]\n summary: str\n raw_response: Optional[str] = None\n# [/DEF:ValidationResult:Class]\n" }, @@ -61776,7 +64070,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All LLM interactions must be executed as asynchronous tasks.", "LAYER": "Domain", "PURPOSE": "Implements DashboardValidationPlugin and DocumentationPlugin.", @@ -61841,6 +64135,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -61882,6 +64177,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -61919,6 +64223,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -61944,6 +64257,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -61998,6 +64320,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62031,6 +64362,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -62085,6 +64425,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62107,7 +64456,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Provides helper functions to schedule LLM-based validation tasks.", "SEMANTICS": [ @@ -62149,6 +64498,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62183,6 +64541,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -62202,7 +64569,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Screenshots must be 1920px width and capture full page height.", "LAYER": "Domain", "PURPOSE": "Services for LLM interaction and dashboard screenshots.", @@ -62260,7 +64627,17 @@ "PURPOSE": "Handles capturing screenshots of Superset dashboards." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ScreenshotService:Class]\n# @PURPOSE: Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # [DEF:ScreenshotService.__init__:Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # [/DEF:ScreenshotService.__init__:Function]\n\n # [DEF:ScreenshotService._find_first_visible_locator:Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # [/DEF:ScreenshotService._find_first_visible_locator:Function]\n\n # [DEF:ScreenshotService._iter_login_roots:Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> List[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # [/DEF:ScreenshotService._iter_login_roots:Function]\n\n # [DEF:ScreenshotService._extract_hidden_login_fields:Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> Dict[str, str]:\n hidden_fields: Dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # [/DEF:ScreenshotService._extract_hidden_login_fields:Function]\n\n # [DEF:ScreenshotService._extract_csrf_token:Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # [/DEF:ScreenshotService._extract_csrf_token:Function]\n\n # [DEF:ScreenshotService._response_looks_like_login_page:Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # [/DEF:ScreenshotService._response_looks_like_login_page:Function]\n\n # [DEF:ScreenshotService._redirect_looks_authenticated:Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # [/DEF:ScreenshotService._redirect_looks_authenticated:Function]\n\n # [DEF:ScreenshotService._submit_login_via_form_post:Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # [/DEF:ScreenshotService._submit_login_via_form_post:Function]\n\n # [DEF:ScreenshotService._find_login_field_locator:Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # [/DEF:ScreenshotService._find_login_field_locator:Function]\n\n # [DEF:ScreenshotService._find_submit_locator:Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # [/DEF:ScreenshotService._find_submit_locator:Function]\n\n # [DEF:ScreenshotService._goto_resilient:Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # [/DEF:ScreenshotService._goto_resilient:Function]\n\n # [DEF:ScreenshotService.capture_dashboard:Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n \n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n \n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n \n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n \n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n \n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n \n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n \n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n \n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n \n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n \n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n \n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n \n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n \n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n \n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {str(e)}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {str(e)}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n \n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n \n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n \n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n \n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n \n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n \n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n \n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n \n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n \n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n \n if tab_id in processed_tabs:\n continue\n \n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n \n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n \n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n \n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n \n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n \n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n \n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n \n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n \n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n \n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n \n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n \n image_data = base64.b64decode(screenshot_data[\"data\"])\n \n with open(output_path, 'wb') as f:\n f.write(image_data)\n \n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n \n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n \n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n \n await browser.close()\n return True\n # [/DEF:ScreenshotService.capture_dashboard:Function]\n# [/DEF:ScreenshotService:Class]\n" }, @@ -62286,6 +64663,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62323,6 +64709,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62360,6 +64755,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62397,6 +64801,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62434,6 +64847,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62471,6 +64893,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62508,6 +64939,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62545,6 +64985,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62582,6 +65031,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62619,6 +65077,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62656,6 +65123,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62696,6 +65172,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62706,10 +65191,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62727,7 +65226,17 @@ "PURPOSE": "Wrapper for LLM provider APIs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LLMClient:Class]\n# @PURPOSE: Wrapper for LLM provider APIs.\nclass LLMClient:\n # [DEF:LLMClient.__init__:Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n \n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # [/DEF:LLMClient.__init__:Function]\n\n # [DEF:LLMClient._supports_json_response_format:Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # [/DEF:LLMClient._supports_json_response_format:Function]\n\n # [DEF:LLMClient.get_json_completion:Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n \n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {str(e)}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n \n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {str(e)}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {str(e)}\")\n \n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n \n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n \n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {str(e)}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n \n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # [/DEF:LLMClient.get_json_completion:Function]\n\n # [DEF:LLMClient.test_runtime_connection:Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> Dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # [/DEF:LLMClient.test_runtime_connection:Function]\n\n # [DEF:LLMClient.analyze_dashboard:Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> Dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n \n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n \n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n \n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n \n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {str(e)}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {str(e)}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # [/DEF:LLMClient.analyze_dashboard:Function]\n# [/DEF:LLMClient:Class]\n" }, @@ -62753,6 +65262,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62790,6 +65308,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -62829,6 +65356,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62876,6 +65412,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62923,6 +65468,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -62971,17 +65525,12 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Plugins' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Plugins" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -63005,6 +65554,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -63026,7 +65576,17 @@ "PURPOSE": "Plugin for mapping dataset columns verbose names." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MapperPlugin:Class]\n# @PURPOSE: Plugin for mapping dataset columns verbose names.\nclass MapperPlugin(PluginBase):\n \"\"\"\n Plugin for mapping dataset columns verbose names.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"dataset-mapper\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"dataset-mapper\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Dataset Mapper\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Map dataset column verbose names using PostgreSQL comments or Excel files.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the mapper plugin.\n # @RETURN: str - \"/tools/mapper\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/mapper\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (e.g., 'dev').\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset to update.\"\n },\n \"source\": {\n \"type\": \"string\",\n \"title\": \"Mapping Source\",\n \"enum\": [\"postgres\", \"excel\"],\n \"default\": \"postgres\"\n },\n \"connection_id\": {\n \"type\": \"string\",\n \"title\": \"Saved Connection\",\n \"description\": \"The ID of a saved database connection (for postgres source).\"\n },\n \"table_name\": {\n \"type\": \"string\",\n \"title\": \"Table Name\",\n \"description\": \"Target table name in PostgreSQL.\"\n },\n \"table_schema\": {\n \"type\": \"string\",\n \"title\": \"Table Schema\",\n \"description\": \"Target table schema in PostgreSQL.\",\n \"default\": \"public\"\n },\n \"excel_path\": {\n \"type\": \"string\",\n \"title\": \"Excel Path\",\n \"description\": \"Path to the Excel file (for excel source).\"\n }\n },\n \"required\": [\"env\", \"dataset_id\", \"source\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset mapping logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Mapping parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary.\n # @POST: Updates the dataset in Superset.\n # @RETURN: Dict[str, Any] - Execution status.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n source = params.get(\"source\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n superset_log = log.with_source(\"superset_api\") if context else log\n db_log = log.with_source(\"postgres\") if context else log\n \n if not env_name or dataset_id is None or not source:\n log.error(\"Missing required parameters: env, dataset_id, source\")\n raise ValueError(\"Missing required parameters: env, dataset_id, source\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n postgres_config = None\n if source == \"postgres\":\n connection_id = params.get(\"connection_id\")\n if not connection_id:\n log.error(\"connection_id is required for postgres source.\")\n raise ValueError(\"connection_id is required for postgres source.\")\n \n # Load connection from DB\n db = SessionLocal()\n try:\n conn_config = db.query(ConnectionConfig).filter(ConnectionConfig.id == connection_id).first()\n if not conn_config:\n db_log.error(f\"Connection {connection_id} not found.\")\n raise ValueError(f\"Connection {connection_id} not found.\")\n \n postgres_config = {\n 'dbname': conn_config.database,\n 'user': conn_config.username,\n 'password': conn_config.password,\n 'host': conn_config.host,\n 'port': str(conn_config.port) if conn_config.port else '5432'\n }\n db_log.debug(f\"Loaded connection config for {conn_config.host}:{conn_config.port}/{conn_config.database}\")\n finally:\n db.close()\n\n log.info(f\"Starting mapping for dataset {dataset_id} in {env_name}\")\n \n mapper = DatasetMapper()\n \n try:\n mapper.run_mapping(\n superset_client=client,\n dataset_id=dataset_id,\n source=source,\n postgres_config=postgres_config,\n excel_path=params.get(\"excel_path\"),\n table_name=params.get(\"table_name\"),\n table_schema=params.get(\"table_schema\") or \"public\"\n )\n superset_log.info(f\"Mapping completed for dataset {dataset_id}\")\n return {\"status\": \"success\", \"dataset_id\": dataset_id}\n except Exception as e:\n log.error(f\"Mapping failed: {e}\")\n raise\n # [/DEF:execute:Function]\n\n# [/DEF:MapperPlugin:Class]\n" }, @@ -63039,7 +65599,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set]", "INVARIANT": "Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution.", "LAYER": "App", @@ -63090,17 +65650,21 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'App' is not in allowed enum", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "App" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -63115,6 +65679,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -63159,17 +65724,12 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Plugins' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Plugins" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -63193,6 +65753,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -63214,7 +65775,17 @@ "PURPOSE": "Plugin for searching text patterns in Superset datasets." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SearchPlugin:Class]\n# @PURPOSE: Plugin for searching text patterns in Superset datasets.\nclass SearchPlugin(PluginBase):\n \"\"\"\n Plugin for searching text patterns in Superset datasets.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"search-datasets\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"search-datasets\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Search Datasets\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Search for text patterns across all datasets in a specific environment.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the search plugin.\n # @RETURN: str - \"/tools/search\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/search\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the search plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to search in (e.g., 'dev', 'prod').\"\n },\n \"query\": {\n \"type\": \"string\",\n \"title\": \"Search Query (Regex)\",\n \"description\": \"The regex pattern to search for.\"\n }\n },\n \"required\": [\"env\", \"query\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset search logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Search parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env' and 'query'.\n # @POST: Returns a dictionary with count and results list.\n # @RETURN: Dict[str, Any] - Search results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"SearchPlugin.execute\", f\"params={params}\"):\n env_name = params.get(\"env\")\n search_query = params.get(\"query\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n log.with_source(\"superset_api\") if context else log\n search_log = log.with_source(\"search\") if context else log\n \n if not env_name or not search_query:\n log.error(\"Missing required parameters: env, query\")\n raise ValueError(\"Missing required parameters: env, query\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n log.info(f\"Searching for pattern: '{search_query}' in environment: {env_name}\")\n \n try:\n # Ported logic from search_script.py\n _, datasets = client.get_datasets(query={\"columns\": [\"id\", \"table_name\", \"sql\", \"database\", \"columns\"]})\n\n if not datasets:\n search_log.warning(\"No datasets found.\")\n return {\"count\": 0, \"results\": []}\n\n pattern = re.compile(search_query, re.IGNORECASE)\n results = []\n \n for dataset in datasets:\n dataset_id = dataset.get('id')\n dataset_name = dataset.get('table_name', 'Unknown')\n if not dataset_id:\n continue\n\n for field, value in dataset.items():\n value_str = str(value)\n if pattern.search(value_str):\n match_obj = pattern.search(value_str)\n results.append({\n \"dataset_id\": dataset_id,\n \"dataset_name\": dataset_name,\n \"field\": field,\n \"match_context\": self._get_context(value_str, match_obj.group() if match_obj else \"\"),\n \"full_value\": value_str\n })\n\n search_log.info(f\"Found matches in {len(results)} locations.\")\n return {\n \"count\": len(results),\n \"results\": results\n }\n\n except re.error as e:\n search_log.error(f\"Invalid regex pattern: {e}\")\n raise ValueError(f\"Invalid regex pattern: {e}\")\n except Exception as e:\n log.error(f\"Error during search: {e}\")\n raise\n # [/DEF:execute:Function]\n\n # [DEF:_get_context:Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n \n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n \n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n \n return text[:100] + \"...\" if len(text) > 100 else text\n # [/DEF:_get_context:Function]\n\n# [/DEF:SearchPlugin:Class]\n" }, @@ -63259,6 +65830,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -63319,17 +65899,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'App' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "App" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -63353,6 +65928,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -63394,6 +65970,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -63440,6 +66025,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -63484,6 +66078,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -63539,6 +66142,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -63599,6 +66211,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -63633,7 +66254,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -63675,7 +66298,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -63701,7 +66326,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.", "SEMANTICS": [ "tests", @@ -63741,15 +66366,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } } ], "anchor_syntax": "def", @@ -63764,11 +66380,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Helper to create inline TranslationJob records." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_FakeJob:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Helper to create inline TranslationJob records.\nclass _FakeJob:\n pass\n# [/DEF:_FakeJob:Class]\n" }, @@ -64181,7 +66807,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Test", "PURPOSE": "Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.", "SEMANTICS": [ @@ -64215,7 +66841,17 @@ "PURPOSE": "Create a mock TranslationJob with test config." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_make_mock_job:Function]\n# @PURPOSE: Create a mock TranslationJob with test config.\ndef _make_mock_job(**overrides):\n job = MagicMock(spec=TranslationJob)\n job.id = overrides.get(\"id\", \"job-123\")\n job.name = overrides.get(\"name\", \"Test Job\")\n job.source_dialect = overrides.get(\"source_dialect\", \"postgresql\")\n job.target_dialect = overrides.get(\"target_dialect\", \"clickhouse\")\n job.source_datasource_id = overrides.get(\"source_datasource_id\", \"42\")\n job.source_table = overrides.get(\"source_table\", \"source_table\")\n job.translation_column = overrides.get(\"translation_column\", \"title\")\n job.context_columns = overrides.get(\"context_columns\", [\"category\", \"description\"])\n job.target_language = overrides.get(\"target_language\", \"ru\")\n job.provider_id = overrides.get(\"provider_id\", \"provider-1\")\n job.batch_size = overrides.get(\"batch_size\", 50)\n job.upsert_strategy = overrides.get(\"upsert_strategy\", \"MERGE\")\n job.status = overrides.get(\"status\", \"DRAFT\")\n return job\n# [/DEF:_make_mock_job:Function]\n" }, @@ -64231,7 +66867,17 @@ "PURPOSE": "Create a mock LLMProvider." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_make_mock_provider:Function]\n# @PURPOSE: Create a mock LLMProvider.\ndef _make_mock_provider(**overrides):\n provider = MagicMock()\n provider.id = overrides.get(\"id\", \"provider-1\")\n provider.provider_type = overrides.get(\"provider_type\", \"openai\")\n provider.base_url = overrides.get(\"base_url\", \"https://api.openai.com/v1\")\n provider.default_model = overrides.get(\"default_model\", \"gpt-4o-mini\")\n provider.api_key = \"encrypted-key-123\"\n return provider\n# [/DEF:_make_mock_provider:Function]\n" }, @@ -64247,7 +66893,17 @@ "PURPOSE": "Test suite for TranslationPreview service." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:TestTranslationPreview:Class]\n# @PURPOSE: Test suite for TranslationPreview service.\nclass TestTranslationPreview:\n\n # [DEF:test_preview_valid_job:Function]\n # @PURPOSE: Verify preview creates session and records successfully.\n def test_preview_valid_job(self):\n \"\"\"Preview with valid job should create session and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock job query\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n # Mock environments\n env = MagicMock()\n env.id = \"postgresql\"\n env.url = \"https://superset.example.com\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = True\n env.timeout = 30\n config_manager.get_environments.return_value = [env]\n\n # Mock SupersetClient\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n\n # Mock dataset_detail\n mock_client.get_dataset_detail.return_value = {\n \"table_name\": \"test_table\",\n \"columns\": [\n {\"column_name\": \"title\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"category\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"description\", \"type\": \"TEXT\"},\n ],\n }\n\n # Mock build_dataset_preview_query_context\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"row_limit\": 10,\n }],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n\n # Mock network request to Superset\n mock_client.network.request.return_value = {\n \"result\": [{\n \"status\": \"success\",\n \"data\": [\n {\"title\": \"Hello World\", \"category\": \"greeting\", \"description\": \"A friendly message\"},\n {\"title\": \"Goodbye\", \"category\": \"farewell\", \"description\": \"A parting message\"},\n ],\n }]\n }\n\n # Mock LLM provider\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider = _make_mock_provider()\n mock_provider_svc.get_provider.return_value = mock_provider\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n # Mock _call_openai_compatible to return structured JSON\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет мир\"},\n {\"row_id\": \"1\", \"translation\": \"Прощай\"},\n ]\n })\n\n # Mock DictionaryManager.filter_for_batch\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = []\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=10)\n\n # Verify result structure\n assert result[\"job_id\"] == \"job-123\"\n assert result[\"status\"] == \"ACTIVE\"\n assert len(result[\"records\"]) == 2\n assert result[\"records\"][0][\"target_sql\"] == \"Привет мир\"\n assert result[\"records\"][1][\"target_sql\"] == \"Прощай\"\n\n # Verify cost estimate\n assert result[\"cost_estimate\"][\"sample_size\"] == 2\n assert result[\"cost_estimate\"][\"sample_total_tokens\"] > 0\n assert result[\"cost_estimate\"][\"sample_cost\"] >= 0\n\n # Verify config and dict hashes\n assert result[\"config_hash\"] is not None\n assert result[\"dict_snapshot_hash\"] is not None\n\n # Verify session was added to DB\n assert db.add.called\n assert db.commit.called\n # [/DEF:test_preview_valid_job:Function]\n\n # [DEF:test_preview_with_dictionary:Function]\n # @PURPOSE: Verify glossary entries from dictionary are included in LLM prompt.\n def test_preview_with_dictionary(self):\n \"\"\"Preview should include dictionary glossary in the prompt sent to LLM.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n env = MagicMock()\n env.id = \"postgresql\"\n config_manager.get_environments.return_value = [env]\n\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n mock_client.get_dataset_detail.return_value = {\"table_name\": \"test\", \"columns\": []}\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\"columns\": [], \"metrics\": [\"count\"], \"row_limit\": 10}],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n mock_client.network.request.return_value = {\n \"result\": [{\"status\": \"success\", \"data\": [{\"title\": \"Hello World\", \"category\": \"greeting\"}]}]\n }\n\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider_svc.get_provider.return_value = _make_mock_provider()\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\"rows\": [{\"row_id\": \"0\", \"translation\": \"Привет мир\"}]})\n\n # Mock dictionary filter to return glossary entries\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = [\n {\n \"source_term\": \"Hello\",\n \"target_term\": \"Здравствуйте\",\n \"dictionary_id\": \"dict-1\",\n \"dictionary_name\": \"Test Dict\",\n \"context_notes\": \"Formal greeting\",\n }\n ]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=5)\n\n # Verify LLM was called\n mock_llm.assert_called_once()\n assert len(result[\"records\"]) == 1\n # [/DEF:test_preview_with_dictionary:Function]\n\n # [DEF:test_preview_row_approve:Function]\n # @PURPOSE: Verify approving a preview row changes its status.\n def test_preview_row_approve(self):\n \"\"\"Approve action should set record status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock active session\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_at = datetime.now(timezone.utc)\n\n # Mock record\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test translation\"\n record.feedback = None\n\n # Setup: db.query().filter().order_by().first() -> session (session lookup)\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Setup: db.query().filter().first() -> record (record lookup)\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"approve\",\n )\n\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_row_approve:Function]\n\n # [DEF:test_preview_row_reject:Function]\n # @PURPOSE: Verify rejecting a preview row changes its status.\n def test_preview_row_reject(self):\n \"\"\"Reject action should set record status to REJECTED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"reject\",\n )\n\n assert record.status == \"REJECTED\"\n assert result[\"status\"] == \"REJECTED\"\n # [/DEF:test_preview_row_reject:Function]\n\n # [DEF:test_preview_row_edit:Function]\n # @PURPOSE: Verify editing a preview row updates its translation and status.\n def test_preview_row_edit(self):\n \"\"\"Edit action should update translation and set status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"old translation\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"edit\",\n translation=\"edited translation\",\n )\n\n assert record.target_sql == \"edited translation\"\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert result[\"target_sql\"] == \"edited translation\"\n # [/DEF:test_preview_row_edit:Function]\n\n # [DEF:test_preview_row_invalid_action:Function]\n # @PURPOSE: Verify invalid action raises ValueError.\n def test_preview_row_invalid_action(self):\n \"\"\"Invalid action should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"Invalid action\"):\n preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"invalid_action\",\n )\n # [/DEF:test_preview_row_invalid_action:Function]\n\n # [DEF:test_preview_accept_session:Function]\n # @PURPOSE: Verify accepting a preview session gates execution.\n def test_preview_accept_session(self):\n \"\"\"Accept should set session status to APPLIED and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_by = \"test-user\"\n session.created_at = datetime.now(timezone.utc)\n session.expires_at = datetime.now(timezone.utc) + timedelta(hours=24)\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.source_sql = \"Hello\"\n record.target_sql = \"Привет\"\n record.status = \"APPROVED\"\n record.feedback = None\n\n # Session lookup\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Records lookup\n db.query.return_value.filter.return_value.all.return_value = [record]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.accept_preview_session(job_id=\"job-123\")\n\n assert session.status == \"APPLIED\"\n assert result[\"status\"] == \"APPLIED\"\n assert len(result[\"records\"]) == 1\n assert result[\"records\"][0][\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_accept_session:Function]\n\n # [DEF:test_preview_no_active_session:Function]\n # @PURPOSE: Verify accept raises error when no active session.\n def test_preview_no_active_session(self):\n \"\"\"Accept without active session should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"No active preview session\"):\n preview_service.accept_preview_session(job_id=\"job-123\")\n # [/DEF:test_preview_no_active_session:Function]\n\n # [DEF:test_cost_estimation:Function]\n # @PURPOSE: Verify token and cost estimation methods.\n def test_cost_estimation(self):\n \"\"\"Token and cost estimation should return reasonable values.\"\"\"\n prompt = \"Translate this text from English to Russian. \" * 100\n tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n assert tokens > 0\n assert tokens < len(prompt) # tokens typically fewer than chars\n\n output_tokens = TokenEstimator.estimate_output_tokens(10)\n assert output_tokens == 500\n\n cost = TokenEstimator.estimate_cost(1000, 0.002)\n assert cost == 0.002\n\n cost_default = TokenEstimator.estimate_cost(1000)\n assert cost_default == 0.002\n # [/DEF:test_cost_estimation:Function]\n\n # [DEF:test_preview_parse_llm_response:Function]\n # @PURPOSE: Verify LLM JSON response parsing.\n def test_preview_parse_llm_response(self):\n \"\"\"Parse LLM response should extract translations keyed by row_id.\"\"\"\n response = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет\"},\n {\"row_id\": \"1\", \"translation\": \"Мир\"},\n ]\n })\n result = TranslationPreview._parse_llm_response(response, 2)\n assert result == {\"0\": \"Привет\", \"1\": \"Мир\"}\n # [/DEF:test_preview_parse_llm_response:Function]\n\n # [DEF:test_preview_parse_llm_response_with_code_block:Function]\n # @PURPOSE: Verify LLM response parsing handles markdown code blocks.\n def test_preview_parse_llm_response_with_code_block(self):\n \"\"\"Parse LLM response should handle markdown code block wrapping.\"\"\"\n response = \"```json\\n{\\n \\\"rows\\\": [\\n {\\\"row_id\\\": \\\"0\\\", \\\"translation\\\": \\\"Test\\\"}\\n ]\\n}\\n```\"\n result = TranslationPreview._parse_llm_response(response, 1)\n assert result == {\"0\": \"Test\"}\n # [/DEF:test_preview_parse_llm_response_with_code_block:Function]\n\n # [DEF:test_preview_compute_config_hash:Function]\n # @PURPOSE: Verify config hash computation is deterministic.\n def test_preview_compute_config_hash(self):\n \"\"\"Config hash should be deterministic and non-empty.\"\"\"\n job = _make_mock_job()\n hash1 = TranslationPreview._compute_config_hash(job)\n hash2 = TranslationPreview._compute_config_hash(job)\n assert hash1 == hash2\n assert len(hash1) == 16\n # [/DEF:test_preview_compute_config_hash:Function]\n\n # [DEF:test_preview_missing_datasource:Function]\n # @PURPOSE: Verify error when job has no datasource configured.\n def test_preview_missing_datasource(self):\n \"\"\"Preview should fail if job has no datasource.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(source_datasource_id=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"source datasource\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_datasource:Function]\n\n # [DEF:test_preview_missing_translation_column:Function]\n # @PURPOSE: Verify error when job has no translation column.\n def test_preview_missing_translation_column(self):\n \"\"\"Preview should fail if job has no translation column.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(translation_column=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"translation column\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_translation_column:Function]\n\n\n# [/DEF:TestTranslationPreview:Class]\n" }, @@ -64487,26 +67143,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryUtils [C:1] [TYPE Module]\n# #endregion DictionaryUtils\n" }, @@ -64519,27 +67156,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Normalize a term for case-insensitive unique constraint lookup using NFC normalization.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Normalize a term for case-insensitive unique constraint lookup using NFC normalization." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _normalize_term [C:2] [TYPE Function] [SEMANTICS dictionary,normalize]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup using NFC normalization.\ndef _normalize_term(term: str) -> str:\n \"\"\"Normalize a term by NFC, lowercasing, and removing extra whitespace.\"\"\"\n if not term:\n return \"\"\n term = unicodedata.normalize('NFC', term)\n return \" \".join(term.lower().split())\n# #endregion _normalize_term\n" }, @@ -64555,17 +67176,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _detect_delimiter [C:1] [TYPE Function] [SEMANTICS dictionary,delimiter]\ndef _detect_delimiter(header_line: str) -> str:\n \"\"\"Detect delimiter by counting tabs vs commas in the first line.\"\"\"\n if not header_line:\n return \",\"\n tab_count = header_line.count(\"\\t\")\n comma_count = header_line.count(\",\")\n return \"\\t\" if tab_count > comma_count else \",\"\n# #endregion _detect_delimiter\n" }, @@ -64578,11 +67189,11 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]", "INVARIANT": "UniqueConstraint(dictionary_id, source_term_normalized) prohibits duplicate entries within a dictionary.", "LAYER": "Domain", + "PURPOSE": "Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.", "RATIONALE": "C5 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion and unique constraint on normalized source term.", "REJECTED": "\"Keep both\" as conflict option — UniqueConstraint(dictionary_id, source_term_normalized) prohibits variants; only overwrite/keep existing." }, @@ -64613,12 +67224,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "POST", @@ -64637,15 +67242,6 @@ "contract_type": "Module" } }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -64668,26 +67264,29 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Manages terminology dictionaries and their entries with referential integrity, import, and batch filtering.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]", "INVARIANT": "UniqueConstraint(dictionary_id, source_term_normalized) enforces no duplicates within a dictionary.", "POST": "Dictionary and entry mutations are persisted with conflict detection.", "PRE": "Database session is open and valid.", + "PURPOSE": "Manages terminology dictionaries and their entries with referential integrity, import, and batch filtering.", "SIDE_EFFECT": "Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C5", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", "detail": { "actual_complexity": 5, "contract_type": "Class" @@ -64715,8 +67314,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Create a new terminology dictionary with name, source_dialect, and target_dialect.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Create a new terminology dictionary with name, source_dialect, and target_dialect." }, "relations": [ { @@ -64726,23 +67325,7 @@ "target_ref": "[TerminologyDictionary]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.create_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,create]\n # @BRIEF Create a new terminology dictionary with name, source_dialect, and target_dialect.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def create_dictionary(\n db: Session, name: str, source_dialect: str, target_dialect: str,\n created_by: Optional[str] = None, description: Optional[str] = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.create_dictionary\"):\n log.reason(\"Creating dictionary\", payload={\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name,\n description=description,\n source_dialect=source_dialect,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary created\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.create_dictionary\n" }, @@ -64755,8 +67338,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Update an existing terminology dictionary's metadata fields.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Update an existing terminology dictionary's metadata fields." }, "relations": [ { @@ -64766,23 +67349,7 @@ "target_ref": "[TerminologyDictionary]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.update_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,update]\n # @BRIEF Update an existing terminology dictionary's metadata fields.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: Optional[str] = None,\n description: Optional[str] = None, source_dialect: Optional[str] = None,\n target_dialect: Optional[str] = None, is_active: Optional[bool] = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Updating dictionary\", payload={\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.update_dictionary\n" }, @@ -64795,29 +67362,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).", "COMPLEXITY": 4, "POST": "Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.", "PRE": "dict_id exists.", + "PURPOSE": "Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).", "SIDE_EFFECT": "Deletes TerminologyDictionary row and all DictionaryEntry rows; checks job attachments first." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -64840,27 +67392,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Get a single dictionary by ID with entry count.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Get a single dictionary by ID with entry count." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.get_dictionary [C:2] [TYPE Function] [SEMANTICS dictionary,get]\n # @BRIEF Get a single dictionary by ID with entry count.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # #endregion DictionaryManager.get_dictionary\n" }, @@ -64873,27 +67409,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "List dictionaries with pagination and total count.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "List dictionaries with pagination and total count." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.list_dictionaries [C:2] [TYPE Function] [SEMANTICS dictionary,list]\n # @BRIEF List dictionaries with pagination and total count.\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> Tuple[List[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # #endregion DictionaryManager.list_dictionaries\n" }, @@ -64906,8 +67426,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary." }, "relations": [ { @@ -64917,23 +67437,7 @@ "target_ref": "[DictionaryEntry]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.add_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,add]\n # @BRIEF Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n log.reason(\"Adding dictionary entry\", payload={\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.add_entry\n" }, @@ -64946,8 +67450,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Edit an existing dictionary entry with duplicate-aware normalization check.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Edit an existing dictionary entry with duplicate-aware normalization check." }, "relations": [ { @@ -64957,23 +67461,7 @@ "target_ref": "[DictionaryEntry]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.edit_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,edit]\n # @BRIEF Edit an existing dictionary entry with duplicate-aware normalization check.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: Optional[str] = None,\n target_term: Optional[str] = None, context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n\n log.reason(\"Editing dictionary entry\", payload={\"entry_id\": entry_id})\n if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.edit_entry\n" }, @@ -64986,27 +67474,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Delete a single dictionary entry.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Delete a single dictionary entry." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.delete_entry [C:2] [TYPE Function] [SEMANTICS dictionary,entry,delete]\n # @BRIEF Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n log.reason(\"Deleting dictionary entry\", payload={\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n # #endregion DictionaryManager.delete_entry\n" }, @@ -65019,27 +67491,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Delete all entries for a dictionary.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Delete all entries for a dictionary." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.clear_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,clear]\n # @BRIEF Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryManager.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Clearing all entries\", payload={\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n log.reflect(\"Entries cleared\", payload={\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # #endregion DictionaryManager.clear_entries\n" }, @@ -65052,27 +67508,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "List entries for a dictionary with pagination.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "List entries for a dictionary with pagination." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region DictionaryManager.list_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,list]\n # @BRIEF List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> Tuple[List[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar()\n or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # #endregion DictionaryManager.list_entries\n" }, @@ -65085,29 +67525,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).", "COMPLEXITY": 4, "POST": "Entries are created/updated/skipped per conflict mode. Returns result summary with preview support.", "PRE": "content is valid CSV or TSV. dict_id exists.", + "PURPOSE": "Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).", "SIDE_EFFECT": "Batch-inserts or updates DictionaryEntry rows." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65130,29 +67555,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.", "COMPLEXITY": 4, "POST": "Returns list of matched entries with match info, sorted by dictionary link priority.", "PRE": "job_id exists and source_texts is a list of strings.", + "PURPOSE": "Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.", "SIDE_EFFECT": "Queries TranslationJobDictionary, TerminologyDictionary, and DictionaryEntry tables." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65175,29 +67585,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Submit a term correction from a run result with conflict detection and origin tracking.", "COMPLEXITY": 4, "POST": "Entry created or updated; origin tracking populated. Returns action + conflict info.", "PRE": "source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.", + "PURPOSE": "Submit a term correction from a run result with conflict detection and origin tracking.", "SIDE_EFFECT": "Creates/updates DictionaryEntry row." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65220,29 +67615,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Submit multiple term corrections atomically — all succeed or all fail on conflict.", "COMPLEXITY": 4, "POST": "All corrections applied or none applied with conflict list (atomic).", "PRE": "corrections list is non-empty. dict_id exists.", + "PURPOSE": "Submit multiple term corrections atomically — all succeed or all fail on conflict.", "SIDE_EFFECT": "Creates/updates DictionaryEntry rows; commits once." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65265,13 +67645,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Structured event logging for translation operations with terminal event invariant enforcement.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]", "INVARIANT": "Exactly one run_started + exactly one terminal event per non-null run_id.", "LAYER": "Domain", "POST": "Events are persisted immutably; terminal events enforce exactly-one invariant per run.", "PRE": "Database session is open and valid.", + "PURPOSE": "Structured event logging for translation operations with terminal event invariant enforcement.", "RATIONALE": "Immutable event log with nullable run_id allows job-level events (no run context) and run-level events.", "REJECTED": "Separate event table per entity — single TranslationEvent table with nullable run_id is simpler for audit.", "SIDE_EFFECT": "Writes TranslationEvent rows; prunes expired events with MetricSnapshot before deletion." @@ -65290,23 +67670,7 @@ "target_ref": "[MetricSnapshot]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationEventLog [C:5] [TYPE Module] [SEMANTICS translate,events,audit,logging]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @PRE Database session is open and valid.\n# @POST Events are persisted immutably; terminal events enforce exactly-one invariant per run.\n# @SIDE_EFFECT Writes TranslationEvent rows; prunes expired events with MetricSnapshot before deletion.\n# @DATA_CONTRACT Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]\n# @INVARIANT Exactly one run_started + exactly one terminal event per non-null run_id.\n# @RATIONALE Immutable event log with nullable run_id allows job-level events (no run context) and run-level events.\n# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n# @REJECTED Separate event table per entity — single TranslationEvent table with nullable run_id is simpler for audit.\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\nfrom datetime import datetime, timezone, timedelta\nimport uuid\n\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import belief_scope\nfrom ...models.translate import TranslationEvent, MetricSnapshot\n\nlog = MarkerLogger(\"EventLog\")\n\n# Terminal events per run — exactly one of these must exist for a completed run.\nTERMINAL_EVENT_TYPES = {\"RUN_COMPLETED\", \"RUN_FAILED\", \"RUN_CANCELLED\"}\n# All valid event types for validation.\nVALID_EVENT_TYPES = TERMINAL_EVENT_TYPES | {\n \"JOB_CREATED\", \"JOB_UPDATED\", \"JOB_DELETED\",\n \"RUN_STARTED\", \"RUN_RETRYING\", \"RUN_RETRY_INSERT\",\n \"BATCH_STARTED\", \"BATCH_COMPLETED\", \"BATCH_FAILED\", \"BATCH_RETRYING\",\n \"TRANSLATION_PHASE_STARTED\", \"TRANSLATION_PHASE_COMPLETED\",\n \"INSERT_PHASE_STARTED\", \"INSERT_PHASE_COMPLETED\",\n \"PREVIEW_CREATED\", \"PREVIEW_ACCEPTED\", \"PREVIEW_DISCARDED\",\n \"SCHEDULE_CREATED\", \"SCHEDULE_UPDATED\", \"SCHEDULE_DELETED\",\n \"METRICS_SNAPSHOT_CREATED\",\n}\n\nDEFAULT_RETENTION_DAYS = 90\n\n\n# #region TranslationEventLog [C:5] [TYPE Class] [SEMANTICS translate,events,log,invariants]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement and pruning.\n# @PRE Database session is available.\n# @POST Events are written immutably; terminal events enforced per run; expired events pruned with MetricSnapshot.\n# @SIDE_EFFECT Writes TranslationEvent rows; prunes expired events.\n# @DATA_CONTRACT Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]\n# @INVARIANT Exactly one run_started + exactly one terminal event per non-null run_id.\nclass TranslationEventLog:\n\n def __init__(self, db: Session):\n self.db = db\n\n # #region _create_event_raw [C:3] [TYPE Function] [SEMANTICS translate,events,create]\n # @BRIEF Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def _create_event_raw(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job_id,\n run_id=run_id,\n event_type=event_type,\n event_data=payload or {},\n created_by=created_by,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(event)\n self.db.flush()\n return event\n # #endregion _create_event_raw\n\n # #region log_event [C:4] [TYPE Function] [SEMANTICS translate,events,log]\n # @BRIEF Write an immutable event. Enforces terminal event invariant and run_started ordering for non-null run_id.\n # @PRE event_type must be a known type. If run_id is not None, enforce terminal + start invariants.\n # @POST TranslationEvent row is created; invariants checked before write.\n # @SIDE_EFFECT DB write.\n def log_event(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n with belief_scope(\"TranslationEventLog.log_event\"):\n if event_type not in VALID_EVENT_TYPES:\n raise ValueError(\n f\"Invalid event_type '{event_type}'. \"\n f\"Must be one of: {', '.join(sorted(VALID_EVENT_TYPES))}\"\n )\n\n # Enforce terminal event invariant for non-null run_id\n if run_id is not None and event_type in TERMINAL_EVENT_TYPES:\n existing_terminal = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type.in_(TERMINAL_EVENT_TYPES),\n )\n .first()\n )\n if existing_terminal:\n existing_id = existing_terminal[0] if isinstance(existing_terminal, (tuple, list)) else str(existing_terminal.id)\n raise ValueError(\n f\"Run '{run_id}' already has a terminal event \"\n f\"({existing_id}). Cannot add another terminal event.\"\n )\n\n # Enforce run_started invariant — must exist before other run events\n if run_id is not None and event_type not in {\"RUN_STARTED\"} | TERMINAL_EVENT_TYPES:\n has_started = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type == \"RUN_STARTED\",\n )\n .first()\n )\n if not has_started:\n raise ValueError(\n f\"Cannot log '{event_type}' for run '{run_id}': \"\n f\"RUN_STARTED event must precede other run events.\"\n )\n\n event = self._create_event_raw(\n job_id=job_id,\n event_type=event_type,\n payload=payload,\n run_id=run_id,\n created_by=created_by,\n )\n log.reason(\"Event logged\", payload={\n \"event_id\": event.id,\n \"job_id\": job_id,\n \"run_id\": run_id,\n \"event_type\": event_type,\n })\n return event\n # #endregion log_event\n\n # #region query_events [C:2] [TYPE Function] [SEMANTICS translate,events,query]\n # @BRIEF Query events with optional filters (job_id, run_id, event_type) and pagination.\n def query_events(\n self,\n job_id: Optional[str] = None,\n run_id: Optional[str] = None,\n event_type: Optional[str] = None,\n limit: int = 100,\n offset: int = 0,\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationEventLog.query_events\"):\n query = self.db.query(TranslationEvent)\n\n if job_id:\n query = query.filter(TranslationEvent.job_id == job_id)\n if run_id:\n query = query.filter(TranslationEvent.run_id == run_id)\n if event_type:\n query = query.filter(TranslationEvent.event_type == event_type)\n\n events = (\n query.order_by(TranslationEvent.created_at.desc())\n .offset(offset)\n .limit(limit)\n .all()\n )\n\n return [\n {\n \"id\": e.id,\n \"job_id\": e.job_id,\n \"run_id\": e.run_id,\n \"event_type\": e.event_type,\n \"event_data\": e.event_data or {},\n \"created_by\": e.created_by,\n \"created_at\": e.created_at.isoformat() if e.created_at else None,\n }\n for e in events\n ]\n # #endregion query_events\n\n # #region prune_expired [C:4] [TYPE Function] [SEMANTICS translate,events,prune]\n # @BRIEF Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.\n # @PRE None.\n # @POST Expired events are deleted; MetricSnapshot is created before deletion in batch.\n # @SIDE_EFFECT Creates MetricSnapshot row; deletes TranslationEvent rows in batches.\n def prune_expired(\n self,\n retention_days: int = DEFAULT_RETENTION_DAYS,\n batch_size: int = 1000,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.prune_expired\"):\n cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)\n log.reason(\"Pruning expired events\", payload={\n \"cutoff\": cutoff.isoformat(),\n \"retention_days\": retention_days,\n })\n\n # Find events to prune\n expired_query = self.db.query(TranslationEvent).filter(\n TranslationEvent.created_at < cutoff\n )\n total_expired = expired_query.count()\n\n if total_expired == 0:\n log.reflect(\"No expired events to prune\", payload={})\n return {\"pruned\": 0, \"snapshot_id\": None}\n\n # Create MetricSnapshot before pruning\n snapshot = MetricSnapshot(\n id=str(uuid.uuid4()),\n job_id=\"_prune_aggregate_\",\n key_hash=f\"prune_{cutoff.timestamp():.0f}\",\n covers_events_before=cutoff,\n total_records=total_expired,\n snapshot_date=datetime.now(timezone.utc),\n )\n self.db.add(snapshot)\n self.db.flush()\n snapshot_id = snapshot.id\n\n # Delete in batches\n pruned = 0\n while True:\n batch_ids = (\n self.db.query(TranslationEvent.id)\n .filter(TranslationEvent.created_at < cutoff)\n .limit(batch_size)\n .all()\n )\n if not batch_ids:\n break\n ids = [row[0] for row in batch_ids]\n self.db.query(TranslationEvent).filter(\n TranslationEvent.id.in_(ids)\n ).delete(synchronize_session=False)\n self.db.flush()\n pruned += len(ids)\n log.reason(\"Pruned batch\", payload={\"batch_size\": len(ids), \"total_pruned\": pruned})\n\n self.db.commit()\n\n log.reflect(\"Pruning complete\", payload={\n \"pruned\": pruned,\n \"snapshot_id\": snapshot_id,\n })\n return {\"pruned\": pruned, \"snapshot_id\": snapshot_id}\n # #endregion prune_expired\n\n # #region get_run_event_summary [C:3] [TYPE Function] [SEMANTICS translate,events,summary]\n # @BRIEF Get a summary of events for a run, including invariant validity check.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_summary(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_summary\"):\n events = self.query_events(run_id=run_id)\n\n has_started = any(e[\"event_type\"] == \"RUN_STARTED\" for e in events)\n terminal_events = [e for e in events if e[\"event_type\"] in TERMINAL_EVENT_TYPES]\n\n return {\n \"run_id\": run_id,\n \"event_count\": len(events),\n \"has_run_started\": has_started,\n \"terminal_event_count\": len(terminal_events),\n \"terminal_events\": terminal_events,\n \"invariant_valid\": has_started and len(terminal_events) <= 1,\n \"events\": events,\n }\n # #endregion get_run_event_summary\n\n # #region get_run_event_invariants_lightweight [C:3] [TYPE Function] [SEMANTICS translate,events,invariants]\n # @BRIEF Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_invariants_lightweight(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_invariants_lightweight\"):\n # Lightweight query: only fetch event_type column, not event_data\n event_types = [\n row[0]\n for row in (\n self.db.query(TranslationEvent.event_type)\n .filter(TranslationEvent.run_id == run_id)\n .order_by(TranslationEvent.created_at.desc())\n .limit(100)\n .all()\n )\n ]\n\n has_run_started = \"RUN_STARTED\" in event_types\n terminal_count = sum(1 for et in event_types if et in TERMINAL_EVENT_TYPES)\n\n return {\n \"has_run_started\": has_run_started,\n \"terminal_event_count\": terminal_count,\n \"invariant_valid\": has_run_started and terminal_count <= 1,\n }\n # #endregion get_run_event_invariants_lightweight\n\n\n# #endregion TranslationEventLog\n# #endregion TranslationEventLog\n" }, @@ -65319,8 +67683,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves." }, "relations": [ { @@ -65330,23 +67694,7 @@ "target_ref": "[TranslationEvent]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region _create_event_raw [C:3] [TYPE Function] [SEMANTICS translate,events,create]\n # @BRIEF Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def _create_event_raw(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job_id,\n run_id=run_id,\n event_type=event_type,\n event_data=payload or {},\n created_by=created_by,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(event)\n self.db.flush()\n return event\n # #endregion _create_event_raw\n" }, @@ -65359,27 +67707,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Query events with optional filters (job_id, run_id, event_type) and pagination.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Query events with optional filters (job_id, run_id, event_type) and pagination." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region query_events [C:2] [TYPE Function] [SEMANTICS translate,events,query]\n # @BRIEF Query events with optional filters (job_id, run_id, event_type) and pagination.\n def query_events(\n self,\n job_id: Optional[str] = None,\n run_id: Optional[str] = None,\n event_type: Optional[str] = None,\n limit: int = 100,\n offset: int = 0,\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationEventLog.query_events\"):\n query = self.db.query(TranslationEvent)\n\n if job_id:\n query = query.filter(TranslationEvent.job_id == job_id)\n if run_id:\n query = query.filter(TranslationEvent.run_id == run_id)\n if event_type:\n query = query.filter(TranslationEvent.event_type == event_type)\n\n events = (\n query.order_by(TranslationEvent.created_at.desc())\n .offset(offset)\n .limit(limit)\n .all()\n )\n\n return [\n {\n \"id\": e.id,\n \"job_id\": e.job_id,\n \"run_id\": e.run_id,\n \"event_type\": e.event_type,\n \"event_data\": e.event_data or {},\n \"created_by\": e.created_by,\n \"created_at\": e.created_at.isoformat() if e.created_at else None,\n }\n for e in events\n ]\n # #endregion query_events\n" }, @@ -65392,29 +67724,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.", "COMPLEXITY": 4, "POST": "Expired events are deleted; MetricSnapshot is created before deletion in batch.", "PRE": "None.", + "PURPOSE": "Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.", "SIDE_EFFECT": "Creates MetricSnapshot row; deletes TranslationEvent rows in batches." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65437,8 +67754,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get a summary of events for a run, including invariant validity check.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get a summary of events for a run, including invariant validity check." }, "relations": [ { @@ -65448,23 +67765,7 @@ "target_ref": "[TranslationEvent]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region get_run_event_summary [C:3] [TYPE Function] [SEMANTICS translate,events,summary]\n # @BRIEF Get a summary of events for a run, including invariant validity check.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_summary(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_summary\"):\n events = self.query_events(run_id=run_id)\n\n has_started = any(e[\"event_type\"] == \"RUN_STARTED\" for e in events)\n terminal_events = [e for e in events if e[\"event_type\"] in TERMINAL_EVENT_TYPES]\n\n return {\n \"run_id\": run_id,\n \"event_count\": len(events),\n \"has_run_started\": has_started,\n \"terminal_event_count\": len(terminal_events),\n \"terminal_events\": terminal_events,\n \"invariant_valid\": has_started and len(terminal_events) <= 1,\n \"events\": events,\n }\n # #endregion get_run_event_summary\n" }, @@ -65477,8 +67778,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance." }, "relations": [ { @@ -65488,23 +67789,7 @@ "target_ref": "[TranslationEvent]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region get_run_event_invariants_lightweight [C:3] [TYPE Function] [SEMANTICS translate,events,invariants]\n # @BRIEF Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_invariants_lightweight(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_invariants_lightweight\"):\n # Lightweight query: only fetch event_type column, not event_data\n event_types = [\n row[0]\n for row in (\n self.db.query(TranslationEvent.event_type)\n .filter(TranslationEvent.run_id == run_id)\n .order_by(TranslationEvent.created_at.desc())\n .limit(100)\n .all()\n )\n ]\n\n has_run_started = \"RUN_STARTED\" in event_types\n terminal_count = sum(1 for et in event_types if et in TERMINAL_EVENT_TYPES)\n\n return {\n \"has_run_started\": has_run_started,\n \"terminal_event_count\": terminal_count,\n \"invariant_valid\": has_run_started and terminal_count <= 1,\n }\n # #endregion get_run_event_invariants_lightweight\n" }, @@ -65517,13 +67802,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[TranslationRun, job] -> Output[TranslationRun with batches and records]", "INVARIANT": "Batch processing with retry — independent batches allow partial recovery; MAX_RETRIES_PER_BATCH = 3.", "LAYER": "Domain", "POST": "TranslationBatch and TranslationRecord rows are created. Run status is updated.", "PRE": "Valid TranslationRun with job configuration. DB session is available.", + "PURPOSE": "Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.", "RATIONALE": "Batch processing with retry — independent batches allow partial recovery.", "REJECTED": "Single monolithic LLM call — would lose all progress on any failure.", "SIDE_EFFECT": "Calls LLM provider; creates DB rows; updates run statistics." @@ -65566,23 +67851,7 @@ "target_ref": "[TranslationPreview]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationExecutor [C:5] [TYPE Module] [SEMANTICS translate,executor,batch,llm]\n# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @PRE Valid TranslationRun with job configuration. DB session is available.\n# @POST TranslationBatch and TranslationRecord rows are created. Run status is updated.\n# @SIDE_EFFECT Calls LLM provider; creates DB rows; updates run statistics.\n# @DATA_CONTRACT Input[TranslationRun, job] -> Output[TranslationRun with batches and records]\n# @INVARIANT Batch processing with retry — independent batches allow partial recovery; MAX_RETRIES_PER_BATCH = 3.\n# @RATIONALE Batch processing with retry — independent batches allow partial recovery.\n# @REJECTED Single monolithic LLM call — would lose all progress on any failure.\n\nimport json\nimport time\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Set, Tuple, Callable\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n TranslationBatch,\n TranslationRecord,\n TranslationPreviewSession,\n TranslationPreviewRecord,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...core.superset_client import SupersetClient\nfrom .dictionary import DictionaryManager\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\n\nlog = MarkerLogger(\"TranslationExecutor\")\n\n# #region MAX_RETRIES_PER_BATCH [C:1] [TYPE Constant] [SEMANTICS translate,executor,retry]\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n\n\n# #region TranslationExecutor [C:5] [TYPE Class] [SEMANTICS translate,executor,batch]\n# @BRIEF Process translation batches: fetch source rows, filter dict, call LLM, persist results.\n# @PRE DB session and config manager available.\n# @POST Batches and records created with status tracking; run statistics updated.\n# @SIDE_EFFECT LLM API calls; DB writes.\n# @DATA_CONTRACT Input[TranslationRun] -> Output[TranslationRun with batches, records, stats]\n# @INVARIANT MAX_RETRIES_PER_BATCH limit enforced; partial batch success tracked via COMPLETED_WITH_ERRORS.\nclass TranslationExecutor:\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: Optional[str] = None,\n on_batch_progress: Optional[Callable[[str, int, int, int, int], None]] = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.on_batch_progress = on_batch_progress\n self._current_run_id: Optional[str] = None\n\n # #region execute_run [C:4] [TYPE Function] [SEMANTICS translate,run,execute]\n # @BRIEF Run full translation execution for a TranslationRun: fetch rows, batch, call LLM, persist.\n # @PRE run is in PENDING or RUNNING status with valid job config.\n # @POST Run is populated with batches and records; run status updated to COMPLETED/FAILED.\n # @SIDE_EFFECT LLM API calls; DB batch writes.\n def execute_run(\n self,\n run: TranslationRun,\n llm_progress_callback: Optional[Callable[[str, int, int, int], None]] = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationExecutor.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found for run '{run.id}'\")\n\n log.reason(\"Starting translation execution\", payload={\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"batch_size\": job.batch_size,\n \"full_translation\": full_translation,\n })\n\n # Mark run as RUNNING\n run.status = \"RUNNING\"\n run.started_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Fetch source rows\n if full_translation:\n # Full translation: fetch ALL rows from Superset dataset\n source_rows = self._fetch_all_rows_from_superset(job)\n else:\n # Preview-based: fetch rows from the accepted preview session\n source_rows = self._fetch_source_rows(job.id, run.id)\n if not source_rows:\n log.explore(\"No source rows to translate\", payload={\"run_id\": run.id}, error=\"Preview produced 0 source rows for translation\")\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n return run\n\n total_rows = len(source_rows)\n run.total_records = total_rows\n\n # Split into batches\n batch_size = job.batch_size or 50\n batches = [\n source_rows[i:i + batch_size]\n for i in range(0, total_rows, batch_size)\n ]\n\n log.reason(f\"Processing {len(batches)} batches\", payload={\n \"run_id\": run.id,\n \"total_rows\": total_rows,\n \"batch_size\": batch_size,\n })\n\n successful_records = 0\n failed_records = 0\n skipped_records = 0\n\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job,\n run_id=run.id,\n batch_index=batch_idx,\n batch_rows=batch_rows,\n )\n successful_records += batch_result[\"successful\"]\n failed_records += batch_result[\"failed\"]\n skipped_records += batch_result[\"skipped\"]\n\n # Update run stats incrementally\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n self.db.flush()\n\n if self.on_batch_progress:\n self.on_batch_progress(\n run.id, batch_idx + 1, len(batches),\n successful_records, total_rows,\n )\n\n # Update final run status\n if failed_records == 0 and skipped_records == 0:\n run.status = \"COMPLETED\"\n elif successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\" # Partial success\n\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n log.reflect(\"Translation execution complete\", payload={\n \"run_id\": run.id,\n \"status\": run.status,\n \"total\": total_rows,\n \"successful\": successful_records,\n \"failed\": failed_records,\n \"skipped\": skipped_records,\n })\n\n return run\n # #endregion execute_run\n\n # #region _fetch_source_rows [C:4] [TYPE Function] [SEMANTICS translate,source,rows]\n # @BRIEF Fetch source rows from the accepted preview session for this job.\n # @PRE job_id exists.\n # @POST Returns list of dicts with source data (row_index, source_text, approved_translation, etc).\n # @SIDE_EFFECT Queries preview session and records from DB.\n def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_source_rows\"):\n # Get the latest APPLIED preview session\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n log.explore(\"No accepted preview session found\", error=\"Preview session has no accepted rows\", payload={\"job_id\": job_id})\n return []\n\n # Fetch APPROVED or all records from the session\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]),\n )\n .all()\n )\n\n source_rows = []\n for rec in records:\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": rec.source_data or {},\n })\n\n log.reason(f\"Fetched {len(source_rows)} source rows from preview\", payload={\n \"run_id\": run_id,\n \"session_id\": session.id,\n })\n return source_rows\n # #endregion _fetch_source_rows\n\n # #region _fetch_all_rows_from_superset [C:4] [TYPE Function] [SEMANTICS translate,superset,rows]\n # @BRIEF Fetch ALL rows from the Superset dataset for full translation (paginated).\n # @PRE job has source_datasource_id and environment_id configured.\n # @POST Returns list of row dicts with row_index, source_text, source_data.\n # @SIDE_EFFECT Calls Superset chart data endpoint (paginated).\n def _fetch_all_rows_from_superset(self, job: TranslationJob) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_all_rows_from_superset\"):\n env_id = job.environment_id or job.source_dialect or \"\"\n environments = self.config_manager.get_environments()\n env_config = next(\n (e for e in environments if e.id == env_id),\n None,\n )\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n\n # Build query context (same as preview, but with large row_limit)\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id),\n dataset_record=dataset_detail,\n template_params={},\n effective_filters=[],\n )\n\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = 100000 # Superset max default\n queries[0].pop(\"result_type\", None)\n queries[0].pop(\"columns\", None)\n queries[0][\"metrics\"] = []\n queries[0][\"row_offset\"] = 0\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n all_rows: List[Dict[str, Any]] = []\n batch_size_fetch = 10000 # Fetch 10K rows per pagination request\n\n while True:\n # Set pagination for this batch\n if queries:\n queries[0][\"row_limit\"] = min(batch_size_fetch, 100000)\n queries[0][\"row_offset\"] = len(all_rows)\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"Chart data API failed during full fetch\", error=\"Chart data API error\",\n payload={\n \"offset\": len(all_rows),\n \"error\": str(e),\n })\n if not all_rows:\n raise ValueError(f\"Failed to fetch data from Superset: {e}\")\n break # Return what we have\n\n from .preview import TranslationPreview\n rows = TranslationPreview._extract_data_rows(response)\n if not rows:\n break # No more data\n\n all_rows.extend(rows)\n log.reason(f\"Fetched {len(rows)} rows (total: {len(all_rows)})\", payload={\n \"offset\": len(all_rows) - len(rows),\n })\n\n if len(rows) < batch_size_fetch:\n break # Last page\n\n # Convert to the format expected by _process_batch\n source_rows = []\n for idx, row in enumerate(all_rows):\n translation_value = str(row.get(job.translation_column, \"\") or \"\")\n context_values = {}\n if job.context_columns:\n for col in job.context_columns:\n context_values[col] = str(row.get(col, \"\") or \"\")\n # Also include target_key_cols values in context_data\n if job.target_key_cols:\n for col in job.target_key_cols:\n context_values[col] = str(row.get(col, \"\") or \"\")\n source_rows.append({\n \"row_index\": str(idx),\n \"source_text\": translation_value,\n \"source_data\": context_values,\n \"source_object_name\": f\"Row {idx + 1}\",\n \"approved_translation\": None,\n })\n\n log.reason(f\"Prepared {len(source_rows)} source rows for full translation\", payload={\n \"job_id\": job.id,\n })\n return source_rows\n # #endregion _fetch_all_rows_from_superset\n\n # #region _process_batch [C:4] [TYPE Function] [SEMANTICS translate,batch,process]\n # @BRIEF Process a single batch: filter dictionary, build prompt, call LLM, persist records.\n # @PRE job and batch_rows are valid.\n # @POST TranslationBatch and TranslationRecord rows are created.\n # @SIDE_EFFECT LLM API call; DB writes.\n def _process_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_index: int,\n batch_rows: List[Dict[str, Any]],\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._process_batch\"):\n batch_start = time.monotonic()\n\n # Create batch record\n batch = TranslationBatch(\n id=str(uuid.uuid4()),\n run_id=run_id,\n batch_index=batch_index,\n status=\"RUNNING\",\n total_records=len(batch_rows),\n started_at=datetime.now(timezone.utc),\n )\n self.db.add(batch)\n self.db.flush()\n batch_id = batch.id\n\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n # Extract source texts for dict filtering\n source_texts = [\n row.get(\"source_text\", \"\")\n for row in batch_rows\n if row.get(\"source_text\")\n ]\n\n # Filter dictionary entries\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, source_texts, job.id\n )\n\n # For each row, determine if we need LLM translation or can use approved translation\n rows_for_llm = []\n pre_translated = []\n\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_translated.append(row)\n else:\n rows_for_llm.append(row)\n\n # Handle pre-translated (approved) rows\n for row in pre_translated:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=row.get(\"approved_translation\"),\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n result[\"successful\"] += 1\n\n # Process rows needing LLM translation\n if rows_for_llm:\n llm_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=rows_for_llm,\n dict_matches=dict_matches,\n batch_id=batch_id,\n )\n result[\"successful\"] += llm_result[\"successful\"]\n result[\"failed\"] += llm_result[\"failed\"]\n result[\"skipped\"] += llm_result[\"skipped\"]\n result[\"retries\"] += llm_result.get(\"retries\", 0)\n\n # Update batch status\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(timezone.utc)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n batch_latency = int((time.monotonic() - batch_start) * 1000)\n log.reason(f\"Batch {batch_index} complete\", payload={\n \"batch_id\": batch_id,\n \"latency_ms\": batch_latency,\n **result,\n })\n\n return result\n # #endregion _process_batch\n\n # #region _call_llm_for_batch [C:4] [TYPE Function] [SEMANTICS translate,llm,batch]\n # @BRIEF Call LLM for a batch of rows requiring translation. Parse structured JSON response.\n # @PRE job has valid provider_id. batch_rows is non-empty.\n # @POST Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm_for_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_rows: List[Dict[str, Any]],\n dict_matches: List[Dict[str, Any]],\n batch_id: str,\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._call_llm_for_batch\"):\n # Build dictionary section\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for m in dict_matches:\n glossary_lines.append(\n f\"- '{m['source_term']}' -> '{m['target_term']}'\"\n f\"{' (' + m['context_notes'] + ')' if m.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # Build rows JSON for LLM\n rows_json = json.dumps([\n {\n \"row_id\": str(row.get(\"row_index\", idx)),\n \"text\": row.get(\"source_text\", \"\"),\n }\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n\n # Build prompt\n prompt = render_prompt(\n DEFAULT_EXECUTION_PROMPT_TEMPLATE,\n {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": job.target_language or job.target_dialect or \"en\",\n \"source_dialect\": job.source_dialect or \"\",\n \"target_dialect\": job.target_dialect or \"\",\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(len(batch_rows)),\n },\n )\n\n # Call LLM with retry\n llm_response = None\n last_error = None\n retries = 0\n\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response = self._call_llm(job, prompt)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n log.explore(\"LLM call failed\", error=\"LLM call failed, retrying\",\n payload={\n \"batch_id\": batch_id,\n \"error\": last_error,\n \"attempt\": attempt,\n })\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n log.explore(\"LLM call exhausted retries\", error=\"LLM retries exhausted\",\n payload={\n \"batch_id\": batch_id,\n \"last_error\": last_error,\n })\n\n if llm_response is None:\n # All retries failed — mark all rows as failed\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n )\n self.db.add(record)\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n # Parse LLM response\n try:\n translations = self._parse_llm_response(llm_response, len(batch_rows))\n except ValueError as e:\n # Parse failure — mark all rows as SKIPPED\n log.explore(\"LLM response parse failed\", error=\"Failed to parse LLM JSON response\",\n payload={\n \"batch_id\": batch_id,\n \"error\": str(e),\n \"response_preview\": llm_response[:500] if llm_response else \"\",\n })\n skipped = len(batch_rows)\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {e}\",\n )\n self.db.add(record)\n return {\n \"successful\": 0,\n \"failed\": 0,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n\n successful = 0\n failed = 0\n skipped = 0\n\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n translation = translations.get(row_id)\n\n if translation is None:\n # NULL translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"NULL translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n if translation.strip() == \"\":\n # Empty translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"Empty translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n successful += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=translation,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n return {\n \"successful\": successful,\n \"failed\": failed,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n # #endregion _call_llm_for_batch\n\n # #region _call_llm [C:4] [TYPE Function] [SEMANTICS translate,llm,call]\n # @BRIEF Call the configured LLM provider with the batch prompt, routing by provider type.\n # @PRE job has valid provider_id.\n # @POST Returns raw LLM response string.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm(self, job: TranslationJob, prompt: str) -> str:\n with belief_scope(\"TranslationExecutor._call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n\n if provider_type in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\"):\n return self._call_openai_compatible(\n base_url=provider.base_url,\n api_key=api_key,\n model=model,\n prompt=prompt,\n provider_type=provider_type,\n )\n else:\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n # #endregion _call_llm\n\n # #region _call_openai_compatible [C:4] [TYPE Function] [SEMANTICS translate,llm,openai]\n # @BRIEF Call OpenAI-compatible API for batch translation with retry and structured output support.\n # @PRE Valid API endpoint, key, model, and prompt.\n # @POST Returns response text.\n # @SIDE_EFFECT HTTP POST to LLM API.\n @staticmethod\n def _call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n ) -> str:\n with belief_scope(\"TranslationExecutor._call_openai_compatible\"):\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n payload = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": \"You are a database content translation assistant. Translate the provided text accurately, preserving data semantics.\"},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": 4096,\n }\n # Structured output (response_format) only for native OpenAI — upstream providers routed via\n # Kilo/OpenRouter may not support it (e.g. StepFun returns \"structured_outputs is not supported\")\n if provider_type in (\"openai\", \"openai_compatible\"):\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n log.reason(\n f\"LLM request model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if not response.ok:\n log.explore(\"LLM API error\", error=f\"LLM API returned status {response.status_code}\", payload={\n \"status_code\": response.status_code,\n \"model\": payload.get('model'),\n \"body\": response.text[:2000],\n })\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n raise ValueError(\"LLM returned no choices\")\n\n content = choices[0].get(\"message\", {}).get(\"content\", \"\")\n if not content:\n # Log full response for diagnostics\n finish_reason = choices[0].get(\"finish_reason\", \"unknown\")\n log.explore(\"LLM returned empty content\", error=\"Empty response from LLM\",\n payload={\n \"finish_reason\": finish_reason,\n \"model\": payload.get(\"model\"),\n \"prompt_len\": len(prompt),\n \"response_keys\": list(data.keys()),\n \"choices_count\": len(choices),\n })\n raise ValueError(\n f\"LLM returned empty content (finish_reason={finish_reason}, \"\n f\"model={payload.get('model')}, prompt_len={len(prompt)})\"\n )\n\n return content\n # #endregion _call_openai_compatible\n\n # #region _parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate,llm,parse]\n # @BRIEF Parse LLM JSON response into dict of row_id -> translation text.\n # @RELATION DEPENDS_ON -> [json]\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int) -> Dict[str, str]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n raise ValueError(\"LLM response was not valid JSON\")\n else:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: Dict[str, str] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n translation = item.get(\"translation\")\n if translation is None:\n # Skip NULL translations — they'll be handled by caller\n continue\n if row_id:\n translations[row_id] = str(translation)\n\n return translations\n # #endregion _parse_llm_response\n\n\n# #endregion TranslationExecutor\n# #endregion TranslationExecutor\n" }, @@ -65610,7 +67879,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -65636,29 +67907,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch source rows from the accepted preview session for this job.", "COMPLEXITY": 4, "POST": "Returns list of dicts with source data (row_index, source_text, approved_translation, etc).", "PRE": "job_id exists.", + "PURPOSE": "Fetch source rows from the accepted preview session for this job.", "SIDE_EFFECT": "Queries preview session and records from DB." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65681,29 +67937,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch ALL rows from the Superset dataset for full translation (paginated).", "COMPLEXITY": 4, "POST": "Returns list of row dicts with row_index, source_text, source_data.", "PRE": "job has source_datasource_id and environment_id configured.", + "PURPOSE": "Fetch ALL rows from the Superset dataset for full translation (paginated).", "SIDE_EFFECT": "Calls Superset chart data endpoint (paginated)." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65726,29 +67967,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Process a single batch: filter dictionary, build prompt, call LLM, persist records.", "COMPLEXITY": 4, "POST": "TranslationBatch and TranslationRecord rows are created.", "PRE": "job and batch_rows are valid.", + "PURPOSE": "Process a single batch: filter dictionary, build prompt, call LLM, persist records.", "SIDE_EFFECT": "LLM API call; DB writes." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65771,29 +67997,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Call LLM for a batch of rows requiring translation. Parse structured JSON response.", "COMPLEXITY": 4, "POST": "Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.", "PRE": "job has valid provider_id. batch_rows is non-empty.", + "PURPOSE": "Call LLM for a batch of rows requiring translation. Parse structured JSON response.", "SIDE_EFFECT": "HTTP call to LLM provider." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65816,29 +68027,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Call the configured LLM provider with the batch prompt, routing by provider type.", "COMPLEXITY": 4, "POST": "Returns raw LLM response string.", "PRE": "job has valid provider_id.", + "PURPOSE": "Call the configured LLM provider with the batch prompt, routing by provider type.", "SIDE_EFFECT": "HTTP call to LLM provider." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65861,29 +68057,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Call OpenAI-compatible API for batch translation with retry and structured output support.", "COMPLEXITY": 4, "POST": "Returns response text.", "PRE": "Valid API endpoint, key, model, and prompt.", + "PURPOSE": "Call OpenAI-compatible API for batch translation with retry and structured output support.", "SIDE_EFFECT": "HTTP POST to LLM API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -65906,8 +68087,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Parse LLM JSON response into dict of row_id -> translation text.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Parse LLM JSON response into dict of row_id -> translation text." }, "relations": [ { @@ -65917,23 +68098,7 @@ "target_ref": "[json]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region _parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate,llm,parse]\n # @BRIEF Parse LLM JSON response into dict of row_id -> translation text.\n # @RELATION DEPENDS_ON -> [json]\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int) -> Dict[str, str]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n raise ValueError(\"LLM response was not valid JSON\")\n else:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: Dict[str, str] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n translation = item.get(\"translation\")\n if translation is None:\n # Skip NULL translations — they'll be handled by caller\n continue\n if row_id:\n translations[row_id] = str(translation)\n\n return translations\n # #endregion _parse_llm_response\n" }, @@ -65946,9 +68111,9 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.", "COMPLEXITY": 3, - "LAYER": "Domain" + "LAYER": "Domain", + "PURPOSE": "Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting." }, "relations": [ { @@ -65976,23 +68141,7 @@ "target_ref": "[TranslationSchedule]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS translate,metrics,aggregation]\n# @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\n\nfrom ...core.logger import logger, belief_scope\nfrom ...models.translate import (\n TranslationEvent,\n MetricSnapshot,\n TranslationRun,\n TranslationSchedule,\n)\n\n\n# #region TranslationMetrics [C:3] [TYPE Class] [SEMANTICS translate,metrics]\n# @BRIEF Aggregate translation metrics from live events and MetricSnapshot.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @RELATION DEPENDS_ON -> [TranslationEvent]\nclass TranslationMetrics:\n\n def __init__(self, db: Session):\n self.db = db\n\n # #region get_job_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,job]\n # @BRIEF Get aggregated metrics for a specific job including run counts, record stats, and duration.\n def get_job_metrics(self, job_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationMetrics.get_job_metrics\"):\n # Run counts from TranslationRun\n run_counts = (\n self.db.query(\n TranslationRun.status,\n func.count(TranslationRun.id),\n )\n .filter(TranslationRun.job_id == job_id)\n .group_by(TranslationRun.status)\n .all()\n )\n\n total_runs = 0\n status_counts: Dict[str, int] = {}\n for status_val, count in run_counts:\n status_counts[status_val] = count\n total_runs += count\n\n # Aggregate record stats from runs\n record_stats = (\n self.db.query(\n func.coalesce(func.sum(TranslationRun.total_records), 0),\n func.coalesce(func.sum(TranslationRun.successful_records), 0),\n func.coalesce(func.sum(TranslationRun.failed_records), 0),\n func.coalesce(func.sum(TranslationRun.skipped_records), 0),\n )\n .filter(TranslationRun.job_id == job_id)\n .first()\n )\n total_records = record_stats[0] if record_stats else 0\n successful_records = record_stats[1] if record_stats else 0\n failed_records = record_stats[2] if record_stats else 0\n skipped_records = record_stats[3] if record_stats else 0\n\n # Average duration from runs\n avg_duration = (\n self.db.query(\n func.avg(\n func.extract('epoch', TranslationRun.completed_at - TranslationRun.started_at) * 1000\n )\n )\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.started_at.isnot(None),\n TranslationRun.completed_at.isnot(None),\n )\n .scalar()\n )\n\n # Last run\n last_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job_id)\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n\n # Next scheduled run\n next_schedule = (\n self.db.query(TranslationSchedule)\n .filter(\n TranslationSchedule.job_id == job_id,\n TranslationSchedule.is_active == True,\n )\n .first()\n )\n\n # Cumulative tokens/cost from MetricSnapshot + events\n cumulative_tokens = 0\n cumulative_cost = 0.0\n\n # Latest MetricSnapshot\n latest_snapshot = (\n self.db.query(MetricSnapshot)\n .filter(MetricSnapshot.job_id == job_id)\n .order_by(MetricSnapshot.snapshot_date.desc())\n .first()\n )\n if latest_snapshot:\n # MetricSnapshot stores per-snapshot aggregated tokens/cost\n # Here we sum what we stored — in practice MetricSnapshot.covers_events_before\n # indicates cutoff\n pass\n\n # Live events (<90 days) for token/cost\n cutoff = datetime.now(timezone.utc)\n live_events = (\n self.db.query(TranslationEvent)\n .filter(\n TranslationEvent.job_id == job_id,\n TranslationEvent.event_type.in_([\"TRANSLATION_PHASE_COMPLETED\", \"RUN_COMPLETED\"]),\n TranslationEvent.created_at > cutoff, # events newer than snapshot\n )\n .all()\n )\n\n return {\n \"job_id\": job_id,\n \"total_runs\": total_runs,\n \"successful_runs\": status_counts.get(\"COMPLETED\", 0),\n \"failed_runs\": status_counts.get(\"FAILED\", 0),\n \"cancelled_runs\": status_counts.get(\"CANCELLED\", 0),\n \"total_records\": int(total_records),\n \"successful_records\": int(successful_records),\n \"failed_records\": int(failed_records),\n \"skipped_records\": int(skipped_records),\n \"cumulative_tokens\": cumulative_tokens,\n \"cumulative_cost\": cumulative_cost,\n \"avg_duration_ms\": int(avg_duration) if avg_duration else None,\n \"last_run_at\": last_run.created_at.isoformat() if last_run else None,\n \"next_scheduled_run\": next_schedule.last_run_at.isoformat() if next_schedule and next_schedule.last_run_at else None,\n }\n # #endregion get_job_metrics\n\n # #region get_all_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,all]\n # @BRIEF Get aggregated metrics for all jobs.\n def get_all_metrics(self) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationMetrics.get_all_metrics\"):\n job_ids = (\n self.db.query(TranslationRun.job_id)\n .distinct()\n .all()\n )\n return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]]\n # #endregion get_all_metrics\n\n\n# #endregion TranslationMetrics\n# #endregion TranslationMetrics\n" }, @@ -66005,27 +68154,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Get aggregated metrics for all jobs.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Get aggregated metrics for all jobs." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region get_all_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,all]\n # @BRIEF Get aggregated metrics for all jobs.\n def get_all_metrics(self) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationMetrics.get_all_metrics\"):\n job_ids = (\n self.db.query(TranslationRun.job_id)\n .distinct()\n .all()\n )\n return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]]\n # #endregion get_all_metrics\n" }, @@ -66038,13 +68171,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[db, config_manager, current_user] -> Output[TranslationRun]", "INVARIANT": "State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.", "LAYER": "Domain", "POST": "Translation run is executed, SQL generated and submitted, events recorded.", "PRE": "Valid job and accepted preview (for manual runs). Superset and LLM are reachable.", + "PURPOSE": "Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.", "RATIONALE": "C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.", "REJECTED": "stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.", "SIDE_EFFECT": "Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events." @@ -66093,23 +68226,7 @@ "target_ref": "[TranslationPreviewSession]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS translate,orchestrator,lifecycle,run]\n# @BRIEF Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationExecutor]\n# @RELATION DEPENDS_ON -> [SQLGenerator]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @PRE Valid job and accepted preview (for manual runs). Superset and LLM are reachable.\n# @POST Translation run is executed, SQL generated and submitted, events recorded.\n# @SIDE_EFFECT Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events.\n# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.\n# @RATIONALE C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.\n# @REJECTED Distributed actor model (Celery) — eventual-consistency challenges at current scale.\n# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n\nimport json\nimport time\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Callable, Tuple\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n TranslationBatch,\n TranslationRecord,\n TranslationPreviewSession,\n)\nfrom ...schemas.translate import TranslationRunResponse\nfrom .executor import TranslationExecutor\nfrom .sql_generator import SQLGenerator\nfrom .superset_executor import SupersetSqlLabExecutor\nfrom .events import TranslationEventLog\nfrom ..translate.service import TranslateJobService\n\nlog = MarkerLogger(\"TranslationOrchestrator\")\n\n# #region TranslationOrchestrator [C:5] [TYPE Class] [SEMANTICS translate,orchestrator]\n# @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.\n# @PRE DB session and config manager are available.\n# @POST Runs are created, executed, and finalized with event records.\n# @SIDE_EFFECT Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows.\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.\n# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]\nclass TranslationOrchestrator:\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: Optional[str] = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n self._job: Optional[TranslationJob] = None\n\n # #region start_run [C:5] [TYPE Function] [SEMANTICS translate,run,create]\n # @BRIEF Start a new translation run for a job with config snapshot and hash computation.\n # @PRE job_id exists. For manual runs, there must be an accepted preview session.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.\n # @SIDE_EFFECT DB writes; event logging.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: Optional[str] = None,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n # Load and validate job\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n self._job = job\n\n log.reason(\"Starting translation run\", payload={\n \"job_id\": job_id,\n \"is_scheduled\": is_scheduled,\n })\n\n # Validate preconditions\n self._validate_preconditions(job, is_scheduled=is_scheduled)\n\n # Compute hashes\n config_hash = self._compute_config_hash(job)\n dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id)\n\n # Build config snapshot\n config_snapshot = {\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"database_dialect\": job.database_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n \"target_schema\": job.target_schema,\n \"target_table\": job.target_table,\n \"source_key_cols\": job.source_key_cols,\n \"target_key_cols\": job.target_key_cols,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": self._compute_dict_snapshot_hash(job_id),\n }\n\n # Compute key_hash from source_key_cols\n import hashlib\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n # Create run record with all hash/snapshot fields\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash,\n dict_snapshot_hash=dict_snapshot_hash,\n created_by=self.current_user,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(run)\n self.db.flush()\n\n # Record event\n self.event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash,\n \"dict_snapshot_hash\": dict_snapshot_hash,\n \"key_hash\": key_hash,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run created\", payload={\n \"run_id\": run.id,\n \"job_id\": job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n })\n return run\n # #endregion start_run\n\n # #region execute_run [C:5] [TYPE Function] [SEMANTICS translate,run,execute]\n # @BRIEF Execute a translation run: dispatch executor, generate SQL, submit to Superset.\n # @PRE run is in PENDING status.\n # @POST Run is executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Optional[Callable[[str, int, int, int, int], None]] = None,\n skip_insert: bool = False,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n if run.status != \"PENDING\":\n raise ValueError(\n f\"Cannot execute run in status '{run.status}'. \"\n f\"Run must be in PENDING status.\"\n )\n\n log.reason(\"Executing run\", payload={\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"skip_insert\": skip_insert,\n })\n\n # Record translation phase start\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_STARTED\",\n payload={},\n created_by=self.current_user,\n )\n\n # Dispatch executor\n executor = TranslationExecutor(\n self.db, self.config_manager, self.current_user,\n on_batch_progress=on_batch_progress,\n )\n try:\n run = executor.execute_run(run, llm_progress_callback=None, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Translation execution failed\", error=str(e), payload={\n \"run_id\": run.id,\n })\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {e}\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_FAILED\",\n payload={\"error\": str(e), \"phase\": \"translation\"},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Record translation phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n # Skip insert phase if requested (e.g., for preview-only execution)\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Generate SQL and submit to Superset\n insert_result = self._generate_and_insert_sql(job, run)\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n run.superset_execution_log = insert_result\n run.status = \"COMPLETED\" if insert_result.get(\"status\") == \"success\" else \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Record terminal event\n terminal_event = \"RUN_COMPLETED\"\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=terminal_event,\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"rows_affected\": insert_result.get(\"rows_affected\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run execution complete\", payload={\n \"run_id\": run.id,\n \"status\": run.status,\n \"insert_status\": run.insert_status,\n })\n return run\n # #endregion execute_run\n\n # #region _generate_and_insert_sql [C:5] [TYPE Function] [SEMANTICS translate,sql,insert]\n # @BRIEF Generate INSERT SQL from successful records and submit to Superset SQL Lab.\n # @PRE job has target table configured. run has successful records.\n # @POST SQL is generated and submitted. Returns execution result.\n # @SIDE_EFFECT Superset API call; event logging.\n def _generate_and_insert_sql(\n self,\n job: TranslationJob,\n run: TranslationRun,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator._generate_and_insert_sql\"):\n # Fetch successful records\n records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n log.reason(\"No successful records to insert\", payload={\"run_id\": run.id})\n return {\"status\": \"skipped\", \"reason\": \"no_records\", \"query_id\": None}\n\n log.reason(f\"Generating SQL for {len(records)} records\", payload={\n \"run_id\": run.id,\n \"dialect\": job.database_dialect or job.target_dialect,\n })\n\n # Build rows for SQL generation\n # Only include the translation column — context columns are for LLM only\n columns = []\n if job.translation_column:\n columns.append(job.translation_column)\n\n # Include key columns if present (for UPSERT matching)\n if job.target_key_cols:\n for k in job.target_key_cols:\n if k not in columns:\n columns.append(k)\n\n rows_for_sql = []\n for rec in records:\n row_data = {}\n if job.translation_column:\n row_data[job.translation_column] = rec.target_sql or \"\"\n if job.target_key_cols:\n source_data = rec.source_data or {}\n for k in job.target_key_cols:\n # Use source_data value if available, fall back to empty string\n row_data[k] = source_data.get(k, \"\")\n rows_for_sql.append(row_data)\n\n if not columns:\n # Use target_sql as the sole column\n columns = [job.translation_column or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n # Generate SQL\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=job.database_dialect or job.target_dialect or \"postgresql\",\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n log.explore(\"SQL generation failed\", error=str(e))\n return {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase start\n log.reason(\"Insert SQL generated\", payload={\n \"run_id\": run.id,\n \"sql_preview\": sql[:500],\n \"sql_length\": len(sql),\n \"row_count\": row_count,\n \"dialect\": job.database_dialect or job.target_dialect or \"postgresql\",\n \"columns\": columns,\n \"has_key_cols\": bool(job.target_key_cols),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_STARTED\",\n payload={\"sql_length\": len(sql), \"row_count\": row_count},\n created_by=self.current_user,\n )\n\n # Submit to Superset\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n # Resolve database_id: explicit target_database_id from job (int or UUID), or resolve from environment\n target_db_id = None\n if job.target_database_id:\n try:\n target_db_id = int(job.target_database_id)\n except (ValueError, TypeError):\n # Could be a UUID — pass as-is, executor will resolve it\n target_db_id = job.target_database_id\n log.reason(\"target_database_id is not an integer, will resolve as UUID\", payload={\n \"target_database_id\": job.target_database_id,\n })\n executor = SupersetSqlLabExecutor(self.config_manager, env_id, database_id=target_db_id)\n result = executor.execute_and_poll(\n sql=sql,\n max_polls=30,\n poll_interval_seconds=2.0,\n )\n except Exception as e:\n log.explore(\"Superset SQL submission failed\", error=str(e), payload={\n \"run_id\": run.id,\n })\n result = {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_COMPLETED\",\n payload=result,\n created_by=self.current_user,\n )\n\n return result\n # #endregion _generate_and_insert_sql\n\n # #region _validate_preconditions [C:5] [TYPE Function] [SEMANTICS translate,validation]\n # @BRIEF Validate preconditions before starting a translation run.\n # @PRE None.\n # @POST Raises ValueError if preconditions are not met.\n # @SIDE_EFFECT None.\n def _validate_preconditions(\n self,\n job: TranslationJob,\n is_scheduled: bool = False,\n ) -> None:\n with belief_scope(\"TranslationOrchestrator._validate_preconditions\"):\n # Job must be in valid status\n if job.status in (\"DRAFT\",):\n raise ValueError(\n f\"Cannot run job '{job.id}' in status '{job.status}'. \"\n f\"Job must be READY, ACTIVE, or COMPLETED.\"\n )\n\n # Must have target table configured\n if not job.target_table:\n raise ValueError(\n f\"Job '{job.id}' has no target table configured. \"\n \"Configure a target table before running.\"\n )\n\n # Must have LLM provider configured\n if not job.provider_id:\n raise ValueError(\n f\"Job '{job.id}' has no LLM provider configured. \"\n \"Select an LLM provider before running.\"\n )\n\n # Must have a translation column\n if not job.translation_column:\n raise ValueError(\n f\"Job '{job.id}' has no translation column configured. \"\n \"Select a translation column before running.\"\n )\n\n # For manual runs, must have accepted preview\n if not is_scheduled:\n accepted_session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job.id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not accepted_session:\n raise ValueError(\n f\"Job '{job.id}' has no accepted preview session. \"\n \"Run and accept a preview before executing a manual translation run.\"\n )\n\n log.reason(\"Preconditions validated\", payload={\n \"job_id\": job.id,\n \"is_scheduled\": is_scheduled,\n })\n # #endregion _validate_preconditions\n\n # #region retry_failed_batches [C:5] [TYPE Function] [SEMANTICS translate,retry,batch]\n # @BRIEF Retry failed batches in a run by re-processing failed records.\n # @PRE run exists and has failed batches.\n # @POST Failed batches are re-processed; run status and stats updated.\n # @SIDE_EFFECT LLM calls; DB writes; event logging.\n def retry_failed_batches(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_failed_batches\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n # Find failed batches\n failed_batches = (\n self.db.query(TranslationBatch)\n .filter(\n TranslationBatch.run_id == run_id,\n TranslationBatch.status.in_([\"FAILED\", \"COMPLETED_WITH_ERRORS\"]),\n )\n .all()\n )\n\n if not failed_batches:\n raise ValueError(f\"No failed batches found for run '{run_id}'\")\n\n log.reason(\"Retrying failed batches\", payload={\n \"run_id\": run_id,\n \"batch_count\": len(failed_batches),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRYING\",\n payload={\"batch_count\": len(failed_batches)},\n created_by=self.current_user,\n )\n\n # Re-process each failed batch\n executor = TranslationExecutor(self.db, self.config_manager, self.current_user)\n run.status = \"RUNNING\"\n self.db.flush()\n\n for batch in failed_batches:\n # Fetch failed records for this batch\n failed_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.batch_id == batch.id,\n TranslationRecord.status == \"FAILED\",\n )\n .all()\n )\n\n if not failed_records:\n continue\n\n # Build rows from failed records\n retry_rows = []\n for rec in failed_records:\n retry_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": None,\n \"source_object_name\": rec.source_object_name or \"\",\n })\n\n # Process retry batch\n result = executor._process_batch(\n job=job,\n run_id=run_id,\n batch_index=batch.batch_index,\n batch_rows=retry_rows,\n )\n\n # Update run stats\n run.successful_records = (run.successful_records or 0) + result[\"successful\"]\n run.failed_records = (run.failed_records or 0) + result[\"failed\"]\n run.skipped_records = (run.skipped_records or 0) + result[\"skipped\"]\n self.db.flush()\n\n # Update run status\n if run.failed_records == 0:\n run.status = \"COMPLETED\"\n elif run.successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\"\n\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"retry\": True},\n created_by=self.current_user,\n )\n\n self.db.commit()\n log.reflect(\"Retry complete\", payload={\n \"run_id\": run_id,\n \"status\": run.status,\n })\n return run\n # #endregion retry_failed_batches\n\n # #region retry_insert [C:5] [TYPE Function] [SEMANTICS translate,retry,insert]\n # @BRIEF Retry the SQL insert phase for a completed run.\n # @PRE run exists and has successful records.\n # @POST SQL is regenerated and re-submitted to Superset.\n # @SIDE_EFFECT Superset API call; event logging.\n def retry_insert(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_insert\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n log.reason(\"Retrying insert phase\", payload={\n \"run_id\": run_id,\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRY_INSERT\",\n payload={},\n created_by=self.current_user,\n )\n\n # Regenerate SQL and submit\n insert_result = self._generate_and_insert_sql(job, run)\n\n # Update run\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n self.db.flush()\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Insert retry complete\", payload={\n \"run_id\": run_id,\n \"insert_status\": run.insert_status,\n })\n return run\n # #endregion retry_insert\n\n # #region cancel_run [C:5] [TYPE Function] [SEMANTICS translate,run,cancel]\n # @BRIEF Cancel a running translation run.\n # @PRE run is in PENDING or RUNNING status.\n # @POST Run status is set to CANCELLED; event recorded.\n # @SIDE_EFFECT DB write; records event.\n def cancel_run(self, run_id: str) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.cancel_run\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n if run.status not in (\"PENDING\", \"RUNNING\"):\n raise ValueError(\n f\"Cannot cancel run in status '{run.status}'. \"\n f\"Only PENDING or RUNNING runs can be cancelled.\"\n )\n\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Record terminal event directly — we are the source of the terminal event\n self.event_log._create_event_raw(\n job_id=run.job_id,\n run_id=run.id,\n event_type=\"RUN_CANCELLED\",\n payload={},\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run cancelled\", payload={\n \"run_id\": run_id,\n })\n return run\n # #endregion cancel_run\n\n # #region get_run_status [C:5] [TYPE Function] [SEMANTICS translate,run,status]\n # @BRIEF Get run status with statistics and event invariant checks.\n # @PRE run_id exists.\n # @POST Returns dict with run details.\n def get_run_status(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_status\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n # Count batches\n batch_count = (\n self.db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .count()\n )\n\n # Get event invariants (lightweight — only fetches event_type column)\n invariants = self.event_log.get_run_event_invariants_lightweight(run_id)\n\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"batch_count\": batch_count,\n \"event_invariants\": invariants,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n # #endregion get_run_status\n\n # #region get_run_records [C:5] [TYPE Function] [SEMANTICS translate,run,records]\n # @BRIEF Get paginated records for a run with optional status filter.\n # @PRE run_id exists.\n # @POST Returns dict with records and pagination info.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: Optional[str] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_records\"):\n query = self.db.query(TranslationRecord).filter(\n TranslationRecord.run_id == run_id\n )\n\n if status_filter:\n query = query.filter(TranslationRecord.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n records = (\n query.order_by(TranslationRecord.created_at.desc())\n .offset(offset)\n .limit(page_size)\n .all()\n )\n\n return {\n \"items\": [\n {\n \"id\": r.id,\n \"batch_id\": r.batch_id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"error_message\": r.error_message,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in records\n ],\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"status_filter\": status_filter,\n }\n # #endregion get_run_records\n\n # #region get_run_history [C:5] [TYPE Function] [SEMANTICS translate,run,history]\n # @BRIEF Get paginated run history for a job.\n # @PRE job_id exists.\n # @POST Returns tuple of (total_count, list_of_runs).\n def get_run_history(\n self,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n ) -> Tuple[int, List[Dict[str, Any]]]:\n with belief_scope(\"TranslationOrchestrator.get_run_history\"):\n query = self.db.query(TranslationRun).filter(\n TranslationRun.job_id == job_id\n )\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n return total, [\n {\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in runs\n ]\n # #endregion get_run_history\n\n # #region _compute_config_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of the job's current configuration for snapshot comparison.\n @staticmethod\n def _compute_config_hash(job: TranslationJob) -> str:\n import hashlib\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # #endregion _compute_config_hash\n\n # #region _compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of dictionary state for snapshot comparison.\n def _compute_dict_snapshot_hash(self, job_id: str) -> str:\n import hashlib\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n self.db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n # #endregion _compute_dict_snapshot_hash\n\n\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" }, @@ -66122,20 +68239,14 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Start a new translation run for a job with config snapshot and hash computation.", "COMPLEXITY": 5, "POST": "TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.", "PRE": "job_id exists. For manual runs, there must be an accepted preview session.", + "PURPOSE": "Start a new translation run for a job with config snapshot and hash computation.", "SIDE_EFFECT": "DB writes; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -66156,8 +68267,17 @@ }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C5", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", "detail": { "actual_complexity": 5, "contract_type": "Function" @@ -66185,20 +68305,14 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Generate INSERT SQL from successful records and submit to Superset SQL Lab.", "COMPLEXITY": 5, "POST": "SQL is generated and submitted. Returns execution result.", "PRE": "job has target table configured. run has successful records.", + "PURPOSE": "Generate INSERT SQL from successful records and submit to Superset SQL Lab.", "SIDE_EFFECT": "Superset API call; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -66219,8 +68333,17 @@ }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C5", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", "detail": { "actual_complexity": 5, "contract_type": "Function" @@ -66248,20 +68371,14 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Validate preconditions before starting a translation run.", "COMPLEXITY": 5, "POST": "Raises ValueError if preconditions are not met.", "PRE": "None.", + "PURPOSE": "Validate preconditions before starting a translation run.", "SIDE_EFFECT": "None." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -66282,8 +68399,17 @@ }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C5", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", "detail": { "actual_complexity": 5, "contract_type": "Function" @@ -66311,20 +68437,14 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Retry failed batches in a run by re-processing failed records.", "COMPLEXITY": 5, "POST": "Failed batches are re-processed; run status and stats updated.", "PRE": "run exists and has failed batches.", + "PURPOSE": "Retry failed batches in a run by re-processing failed records.", "SIDE_EFFECT": "LLM calls; DB writes; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -66345,8 +68465,17 @@ }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C5", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", "detail": { "actual_complexity": 5, "contract_type": "Function" @@ -66374,27 +68503,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Compute a SHA-256 hash of the job's current configuration for snapshot comparison.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Compute a SHA-256 hash of the job's current configuration for snapshot comparison." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region _compute_config_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of the job's current configuration for snapshot comparison.\n @staticmethod\n def _compute_config_hash(job: TranslationJob) -> str:\n import hashlib\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # #endregion _compute_config_hash\n" }, @@ -66407,27 +68520,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Compute a SHA-256 hash of dictionary state for snapshot comparison.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Compute a SHA-256 hash of dictionary state for snapshot comparison." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region _compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of dictionary state for snapshot comparison.\n def _compute_dict_snapshot_hash(self, job_id: str) -> str:\n import hashlib\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n self.db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n # #endregion _compute_dict_snapshot_hash\n" }, @@ -66440,9 +68537,9 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.", "COMPLEXITY": 3, - "LAYER": "Domain" + "LAYER": "Domain", + "PURPOSE": "TranslatePlugin skeleton for cross-dialect SQL and dashboard translation." }, "relations": [ { @@ -66452,23 +68549,7 @@ "target_ref": "[PluginBase]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslatePlugin [C:3] [TYPE Module] [SEMANTICS plugin,translate,llm,sql,dashboard]\n# @BRIEF TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.\n# @LAYER Domain\n# @RELATION INHERITS -> [PluginBase]\n\nfrom typing import Dict, Any, Optional\nfrom ...core.plugin_base import PluginBase\n\n\n# #region TranslatePlugin [C:3] [TYPE Class] [SEMANTICS plugin,translate]\n# @BRIEF Plugin for translating SQL queries and dashboard definitions across database dialects.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass TranslatePlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"translate\"\n\n @property\n def name(self) -> str:\n return \"LLM Table Translation\"\n\n @property\n def description(self) -> str:\n return \"Cross-dialect SQL and dashboard translation using LLMs.\"\n\n @property\n def version(self) -> str:\n return \"1.0.0\"\n\n def get_schema(self) -> Dict[str, Any]:\n return {\n \"type\": \"object\",\n \"properties\": {\n \"source_dialect\": {\n \"type\": \"string\",\n \"title\": \"Source Dialect\",\n \"description\": \"Source database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"target_dialect\": {\n \"type\": \"string\",\n \"title\": \"Target Dialect\",\n \"description\": \"Target database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"job_id\": {\n \"type\": \"string\",\n \"title\": \"Translation Job ID\",\n \"description\": \"Existing translation job to execute\",\n },\n },\n \"required\": [\"job_id\"],\n }\n\n async def execute(self, params: Dict[str, Any], context: Optional[Any] = None):\n \"\"\"Execute a translation job — implementation deferred to later phases.\"\"\"\n raise NotImplementedError(\"TranslatePlugin.execute not yet implemented\")\n# #endregion TranslatePlugin\n\n# #endregion TranslatePlugin\n" }, @@ -66496,7 +68577,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -66537,7 +68620,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -66563,27 +68648,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Estimate token counts and costs for LLM translation operations using heuristic chars/token ratio.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Estimate token counts and costs for LLM translation operations using heuristic chars/token ratio." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TokenEstimator [C:2] [TYPE Class] [SEMANTICS translate,tokens,estimate]\n# @BRIEF Estimate token counts and costs for LLM translation operations using heuristic chars/token ratio.\nclass TokenEstimator:\n \"\"\"Estimate token counts and costs for LLM operations.\"\"\"\n\n CHARS_PER_TOKEN_ESTIMATE: float = 4.0\n OUTPUT_TOKENS_PER_ROW_ESTIMATE: int = 50\n TOKEN_COST_PER_1K: float = 0.002 # Default cost per 1K tokens\n\n # #region estimate_prompt_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # #endregion estimate_prompt_tokens\n\n # #region estimate_output_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_output_tokens(row_count: int) -> int:\n return row_count * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE\n # #endregion estimate_output_tokens\n\n # #region estimate_cost [C:1] [TYPE Function] [SEMANTICS translate,cost]\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: Optional[float] = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # #endregion estimate_cost\n\n\n# #endregion TokenEstimator\n" }, @@ -66599,17 +68668,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region estimate_prompt_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # #endregion estimate_prompt_tokens\n" }, @@ -66625,17 +68684,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region estimate_output_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_output_tokens(row_count: int) -> int:\n return row_count * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE\n # #endregion estimate_output_tokens\n" }, @@ -66651,17 +68700,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region estimate_cost [C:1] [TYPE Function] [SEMANTICS translate,cost]\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: Optional[float] = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # #endregion estimate_cost\n" }, @@ -66674,29 +68713,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.", "COMPLEXITY": 4, "POST": "Returns TranslationPreviewResponse with records, cost estimation, and persistent session.", "PRE": "job_id exists and job has source_datasource_id, translation_column configured.", + "PURPOSE": "Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.", "SIDE_EFFECT": "Fetches data from Superset; calls LLM; creates TranslationPreviewSession and TranslationPreviewRecord rows." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -66719,8 +68743,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get the latest preview session for a job with its records.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get the latest preview session for a job with its records." }, "relations": [ { @@ -66730,23 +68754,7 @@ "target_ref": "[TranslationPreviewSession]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region get_preview_session [C:3] [TYPE Function] [SEMANTICS translate,preview,get]\n # @BRIEF Get the latest preview session for a job with its records.\n # @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n def get_preview_session(self, job_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationPreview.get_preview_session\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id)\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No preview session found for job '{job_id}'\")\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n\n return {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": session.status,\n \"created_by\": session.created_by,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"records\": [\n {\n \"id\": r.id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"feedback\": r.feedback,\n }\n for r in records\n ],\n }\n # #endregion get_preview_session\n" }, @@ -66759,29 +68767,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch sample rows from the Superset dataset for preview.", "COMPLEXITY": 4, "POST": "Returns list of dicts with row data from Superset chart data API.", "PRE": "job has source_datasource_id and translation_column.", + "PURPOSE": "Fetch sample rows from the Superset dataset for preview.", "SIDE_EFFECT": "Calls Superset chart data endpoint with pagination." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -66804,8 +68797,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Extract data rows from Superset chart data response, trying multiple response formats.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Extract data rows from Superset chart data response, trying multiple response formats." }, "relations": [ { @@ -66815,23 +68808,7 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region _extract_data_rows [C:3] [TYPE Function] [SEMANTICS translate,superset,data]\n # @BRIEF Extract data rows from Superset chart data response, trying multiple response formats.\n # @RELATION DEPENDS_ON -> [SupersetClient]\n @staticmethod\n def _extract_data_rows(response: Dict[str, Any]) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationPreview._extract_data_rows\"):\n # Try various response formats\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Try flat result\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Legacy: response may have data at top level\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Last resort: return response itself wrapped if it looks like a list of rows\n if isinstance(result, list):\n return result\n\n return []\n # #endregion _extract_data_rows\n" }, @@ -66844,13 +68821,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Manage TranslationSchedule rows and register them with core SchedulerService for cron-based execution.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[job_id, cron_expression] -> Output[TranslationSchedule]", "INVARIANT": "Concurrency guard: max 1 pending/running run per job at scheduled trigger time.", "LAYER": "Domain", "POST": "TranslationSchedule CRUD persisted; translation runs triggered on schedule.", "PRE": "Database session and SchedulerService are available.", + "PURPOSE": "Manage TranslationSchedule rows and register them with core SchedulerService for cron-based execution.", "RATIONALE": "Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.", "REJECTED": "Polling-based approach — event-driven APScheduler is more precise.", "SIDE_EFFECT": "Registers APScheduler jobs; runs translations on trigger; creates events." @@ -66881,23 +68858,7 @@ "target_ref": "[TranslationEventLog]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationScheduler [C:5] [TYPE Module] [SEMANTICS translate,scheduler,apscheduler,cron]\n# @BRIEF Manage TranslationSchedule rows and register them with core SchedulerService for cron-based execution.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [SchedulerService]\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @PRE Database session and SchedulerService are available.\n# @POST TranslationSchedule CRUD persisted; translation runs triggered on schedule.\n# @SIDE_EFFECT Registers APScheduler jobs; runs translations on trigger; creates events.\n# @DATA_CONTRACT Input[job_id, cron_expression] -> Output[TranslationSchedule]\n# @INVARIANT Concurrency guard: max 1 pending/running run per job at scheduled trigger time.\n# @RATIONALE Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.\n# @REJECTED Separate scheduler instance would create resource contention.\n# @REJECTED Polling-based approach — event-driven APScheduler is more precise.\n\nimport uuid\nfrom datetime import datetime, timezone, timedelta\nfrom typing import Any, Dict, List, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import belief_scope\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import TranslationSchedule, TranslationJob, TranslationRun\nfrom .events import TranslationEventLog\n\nlog = MarkerLogger(\"TranslationScheduler\")\n\n\n# #region TranslationScheduler [C:5] [TYPE Class] [SEMANTICS translate,scheduler,crud]\n# @BRIEF CRUD for TranslationSchedule rows + APScheduler registration wrappers.\n# @PRE Database session and ConfigManager are available.\n# @POST Schedule rows created/updated/deleted with event logging.\n# @SIDE_EFFECT Writes TranslationSchedule rows; logs SCHEDULE_CREATED/UPDATED/DELETED events.\n# @DATA_CONTRACT Input[job_id, cron_expression, timezone] -> Output[TranslationSchedule]\n# @INVARIANT Exactly one schedule per job (upsert pattern).\nclass TranslationScheduler:\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: Optional[str] = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n\n # #region create_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,create]\n # @BRIEF Create a new schedule for a job with cron expression and event logging.\n # @PRE job_id exists. cron_expression is valid.\n # @POST TranslationSchedule row created; SCHEDULE_CREATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def create_schedule(\n self,\n job_id: str,\n cron_expression: str,\n timezone: str = \"UTC\",\n is_active: bool = True,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.create_schedule\"):\n # Verify job exists\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n schedule = TranslationSchedule(\n id=str(uuid.uuid4()),\n job_id=job_id,\n cron_expression=cron_expression,\n timezone=timezone,\n is_active=is_active,\n created_by=self.current_user,\n )\n self.db.add(schedule)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_CREATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": cron_expression,\n \"timezone\": timezone,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule created\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion create_schedule\n\n # #region update_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,update]\n # @BRIEF Update an existing schedule's cron expression, timezone, or active state.\n # @PRE job_id has an existing schedule.\n # @POST Schedule updated; SCHEDULE_UPDATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def update_schedule(\n self,\n job_id: str,\n cron_expression: Optional[str] = None,\n timezone_str: Optional[str] = None,\n is_active: Optional[bool] = None,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.update_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n if cron_expression is not None:\n schedule.cron_expression = cron_expression\n if timezone_str is not None:\n schedule.timezone = timezone_str\n if is_active is not None:\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_UPDATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule updated\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion update_schedule\n\n # #region delete_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,delete]\n # @BRIEF Delete a schedule for a job with event logging.\n # @PRE job_id has an existing schedule.\n # @POST Schedule deleted; SCHEDULE_DELETED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def delete_schedule(self, job_id: str) -> None:\n with belief_scope(\"TranslationScheduler.delete_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule_id = schedule.id\n self.db.delete(schedule)\n self.db.commit()\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_DELETED\",\n payload={\"schedule_id\": schedule_id},\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule deleted\", payload={\"schedule_id\": schedule_id, \"job_id\": job_id})\n # #endregion delete_schedule\n\n # #region set_schedule_active [C:4] [TYPE Function] [SEMANTICS translate,schedule,activate]\n # @BRIEF Enable or disable a schedule by setting is_active flag.\n # @PRE job_id has an existing schedule.\n # @POST Schedule is_active updated.\n # @SIDE_EFFECT DB write.\n def set_schedule_active(self, job_id: str, is_active: bool) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.set_schedule_active\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n log.reflect(\"Schedule active state set\", payload={\n \"schedule_id\": schedule.id,\n \"job_id\": job_id,\n \"is_active\": is_active,\n })\n return schedule\n # #endregion set_schedule_active\n\n # #region get_schedule [C:2] [TYPE Function] [SEMANTICS translate,schedule,get]\n # @BRIEF Get the schedule for a job by job_id.\n def get_schedule(self, job_id: str) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.get_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n return schedule\n # #endregion get_schedule\n\n # #region list_active_schedules [C:2] [TYPE Function] [SEMANTICS translate,schedule,list]\n # @BRIEF List all active schedules.\n @staticmethod\n def list_active_schedules(db: Session) -> List[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n # #endregion list_active_schedules\n\n # #region get_next_executions [C:2] [TYPE Function] [SEMANTICS translate,schedule,next]\n # @BRIEF Compute next N execution times from a cron expression using APScheduler's CronTrigger.\n def get_next_executions(cron_expression: str, timezone_str: str = \"UTC\", n: int = 3) -> List[str]:\n from zoneinfo import ZoneInfo\n from apscheduler.triggers.cron import CronTrigger\n from apscheduler.triggers.interval import IntervalTrigger\n\n try:\n tz = ZoneInfo(timezone_str)\n trigger = CronTrigger.from_crontab(cron_expression, timezone=tz)\n except (ValueError, KeyError) as e:\n log.explore(\"Invalid cron\", error=str(e))\n return []\n\n now = datetime.now(tz)\n results = []\n next_time = now\n prev = None\n for _ in range(n):\n ft = trigger.get_next_fire_time(prev, next_time)\n if ft is None:\n break\n results.append(ft.isoformat())\n prev = ft\n next_time = ft\n return results\n # #endregion get_next_executions\n\n\n# #endregion TranslationScheduler\n\n\n# #region execute_scheduled_translation [C:5] [TYPE Function] [SEMANTICS translate,schedule,execute]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.\n# @PRE schedule_id is valid and job exists.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls; event logging.\n# @DATA_CONTRACT Input[schedule_id, job_id, db_session_maker, config_manager] -> Output[None]\n# @INVARIANT Concurrency guard: max 1 pending/running run per job at trigger time.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n log.reason(\"Schedule inactive/missing\", payload={\"schedule_id\": schedule_id})\n return\n\n log.reason(\"Scheduled translation triggered\", payload={\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .first()\n )\n if active_run:\n log.explore(\"Skipping scheduled run — concurrent run in progress\", error=\"Concurrent run detected\",\n payload={\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(timezone.utc) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n log.reason(\"Baseline expired — full translation\", payload={\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n log.explore(\"Scheduled translation execution failed\", error=str(exec_err))\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(timezone.utc)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(timezone.utc)\n db.commit()\n\n log.reflect(\"Scheduled translation complete\", payload={\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n log.explore(\"Unexpected error\", error=str(e))\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n# #endregion TranslationScheduler\n" }, @@ -66910,29 +68871,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Create a new schedule for a job with cron expression and event logging.", "COMPLEXITY": 4, "POST": "TranslationSchedule row created; SCHEDULE_CREATED event logged.", "PRE": "job_id exists. cron_expression is valid.", + "PURPOSE": "Create a new schedule for a job with cron expression and event logging.", "SIDE_EFFECT": "DB write; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -66955,29 +68901,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Update an existing schedule's cron expression, timezone, or active state.", "COMPLEXITY": 4, "POST": "Schedule updated; SCHEDULE_UPDATED event logged.", "PRE": "job_id has an existing schedule.", + "PURPOSE": "Update an existing schedule's cron expression, timezone, or active state.", "SIDE_EFFECT": "DB write; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67000,29 +68931,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Enable or disable a schedule by setting is_active flag.", "COMPLEXITY": 4, "POST": "Schedule is_active updated.", "PRE": "job_id has an existing schedule.", + "PURPOSE": "Enable or disable a schedule by setting is_active flag.", "SIDE_EFFECT": "DB write." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67045,27 +68961,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "List all active schedules.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "List all active schedules." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region list_active_schedules [C:2] [TYPE Function] [SEMANTICS translate,schedule,list]\n # @BRIEF List all active schedules.\n @staticmethod\n def list_active_schedules(db: Session) -> List[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n # #endregion list_active_schedules\n" }, @@ -67078,27 +68978,21 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[schedule_id, job_id, db_session_maker, config_manager] -> Output[None]", "INVARIANT": "Concurrency guard: max 1 pending/running run per job at trigger time.", "POST": "Translation run created and executed if no concurrent run exists.", "PRE": "schedule_id is valid and job exists.", + "PURPOSE": "APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.", "RATIONALE": "New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.", "SIDE_EFFECT": "DB writes; LLM calls; Superset API calls; event logging." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C5", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", "detail": { "actual_complexity": 5, "contract_type": "Function" @@ -67126,13 +69020,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Service layer for translation job CRUD with datasource column validation and database dialect detection.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[TranslateJobCreate/Update] -> Output[TranslationJob/TranslateJobResponse]", "INVARIANT": "Snapshot isolation: in-progress runs use config snapshot; config edits affect future runs only.", "LAYER": "Domain", "POST": "Translation jobs are created/updated/deleted with column validation and dialect caching.", "PRE": "Database session and config manager are available.", + "PURPOSE": "Service layer for translation job CRUD with datasource column validation and database dialect detection.", "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", "REJECTED": "Invalidating in-progress runs on config edit would break scheduled run continuity.", "SIDE_EFFECT": "Queries Superset for column metadata and database dialect at save time." @@ -67157,23 +69051,7 @@ "target_ref": "[ConfigManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateJobService [C:5] [TYPE Module] [SEMANTICS translate,service,crud,validation]\n# @BRIEF Service layer for translation job CRUD with datasource column validation and database dialect detection.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Database session and config manager are available.\n# @POST Translation jobs are created/updated/deleted with column validation and dialect caching.\n# @SIDE_EFFECT Queries Superset for column metadata and database dialect at save time.\n# @DATA_CONTRACT Input[TranslateJobCreate/Update] -> Output[TranslationJob/TranslateJobResponse]\n# @INVARIANT Snapshot isolation: in-progress runs use config snapshot; config edits affect future runs only.\n# @RATIONALE Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.\n# @REJECTED Invalidating in-progress runs on config edit would break scheduled run continuity.\n\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom sqlalchemy.orm import Session\nfrom datetime import datetime, timezone\nimport uuid\n\nfrom ...core.logger import logger\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...core.superset_client import SupersetClient\nfrom ...models.translate import TranslationJob, TranslationJobDictionary\nfrom ...schemas.translate import (\n TranslateJobCreate,\n TranslateJobUpdate,\n TranslateJobResponse,\n DatasourceColumnsResponse,\n DatasourceColumnResponse,\n)\n\nlog = MarkerLogger(\"TranslateJobService\")\n\n# Supported database dialects for translation\nSUPPORTED_DIALECTS = {\n \"postgresql\", \"mysql\", \"clickhouse\", \"sqlite\", \"mssql\",\n \"oracle\", \"snowflake\", \"bigquery\", \"redshift\", \"presto\",\n \"trino\", \"druid\", \"hive\", \"spark\", \"databricks\",\n}\n\n\n# #region get_dialect_from_database [C:4] [TYPE Function] [SEMANTICS translate,dialect,validation]\n# @BRIEF Extract normalized dialect string from a Superset database record and validate it is supported.\n# @PRE database_record is a dict from Superset API with 'backend' or 'engine' key.\n# @POST Returns normalized dialect string or raises ValueError if unsupported.\n# @SIDE_EFFECT None — pure data extraction.\ndef get_dialect_from_database(database_record: Dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n log.reason(\"Extracting dialect from database record\", payload={\n \"has_backend\": bool(database_record.get(\"backend\") or database_record.get(\"engine\")),\n })\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n log.explore(\"Could not determine database dialect\", error=\"No backend/engine field in database record\", payload={\n \"record_keys\": list(database_record.keys()),\n })\n raise ValueError(\"Could not determine database dialect from connection\")\n\n # Map Superset backend names to normalized dialect\n dialect_map = {\n \"postgresql\": \"postgresql\",\n \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\",\n \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\",\n \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\",\n \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\",\n \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\",\n \"presto\": \"presto\",\n \"trino\": \"trino\",\n \"druid\": \"druid\",\n \"hive\": \"hive\",\n \"spark\": \"spark\",\n \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n log.explore(\"Unsupported dialect\", payload={\"backend\": backend, \"normalized\": normalized}, error=f\"Unsupported database dialect: {backend}\")\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n log.reflect(\"Dialect validated\", payload={\"dialect\": normalized})\n return normalized\n# #endregion get_dialect_from_database\n\n\n# #region fetch_datasource_metadata [C:4] [TYPE Function] [SEMANTICS translate,datasource,metadata]\n# @BRIEF Fetch datasource columns and database dialect from Superset for a given dataset.\n# @PRE dataset_id is a valid Superset dataset ID and environment has valid credentials.\n# @POST Returns (columns_list, dialect_string) or raises on failure.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> Tuple[List[Dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n log.reason(\"Fetching datasource metadata\", payload={\n \"dataset_id\": dataset_id,\n \"env_id\": env_id,\n })\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n log.explore(\"Environment not found\", payload={\"env_id\": env_id}, error=f\"Environment {env_id} not configured\")\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n # Create Superset client and fetch dataset detail\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n # Extract database dialect from datasource\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n # Fallback: try to fetch database directly\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n log.explore(\"Could not fetch database dialect\", error=str(e))\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n log.reflect(\"Metadata fetched\", payload={\"columns\": len(columns), \"dialect\": dialect})\n return columns, dialect\n# #endregion fetch_datasource_metadata\n\n\n# #region detect_virtual_columns [C:2] [TYPE Function] [SEMANTICS translate,columns]\n# @BRIEF Identify virtual (calculated) columns from column metadata by filtering non-physical entries.\ndef detect_virtual_columns(columns: List[Dict[str, Any]]) -> List[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n\n\n# #region TranslateJobService [C:5] [TYPE Class] [SEMANTICS translate,service,crud]\n# @BRIEF Service for translation job CRUD with validation and Superset integration.\n# @PRE Database session and config manager are available.\n# @POST Translation jobs are managed with full lifecycle (create/update/delete/duplicate).\n# @SIDE_EFFECT Queries Superset for dialect detection and column validation.\n# @DATA_CONTRACT Input[TranslateJobCreate/Update] -> Output[TranslationJob]\n# @INVARIANT Snapshot isolation: in-progress runs unaffected by config edits.\nclass TranslateJobService:\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: Optional[str] = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n\n # #region list_jobs [C:2] [TYPE Function] [SEMANTICS translate,jobs,query]\n # @BRIEF List translation jobs with optional status filter and pagination.\n def list_jobs(\n self,\n page: int = 1,\n page_size: int = 20,\n status_filter: Optional[str] = None,\n ) -> Tuple[int, List[TranslationJob]]:\n query = self.db.query(TranslationJob)\n\n if status_filter:\n query = query.filter(TranslationJob.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n jobs = query.order_by(TranslationJob.created_at.desc()).offset(offset).limit(page_size).all()\n\n return total, jobs\n # #endregion list_jobs\n\n # #region get_job [C:2] [TYPE Function] [SEMANTICS translate,jobs,query]\n # @BRIEF Get a single translation job by ID, raising ValueError if not found.\n def get_job(self, job_id: str) -> TranslationJob:\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n return job\n # #endregion get_job\n\n # #region create_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,create]\n # @BRIEF Create a new translation job with column validation and dialect detection.\n # @PRE payload contains valid job configuration (name, upsert_strategy, etc).\n # @POST Returns the created TranslationJob with database_dialect cached.\n # @SIDE_EFFECT Validates columns via SupersetClient if source_datasource_id is provided; caches database_dialect.\n def create_job(self, payload: TranslateJobCreate) -> TranslationJob:\n log.reason(\"Creating translation job\", payload={\"name\": payload.name})\n\n # Validate: must have a translation column if datasource is configured\n if payload.source_datasource_id and not payload.translation_column:\n log.explore(\"Missing translation column\", error=\"translation_column required when source_datasource_id is set\", payload={\n \"source_datasource_id\": payload.source_datasource_id,\n })\n raise ValueError(\"A translation column is required when a datasource is selected\")\n\n # Validate upsert strategy\n valid_strategies = {\"MERGE\", \"INSERT\", \"UPDATE\"}\n if payload.upsert_strategy not in valid_strategies:\n raise ValueError(\n f\"Invalid upsert_strategy '{payload.upsert_strategy}'. \"\n f\"Must be one of: {', '.join(sorted(valid_strategies))}\"\n )\n\n # Detect database dialect and validate columns if datasource is specified\n dialect = payload.database_dialect\n if payload.source_datasource_id and (payload.environment_id or payload.source_dialect):\n # If no explicit dialect, try to detect it\n if not dialect:\n try:\n env_id = payload.environment_id or payload.source_dialect\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n dialect = detected_dialect\n except Exception as e:\n log.explore(\"Dialect detection failed\", error=str(e))\n dialect = payload.source_dialect\n\n # Build job instance\n job = TranslationJob(\n id=str(uuid.uuid4()),\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n database_dialect=dialect,\n source_datasource_id=payload.source_datasource_id,\n source_table=payload.source_table,\n target_schema=payload.target_schema,\n target_table=payload.target_table,\n source_key_cols=payload.source_key_cols or [],\n target_key_cols=payload.target_key_cols or [],\n translation_column=payload.translation_column,\n context_columns=payload.context_columns or [],\n target_language=payload.target_language,\n provider_id=payload.provider_id,\n batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy,\n environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n # Attach dictionaries\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job.id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n log.reflect(\"Job created\", payload={\"job_id\": job.id})\n return job\n # #endregion create_job\n\n # #region update_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,update]\n # @BRIEF Update an existing translation job with optional dialect re-detection.\n # @PRE payload contains fields to update; job_id exists.\n # @POST Returns the updated TranslationJob with re-detected dialect if datasource changed.\n # @SIDE_EFFECT Re-detects database_dialect if source_datasource_id changed.\n def update_job(self, job_id: str, payload: TranslateJobUpdate) -> TranslationJob:\n log.reason(\"Updating translation job\", payload={\"job_id\": job_id})\n job = self.get_job(job_id)\n\n update_data = payload.model_dump(exclude_unset=True)\n dict_ids = update_data.pop(\"dictionary_ids\", None)\n\n for field, value in update_data.items():\n if hasattr(job, field):\n setattr(job, field, value)\n\n # Re-detect dialect if datasource changed\n if payload.source_datasource_id and not payload.database_dialect:\n try:\n env_id = (payload.environment_id or payload.source_dialect or job.environment_id or job.source_dialect)\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n job.database_dialect = detected_dialect\n except Exception as e:\n log.explore(\"Dialect re-detection failed\", error=str(e))\n\n job.updated_at = datetime.now(timezone.utc)\n\n # Update dictionary associations if provided\n if dict_ids is not None:\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n for dict_id in dict_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job_id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n log.reflect(\"Job updated\", payload={\"job_id\": job_id})\n return job\n # #endregion update_job\n\n # #region delete_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,delete]\n # @BRIEF Delete a translation job and its dictionary associations.\n # @PRE job_id must exist.\n # @POST Job and all related TranslationJobDictionary records are deleted.\n # @SIDE_EFFECT Deletes TranslationJob and TranslationJobDictionary rows.\n def delete_job(self, job_id: str) -> None:\n log.reason(\"Deleting translation job\", payload={\"job_id\": job_id})\n job = self.get_job(job_id)\n\n # Delete dictionary associations\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n\n self.db.delete(job)\n self.db.commit()\n log.reflect(\"Job deleted\", payload={\"job_id\": job_id})\n # #endregion delete_job\n\n # #region duplicate_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,duplicate]\n # @BRIEF Duplicate a translation job with a new name, copying all fields and dictionary associations.\n # @PRE job_id must exist.\n # @POST Returns the duplicated TranslationJob with status DRAFT.\n # @SIDE_EFFECT Creates new TranslationJob and TranslationJobDictionary rows.\n def duplicate_job(self, job_id: str, new_name: Optional[str] = None) -> TranslationJob:\n log.reason(\"Duplicating translation job\", payload={\"job_id\": job_id, \"new_name\": new_name})\n source = self.get_job(job_id)\n\n # Copy all fields except id, created_at, updated_at, status\n new_job = TranslationJob(\n id=str(uuid.uuid4()),\n name=new_name or f\"{source.name} (Copy)\",\n description=source.description,\n source_dialect=source.source_dialect,\n target_dialect=source.target_dialect,\n database_dialect=source.database_dialect,\n source_datasource_id=source.source_datasource_id,\n source_table=source.source_table,\n target_schema=source.target_schema,\n target_table=source.target_table,\n source_key_cols=source.source_key_cols,\n target_key_cols=source.target_key_cols,\n translation_column=source.translation_column,\n context_columns=source.context_columns,\n target_language=source.target_language,\n provider_id=source.provider_id,\n batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy,\n environment_id=source.environment_id,\n target_database_id=source.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(new_job)\n self.db.flush()\n\n # Copy dictionary associations\n old_dicts = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n for assoc in old_dicts:\n new_assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=new_job.id,\n dictionary_id=assoc.dictionary_id,\n )\n self.db.add(new_assoc)\n\n self.db.commit()\n self.db.refresh(new_job)\n log.reflect(\"Job duplicated\", payload={\"new_job_id\": new_job.id, \"source_job_id\": job_id})\n return new_job\n # #endregion duplicate_job\n\n # #region get_job_dictionary_ids [C:1] [TYPE Function] [SEMANTICS translate,jobs,dictionaries]\n def get_job_dictionary_ids(self, job_id: str) -> List[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n return [a.dictionary_id for a in assocs]\n # #endregion get_job_dictionary_ids\n\n # #region fetch_available_datasources [C:2] [TYPE Function] [SEMANTICS translate,datasources,query]\n # @BRIEF List available Superset datasets for translation job creation with optional search filter.\n def fetch_available_datasources(self, env_id: str, search: Optional[str] = None) -> list:\n \"\"\"List Superset datasets available for translation.\"\"\"\n from ...core.superset_client import SupersetClient\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment '{env_id}' not found\")\n client = SupersetClient(env)\n _, datasets = client.get_datasets()\n result = []\n for ds in datasets:\n name = ds.get(\"table_name\", \"\")\n if search and search.lower() not in name.lower():\n continue\n db_info = ds.get(\"database\", {})\n backend = db_info.get(\"backend\", \"\")\n dialect = _extract_dialect(backend)\n result.append({\n \"id\": ds.get(\"id\"),\n \"table_name\": name,\n \"schema\": ds.get(\"schema\"),\n \"database_name\": db_info.get(\"database_name\", \"Unknown\"),\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # #endregion fetch_available_datasources\n# #endregion TranslateJobService\n\n\n# #region _extract_dialect [C:1] [TYPE Function] [SEMANTICS translate,dialect]\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI (e.g. 'postgresql+psycopg2://...').\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n\n\n# #region job_to_response [C:2] [TYPE Function] [SEMANTICS translate,serialization]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n context_columns=job.context_columns or [],\n target_language=job.target_language,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n\n\n# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS translate,datasource,columns]\n# @BRIEF Fetch datasource column metadata from Superset and return structured DatasourceColumnsResponse.\n# @PRE datasource_id is a valid Superset dataset ID.\n# @POST Returns DatasourceColumnsResponse with column metadata and database dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\ndef get_datasource_columns(\n datasource_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> DatasourceColumnsResponse:\n \"\"\"Fetch and return column metadata for a given Superset datasource.\"\"\"\n log.reason(\"Fetching datasource columns\", payload={\"datasource_id\": datasource_id, \"env_id\": env_id})\n\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n log.explore(\"Environment not found\", payload={\"env_id\": env_id}, error=f\"Environment {env_id} not configured\")\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n # Create Superset client\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(datasource_id)\n\n # Extract database dialect\n database_info = dataset_detail.get(\"database\", {})\n dialect = None\n if isinstance(database_info, dict):\n try:\n dialect = get_dialect_from_database(database_info)\n except (ValueError, KeyError):\n dialect = None\n if dialect is None:\n database_id = dataset_detail.get(\"database_id\")\n if database_id:\n db_record = client.get_database(int(database_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n log.explore(\"Could not determine dialect\", payload={\"datasource_id\": datasource_id}, error=f\"Could not determine dialect for datasource {datasource_id}\")\n raise ValueError(\"Could not determine database dialect for this datasource\")\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append(DatasourceColumnResponse(\n name=str(col_name),\n type=col.get(\"type\"),\n is_physical=col.get(\"is_physical\", True),\n is_dttm=col.get(\"is_dttm\", False),\n description=col.get(\"description\", \"\"),\n ))\n\n result = DatasourceColumnsResponse(\n datasource_id=datasource_id,\n datasource_name=dataset_detail.get(\"table_name\"),\n schema_name=dataset_detail.get(\"schema\"),\n database_dialect=dialect,\n columns=columns,\n )\n log.reflect(\"Datasource columns fetched\", payload={\n \"datasource_id\": datasource_id,\n \"columns_count\": len(columns),\n \"dialect\": dialect,\n })\n return result\n# #endregion get_datasource_columns\n\n# #endregion TranslateJobService\n" }, @@ -67186,29 +69064,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Extract normalized dialect string from a Superset database record and validate it is supported.", "COMPLEXITY": 4, "POST": "Returns normalized dialect string or raises ValueError if unsupported.", "PRE": "database_record is a dict from Superset API with 'backend' or 'engine' key.", + "PURPOSE": "Extract normalized dialect string from a Superset database record and validate it is supported.", "SIDE_EFFECT": "None — pure data extraction." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67231,29 +69094,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch datasource columns and database dialect from Superset for a given dataset.", "COMPLEXITY": 4, "POST": "Returns (columns_list, dialect_string) or raises on failure.", "PRE": "dataset_id is a valid Superset dataset ID and environment has valid credentials.", + "PURPOSE": "Fetch datasource columns and database dialect from Superset for a given dataset.", "SIDE_EFFECT": "Queries Superset API for dataset detail and database info." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67276,27 +69124,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Identify virtual (calculated) columns from column metadata by filtering non-physical entries.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Identify virtual (calculated) columns from column metadata by filtering non-physical entries." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region detect_virtual_columns [C:2] [TYPE Function] [SEMANTICS translate,columns]\n# @BRIEF Identify virtual (calculated) columns from column metadata by filtering non-physical entries.\ndef detect_virtual_columns(columns: List[Dict[str, Any]]) -> List[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n" }, @@ -67312,17 +69144,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region get_job_dictionary_ids [C:1] [TYPE Function] [SEMANTICS translate,jobs,dictionaries]\n def get_job_dictionary_ids(self, job_id: str) -> List[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n return [a.dictionary_id for a in assocs]\n # #endregion get_job_dictionary_ids\n" }, @@ -67335,27 +69157,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "List available Superset datasets for translation job creation with optional search filter.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "List available Superset datasets for translation job creation with optional search filter." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region fetch_available_datasources [C:2] [TYPE Function] [SEMANTICS translate,datasources,query]\n # @BRIEF List available Superset datasets for translation job creation with optional search filter.\n def fetch_available_datasources(self, env_id: str, search: Optional[str] = None) -> list:\n \"\"\"List Superset datasets available for translation.\"\"\"\n from ...core.superset_client import SupersetClient\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment '{env_id}' not found\")\n client = SupersetClient(env)\n _, datasets = client.get_datasets()\n result = []\n for ds in datasets:\n name = ds.get(\"table_name\", \"\")\n if search and search.lower() not in name.lower():\n continue\n db_info = ds.get(\"database\", {})\n backend = db_info.get(\"backend\", \"\")\n dialect = _extract_dialect(backend)\n result.append({\n \"id\": ds.get(\"id\"),\n \"table_name\": name,\n \"schema\": ds.get(\"schema\"),\n \"database_name\": db_info.get(\"database_name\", \"Unknown\"),\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # #endregion fetch_available_datasources\n" }, @@ -67371,17 +69177,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _extract_dialect [C:1] [TYPE Function] [SEMANTICS translate,dialect]\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI (e.g. 'postgresql+psycopg2://...').\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n" }, @@ -67394,27 +69190,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region job_to_response [C:2] [TYPE Function] [SEMANTICS translate,serialization]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n context_columns=job.context_columns or [],\n target_language=job.target_language,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n" }, @@ -67427,11 +69207,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Dialect-aware safe SQL generation for INSERT/UPSERT operations with identifier quoting and value encoding.", "COMPLEXITY": 4, "LAYER": "Domain", "POST": "Returns safe SQL strings for the target dialect.", - "PRE": "Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS." + "PRE": "Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.", + "PURPOSE": "Dialect-aware safe SQL generation for INSERT/UPSERT operations with identifier quoting and value encoding." }, "relations": [ { @@ -67448,21 +69228,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -67485,27 +69250,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _normalize_timestamp_value [C:2] [TYPE Function] [SEMANTICS translate,sql,timestamp]\n# @BRIEF Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.\ndef _normalize_timestamp_value(value: Any) -> Optional[str]:\n \"\"\"Detect Unix timestamp values and convert to date string.\n\n Handles:\n - Integer/float Unix timestamps (seconds or milliseconds)\n - String representations of Unix timestamps (e.g. '1726358400000.0')\n\n Returns 'YYYY-MM-DD' if conversion succeeds, None if value is not a timestamp.\n \"\"\"\n # Try numeric conversion first\n try:\n ts = float(value)\n except (ValueError, TypeError):\n return None\n\n # Heuristic: Unix timestamps in seconds are ~10 digits (1e9 range for 2001-2033)\n # Unix timestamps in milliseconds are ~13 digits (1e12 range for 2001-2033)\n if 1e9 <= ts < 1e12:\n # Already in seconds\n pass\n elif 1e12 <= ts < 1e15:\n # Milliseconds — convert to seconds\n ts = ts / 1000.0\n else:\n # Not a plausible Unix timestamp\n return None\n\n try:\n dt = datetime.fromtimestamp(ts, tz=timezone.utc)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n" }, @@ -67518,27 +69267,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _quote_identifier [C:2] [TYPE Function] [SEMANTICS translate,sql,quoting]\n# @BRIEF Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse.\ndef _quote_identifier(identifier: str, dialect: str) -> str:\n \"\"\"Quote a SQL identifier per dialect rules.\"\"\"\n if not identifier:\n return identifier\n # Remove any existing quotes to avoid double-quoting\n cleaned = identifier.strip().strip('\"').strip('`').strip('[]')\n if dialect in POSTGRESQL_DIALECTS:\n return f'\"{cleaned}\"'\n elif dialect in CLICKHOUSE_DIALECTS:\n return f\"`{cleaned}`\"\n else:\n # Generic ANSI double-quote\n return f'\"{cleaned}\"'\n# #endregion _quote_identifier\n" }, @@ -67551,27 +69284,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _encode_sql_value [C:2] [TYPE Function] [SEMANTICS translate,sql,encoding]\n# @BRIEF Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.\ndef _encode_sql_value(value: Any, dialect: Optional[str] = None) -> str:\n \"\"\"Encode a Python value into a SQL-safe literal.\n\n For ClickHouse dialect, attempts to detect Unix timestamp strings\n and convert them to 'YYYY-MM-DD' format for Date column compatibility.\n \"\"\"\n if value is None:\n return \"NULL\"\n if isinstance(value, bool):\n return \"TRUE\" if value else \"FALSE\"\n\n # For ClickHouse: try to normalize timestamp-like string values\n if dialect in CLICKHOUSE_DIALECTS and isinstance(value, str) and value:\n normalized = _normalize_timestamp_value(value)\n if normalized:\n return f\"'{normalized}'\"\n\n if isinstance(value, (int, float)):\n return str(value)\n # String — escape single quotes by doubling them\n escaped = str(value).replace(\"'\", \"''\")\n return f\"'{escaped}'\"\n# #endregion _encode_sql_value\n" }, @@ -67584,27 +69301,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _build_values_clause [C:2] [TYPE Function] [SEMANTICS translate,sql,values]\n# @BRIEF Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.\ndef _build_values_clause(columns: List[str], rows: List[Dict[str, Any]], dialect: Optional[str] = None) -> str:\n \"\"\"Build VALUES (...) clause for multiple rows.\n\n NOTE: columns may be quoted (e.g. '\"col\"' or '`col`') for the SQL column list,\n but row dicts have UNQUOTED keys. Strip quotes before value lookup.\n \"\"\"\n value_groups = []\n for row in rows:\n values = []\n for col in columns:\n # Strip quoting for value lookup — row keys are unquoted\n lookup_key = col.strip('\"').strip('`').strip('[]')\n val = row.get(lookup_key)\n values.append(_encode_sql_value(val, dialect=dialect))\n value_groups.append(f\"({', '.join(values)})\")\n return \",\\n\".join(value_groups)\n# #endregion _build_values_clause\n" }, @@ -67617,8 +69318,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows." }, "relations": [ { @@ -67634,23 +69335,7 @@ "target_ref": "[_build_values_clause]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region generate_insert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,insert]\n# @BRIEF Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_insert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate a plain INSERT SQL.\"\"\"\n with belief_scope(\"generate_insert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for INSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for INSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for INSERT SQL generation\")\n\n col_list = \", \".join(columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n sql = f\"INSERT INTO {table_ref} ({col_list})\\nVALUES\\n{values};\"\n return sql\n# #endregion generate_insert_sql\n" }, @@ -67663,8 +69348,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING." }, "relations": [ { @@ -67680,23 +69365,7 @@ "target_ref": "[_build_values_clause]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region generate_upsert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,upsert]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_upsert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n key_columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate INSERT ... ON CONFLICT (key_cols) DO UPDATE SET ... SQL.\"\"\"\n with belief_scope(\"generate_upsert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for UPSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for UPSERT SQL generation\")\n if not key_columns:\n raise ValueError(\"key_columns are required for UPSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for UPSERT SQL generation\")\n\n col_list = \", \".join(columns)\n key_list = \", \".join(key_columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n # Build SET clause: exclude key columns from update\n update_cols = [c for c in columns if c not in key_columns]\n if not update_cols:\n # If only key columns, use DO NOTHING\n conflict_action = \"DO NOTHING\"\n else:\n set_parts = [f\"{col} = EXCLUDED.{col}\" for col in update_cols]\n conflict_action = \"DO UPDATE SET\\n\" + \",\\n\".join(set_parts)\n\n sql = (\n f\"INSERT INTO {table_ref} ({col_list})\\n\"\n f\"VALUES\\n\"\n f\"{values}\\n\"\n f\"ON CONFLICT ({key_list}) {conflict_action};\"\n )\n return sql\n# #endregion generate_upsert_sql\n" }, @@ -67709,29 +69378,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Generate SQL for a set of rows, detecting dialect from the job configuration.", "COMPLEXITY": 4, "POST": "Returns tuple of (sql_string, statement_count).", "PRE": "dialect is a supported database dialect. columns list is non-empty. rows is non-empty.", + "PURPOSE": "Generate SQL for a set of rows, detecting dialect from the job configuration.", "SIDE_EFFECT": "None — pure SQL generation." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67754,8 +69408,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Generate separate INSERT statements for each row chunk (batch-safe version).", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Generate separate INSERT statements for each row chunk (batch-safe version)." }, "relations": [ { @@ -67765,23 +69419,7 @@ "target_ref": "[SQLGenerator.generate]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region SQLGenerator.generate_batch [C:3] [TYPE Function] [SEMANTICS translate,sql,batch]\n # @BRIEF Generate separate INSERT statements for each row chunk (batch-safe version).\n # @RELATION DEPENDS_ON -> [SQLGenerator.generate]\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n key_columns: Optional[List[str]] = None,\n upsert_strategy: str = \"MERGE\",\n max_rows_per_statement: int = 500,\n ) -> List[Tuple[str, int]]:\n \"\"\"Generate SQL in batches, splitting large row sets into multiple statements.\n\n Returns:\n List of (sql_string, row_count) tuples.\n \"\"\"\n with belief_scope(\"SQLGenerator.generate_batch\"):\n if not rows:\n return []\n\n statements = []\n for i in range(0, len(rows), max_rows_per_statement):\n chunk = rows[i:i + max_rows_per_statement]\n sql, count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=target_schema,\n target_table=target_table,\n columns=columns,\n rows=chunk,\n key_columns=key_columns,\n upsert_strategy=upsert_strategy,\n )\n statements.append((sql, count))\n\n return statements\n # #endregion SQLGenerator.generate_batch\n" }, @@ -67794,13 +69432,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Submit SQL to Superset SQL Lab API and poll execution status with retry.", "COMPLEXITY": 5, "DATA_CONTRACT": "Input[sql:str, database_id:Optional] -> Output[Dict with query_id, status, error_message]", "INVARIANT": "Polling respects max_polls limit; timeout returns structured error.", "LAYER": "Infra", "POST": "SQL is submitted to Superset SQL Lab; execution reference is returned.", "PRE": "Valid Superset environment configuration and authenticated client.", + "PURPOSE": "Submit SQL to Superset SQL Lab API and poll execution status with retry.", "RATIONALE": "Direct SQL Lab API submission provides audit trail and execution monitoring within Superset.", "REJECTED": "Direct database connection bypass — would skip Superset's SQL Lab audit and RBAC.", "SIDE_EFFECT": "Makes HTTP calls to Superset /api/v1/sqllab/execute/; polls status." @@ -67819,23 +69457,7 @@ "target_ref": "[ConfigManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region SupersetSqlLabExecutor [C:5] [TYPE Module] [SEMANTICS translate,superset,sqllab,execute]\n# @BRIEF Submit SQL to Superset SQL Lab API and poll execution status with retry.\n# @LAYER Infra\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Valid Superset environment configuration and authenticated client.\n# @POST SQL is submitted to Superset SQL Lab; execution reference is returned.\n# @SIDE_EFFECT Makes HTTP calls to Superset /api/v1/sqllab/execute/; polls status.\n# @DATA_CONTRACT Input[sql:str, database_id:Optional] -> Output[Dict with query_id, status, error_message]\n# @INVARIANT Polling respects max_polls limit; timeout returns structured error.\n# @RATIONALE Direct SQL Lab API submission provides audit trail and execution monitoring within Superset.\n# @REJECTED Direct database connection bypass — would skip Superset's SQL Lab audit and RBAC.\n\nfrom typing import Any, Dict, List, Optional, Tuple, Union\nfrom datetime import datetime, timezone\nimport json\nimport time\nimport uuid\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...core.superset_client import SupersetClient\n\nlog = MarkerLogger(\"SupersetExecutor\")\n\n# #region SupersetSqlLabExecutor [C:5] [TYPE Class] [SEMANTICS translate,superset,sqllab]\n# @BRIEF Submit SQL to Superset SQL Lab API with polling and status tracking.\n# @PRE Valid environment ID and ConfigManager.\n# @POST SQL submitted to Superset; execution reference recorded; polling completes.\n# @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/; polls GET for status.\n# @DATA_CONTRACT Input[env_id:str, database_id:Optional] -> Output[Dict with query_id, status]\n# @INVARIANT database_id resolved lazily; polling capped at max_polls.\nclass SupersetSqlLabExecutor:\n\n def __init__(self, config_manager: ConfigManager, env_id: str, database_id: Optional[int] = None):\n self.config_manager = config_manager\n self.env_id = env_id\n self._client: Optional[SupersetClient] = None\n self._database_id: Optional[int] = database_id\n\n # #region _get_client [C:4] [TYPE Function] [SEMANTICS translate,superset,client]\n # @BRIEF Lazy-initialize SupersetClient for the configured environment.\n # @PRE env_id must correspond to a valid environment config.\n # @POST Returns authenticated SupersetClient.\n # @SIDE_EFFECT Authenticates against Superset API on first call.\n def _get_client(self) -> SupersetClient:\n with belief_scope(\"SupersetSqlLabExecutor._get_client\"):\n if self._client is not None:\n return self._client\n\n environments = self.config_manager.get_environments()\n env_config = next((e for e in environments if e.id == self.env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{self.env_id}' not found\")\n\n self._client = SupersetClient(env_config)\n return self._client\n # #endregion _get_client\n\n # #region resolve_database_id [C:4] [TYPE Function] [SEMANTICS translate,database,resolve]\n # @BRIEF Resolve the target database ID from the environment by name or default.\n # @PRE Superset environment is reachable and has databases.\n # @POST Returns database_id integer or raises ValueError.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_id(self, database_name: Optional[str] = None) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n if database_name:\n for db in databases:\n if db.get(\"database_name\", \"\").lower() == database_name.lower():\n self._database_id = db[\"id\"]\n log.reason(\"Resolved database ID by name\", payload={\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n })\n return self._database_id\n raise ValueError(\n f\"Database '{database_name}' not found in Superset environment\"\n )\n\n # Default: use first database\n self._database_id = databases[0][\"id\"]\n log.reason(\"Using default database\", payload={\n \"database_name\": databases[0].get(\"database_name\"),\n \"database_id\": self._database_id,\n })\n return self._database_id\n # #endregion resolve_database_id\n\n # #region resolve_database_uuid [C:4] [TYPE Function] [SEMANTICS translate,database,uuid]\n # @BRIEF Resolve a database UUID to a numeric database ID from Superset.\n # @PRE uuid_str is a valid Superset database UUID.\n # @POST Returns numeric database_id or raises ValueError if not found.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_uuid(self, uuid_str: str) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_uuid\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"uuid\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n for db in databases:\n db_uuid = db.get(\"uuid\") or \"\"\n if db_uuid.lower() == uuid_str.lower():\n db_id = db[\"id\"]\n self._database_id = db_id\n log.reason(\"Resolved database ID by UUID\", payload={\n \"uuid\": uuid_str,\n \"database_id\": db_id,\n \"database_name\": db.get(\"database_name\"),\n })\n return db_id\n\n raise ValueError(\n f\"Database with UUID '{uuid_str}' not found in Superset environment\"\n )\n # #endregion resolve_database_uuid\n\n # #region execute_sql [C:4] [TYPE Function] [SEMANTICS translate,sql,execute]\n # @BRIEF Submit SQL to Superset SQL Lab and return execution tracking info.\n # @PRE sql is valid SQL string. database_id is a valid Superset DB ID.\n # @POST Returns execution result dict with query_id and status.\n # @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/.\n def execute_sql(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_sql\"):\n client = self._get_client()\n db_id = database_id or self._database_id\n if db_id is None:\n db_id = self.resolve_database_id()\n # Handle UUID string passed as database_id — resolve to numeric ID\n if isinstance(db_id, str):\n try:\n db_id = int(db_id)\n except (ValueError, TypeError):\n db_id = self.resolve_database_uuid(db_id)\n\n log.reason(\"Submitting SQL to Superset SQL Lab\", payload={\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"client_id\": uuid.uuid4().hex[:10],\n \"schema\": None,\n }\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/sqllab/execute/\",\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"SQL Lab execute failed\", error=str(e))\n raise ValueError(f\"Superset SQL Lab execute failed: {e}\")\n\n # Parse response — try multiple formats\n result = response if isinstance(response, dict) else {}\n query_id = (result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"sql_lab_session_ref\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\"))\n status = result.get(\"status\", \"unknown\")\n\n log.reason(\"SQL Lab execute response\", payload={\n \"query_id\": query_id,\n \"status\": status,\n \"has_query_id\": query_id is not None,\n \"has_errors\": \"errors\" in result or \"error\" in result,\n \"response_keys\": list(result.keys()) if isinstance(result, dict) else type(result).__name__,\n \"response_preview\": str(result)[:1000] if isinstance(result, dict) else str(result)[:1000],\n })\n\n return {\n \"query_id\": query_id,\n \"status\": status,\n \"raw_response\": result,\n \"database_id\": db_id,\n }\n # #endregion execute_sql\n\n # #region poll_execution_status [C:4] [TYPE Function] [SEMANTICS translate,poll,status]\n # @BRIEF Poll Superset for SQL execution status until completion or timeout.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns final execution status dict with success/failure/timeout.\n # @SIDE_EFFECT Makes HTTP GET requests to Superset API.\n def poll_execution_status(\n self,\n query_id: str,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.poll_execution_status\"):\n client = self._get_client()\n\n log.reason(\"Polling SQL execution status\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n \"interval\": poll_interval_seconds,\n })\n\n for attempt in range(max_polls):\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}\",\n )\n result = response if isinstance(response, dict) else {}\n status = result.get(\"status\", \"unknown\")\n state = result.get(\"state\", result.get(\"status\", \"\"))\n\n # Terminal states\n if state in (\"success\", \"finished\", \"completed\"):\n log.reason(\"SQL execution completed\", payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"rows_affected\": result.get(\"rows\"),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"success\",\n \"state\": state,\n \"rows_affected\": result.get(\"rows\"),\n \"error_message\": result.get(\"error_message\"),\n \"results\": result.get(\"results\"),\n \"completed_on\": result.get(\"completed_on\"),\n }\n\n if state in (\"failed\", \"error\", \"stopped\"):\n error_msg = result.get(\"error_message\", \"Unknown error\")\n log.explore(\"SQL execution failed\", error=\"SQL query execution returned failed/error state\",\n payload={\n \"query_id\": query_id,\n \"state\": state,\n \"error\": error_msg,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"state\": state,\n \"error_message\": error_msg,\n \"results\": None,\n }\n\n if state in (\"pending\", \"running\", \"started\"):\n time.sleep(poll_interval_seconds)\n continue\n\n # Unknown state — treat as still running\n time.sleep(poll_interval_seconds)\n\n except Exception as e:\n log.explore(\"Polling error, retrying\", error=\"Polling encountered error, will retry\",\n payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"error\": str(e),\n })\n time.sleep(poll_interval_seconds)\n continue\n\n # Timeout\n log.explore(\"SQL execution polling timed out\", error=\"Polling timed out\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"timeout\",\n \"error_message\": f\"Polling timed out after {max_polls} attempts\",\n \"results\": None,\n }\n # #endregion poll_execution_status\n\n # #region execute_and_poll [C:4] [TYPE Function] [SEMANTICS translate,sql,execute,poll]\n # @BRIEF Execute SQL and wait for completion. One-shot convenience method.\n # @PRE sql is valid SQL.\n # @POST Returns final execution result.\n # @SIDE_EFFECT Makes HTTP calls to Superset API.\n def execute_and_poll(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_and_poll\"):\n exec_result = self.execute_sql(\n sql=sql,\n database_id=database_id,\n )\n\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n log.explore(\"No query_id from SQL Lab execute\", error=\"No query_id returned\", payload={\n \"raw_response\": exec_result.get(\"raw_response\"),\n })\n return {\n \"status\": \"failed\",\n \"error_message\": \"No query_id returned from SQL Lab\",\n \"query_id\": None,\n }\n\n return self.poll_execution_status(\n query_id=str(query_id),\n max_polls=max_polls,\n poll_interval_seconds=poll_interval_seconds,\n )\n # #endregion execute_and_poll\n\n # #region get_query_results [C:4] [TYPE Function] [SEMANTICS translate,query,results]\n # @BRIEF Fetch the results of a completed query from Superset.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns query results if available, or error dict.\n # @SIDE_EFFECT Makes HTTP GET to Superset API.\n def get_query_results(self, query_id: str) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.get_query_results\"):\n client = self._get_client()\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}/results\",\n )\n result = response if isinstance(response, dict) else {}\n return {\n \"query_id\": query_id,\n \"status\": \"success\" if result.get(\"results\") else \"no_results\",\n \"results\": result.get(\"results\"),\n }\n except Exception as e:\n log.explore(\"Failed to fetch query results\", error=\"Failed to fetch query results from Superset\",\n payload={\n \"query_id\": query_id,\n \"error\": str(e),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"error_message\": str(e),\n \"results\": None,\n }\n # #endregion get_query_results\n\n\n# #endregion SupersetSqlLabExecutor\n# #endregion SupersetSqlLabExecutor\n" }, @@ -67848,29 +69470,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Resolve the target database ID from the environment by name or default.", "COMPLEXITY": 4, "POST": "Returns database_id integer or raises ValueError.", "PRE": "Superset environment is reachable and has databases.", + "PURPOSE": "Resolve the target database ID from the environment by name or default.", "SIDE_EFFECT": "Fetches databases list from Superset API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67893,29 +69500,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Resolve a database UUID to a numeric database ID from Superset.", "COMPLEXITY": 4, "POST": "Returns numeric database_id or raises ValueError if not found.", "PRE": "uuid_str is a valid Superset database UUID.", + "PURPOSE": "Resolve a database UUID to a numeric database ID from Superset.", "SIDE_EFFECT": "Fetches databases list from Superset API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67938,29 +69530,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Submit SQL to Superset SQL Lab and return execution tracking info.", "COMPLEXITY": 4, "POST": "Returns execution result dict with query_id and status.", "PRE": "sql is valid SQL string. database_id is a valid Superset DB ID.", + "PURPOSE": "Submit SQL to Superset SQL Lab and return execution tracking info.", "SIDE_EFFECT": "Makes HTTP POST to /api/v1/sqllab/execute/." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -67983,29 +69560,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Poll Superset for SQL execution status until completion or timeout.", "COMPLEXITY": 4, "POST": "Returns final execution status dict with success/failure/timeout.", "PRE": "query_id is a valid Superset query ID.", + "PURPOSE": "Poll Superset for SQL execution status until completion or timeout.", "SIDE_EFFECT": "Makes HTTP GET requests to Superset API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -68028,29 +69590,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Execute SQL and wait for completion. One-shot convenience method.", "COMPLEXITY": 4, "POST": "Returns final execution result.", "PRE": "sql is valid SQL.", + "PURPOSE": "Execute SQL and wait for completion. One-shot convenience method.", "SIDE_EFFECT": "Makes HTTP calls to Superset API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -68073,29 +69620,14 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Fetch the results of a completed query from Superset.", "COMPLEXITY": 4, "POST": "Returns query results if available, or error dict.", "PRE": "query_id is a valid Superset query ID.", + "PURPOSE": "Fetch the results of a completed query from Superset.", "SIDE_EFFECT": "Makes HTTP GET to Superset API." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -68133,7 +69665,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -68159,7 +69693,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Regression tests for settings and health schema contracts updated in 026 fix batch." }, "relations": [ @@ -68171,15 +69705,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -68192,6 +69717,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -68309,7 +69835,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Sensitive fields like password must not be included in response schemas.", "LAYER": "API", "PURPOSE": "Pydantic schemas for authentication requests and responses.", @@ -68338,20 +69864,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -68366,11 +69878,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents a JWT access token response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:Token:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# [/DEF:Token:Class]\n" }, @@ -68383,11 +69905,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents the data encoded in a JWT token." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:TokenData:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: Optional[str] = None\n scopes: List[str] = []\n\n\n# [/DEF:TokenData:Class]\n" }, @@ -68400,11 +69932,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents a permission in API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PermissionSchema:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: Optional[str] = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:PermissionSchema:Class]\n" }, @@ -68420,7 +69962,17 @@ "PURPOSE": "Represents a role in API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RoleSchema:Class]\n# @PURPOSE: Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n permissions: List[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:RoleSchema:Class]\n" }, @@ -68436,7 +69988,17 @@ "PURPOSE": "Schema for creating a new role." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RoleCreate:Class]\n# @PURPOSE: Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: Optional[str] = None\n permissions: List[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# [/DEF:RoleCreate:Class]\n" }, @@ -68452,7 +70014,17 @@ "PURPOSE": "Schema for updating an existing role." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RoleUpdate:Class]\n# @PURPOSE: Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n permissions: Optional[List[str]] = None\n\n\n# [/DEF:RoleUpdate:Class]\n" }, @@ -68468,7 +70040,17 @@ "PURPOSE": "Represents an AD Group to Role mapping in API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ADGroupMappingSchema:Class]\n# @PURPOSE: Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ADGroupMappingSchema:Class]\n" }, @@ -68484,7 +70066,17 @@ "PURPOSE": "Schema for creating an AD Group mapping." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ADGroupMappingCreate:Class]\n# @PURPOSE: Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n\n# [/DEF:ADGroupMappingCreate:Class]\n" }, @@ -68500,7 +70092,17 @@ "PURPOSE": "Base schema for user data." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:UserBase:Class]\n# @PURPOSE: Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: Optional[EmailStr] = None\n is_active: bool = True\n\n\n# [/DEF:UserBase:Class]\n" }, @@ -68516,7 +70118,17 @@ "PURPOSE": "Schema for creating a new user." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:UserCreate:Class]\n# @PURPOSE: Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str\n roles: List[str] = []\n\n\n# [/DEF:UserCreate:Class]\n" }, @@ -68532,7 +70144,17 @@ "PURPOSE": "Schema for updating an existing user." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:UserUpdate:Class]\n# @PURPOSE: Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: Optional[EmailStr] = None\n password: Optional[str] = None\n is_active: Optional[bool] = None\n roles: Optional[List[str]] = None\n\n\n# [/DEF:UserUpdate:Class]\n" }, @@ -68548,7 +70170,17 @@ "PURPOSE": "Schema for user data in API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:User:Class]\n# @PURPOSE: Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: Optional[datetime] = None\n roles: List[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:User:Class]\n" }, @@ -68561,7 +70193,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "Thin facade re-exporting all dataset review API schemas from decomposed sub-modules.", "RATIONALE": "Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules.", @@ -68578,17 +70210,21 @@ "relations": [], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C2", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 2, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" } } ], @@ -68604,7 +70240,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses." }, @@ -68617,20 +70253,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -68653,11 +70275,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Clarification option DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationOptionDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification option DTO.\nclass ClarificationOptionDto(BaseModel):\n option_id: str\n question_id: str\n label: str\n value: str\n is_recommended: bool\n display_order: int\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationOptionDto:Class]\n" }, @@ -68670,11 +70302,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Clarification answer DTO with feedback." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationAnswerDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n answered_by_user_id: str\n impact_summary: Optional[str] = None\n user_feedback: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationAnswerDto:Class]\n" }, @@ -68687,7 +70329,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Clarification question DTO with nested options and answer." }, "relations": [], @@ -68704,7 +70346,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Clarification session DTO with nested questions." }, "relations": [], @@ -68721,11 +70363,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Compiled preview DTO with fingerprint and session version." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CompiledPreviewDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: Optional[int] = None\n preview_status: PreviewStatus\n compiled_sql: Optional[str] = None\n preview_fingerprint: str\n compiled_by: str\n error_code: Optional[str] = None\n error_details: Optional[str] = None\n compiled_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:CompiledPreviewDto:Class]\n" }, @@ -68738,11 +70390,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Run context DTO with launch audit data and session version." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DatasetRunContextDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Run context DTO with launch audit data and session version.\nclass DatasetRunContextDto(BaseModel):\n run_context_id: str\n session_id: str\n session_version: Optional[int] = None\n dataset_ref: str\n environment_id: str\n preview_id: str\n sql_lab_session_ref: str\n effective_filters: Any\n template_params: Any\n approved_mapping_ids: List[str]\n semantic_decision_refs: List[str]\n open_warning_refs: List[str]\n launch_status: LaunchStatus\n launch_error: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetRunContextDto:Class]\n" }, @@ -68755,7 +70417,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Lightweight session summary DTO for list responses." }, "relations": [], @@ -68772,7 +70434,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Full session detail DTO with all nested aggregates for detail views." }, "relations": [ @@ -68806,7 +70468,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads." }, @@ -68819,20 +70481,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -68855,11 +70503,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Collaborator DTO for session access control." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SessionCollaboratorDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SessionCollaboratorDto:Class]\n" }, @@ -68872,11 +70530,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Dataset profile DTO with business summary and confidence metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DatasetProfileDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Dataset profile DTO with business summary and confidence metadata.\nclass DatasetProfileDto(BaseModel):\n profile_id: str\n session_id: str\n dataset_name: str\n schema_name: Optional[str] = None\n database_name: Optional[str] = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: Optional[str] = None\n dataset_type: Optional[str] = None\n is_sqllab_view: bool\n completeness_score: Optional[float] = None\n confidence_state: ConfidenceState\n has_blocking_findings: bool\n has_warning_findings: bool\n manual_summary_locked: bool\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetProfileDto:Class]\n" }, @@ -68889,11 +70557,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Validation finding DTO with resolution tracking." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationFindingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Validation finding DTO with resolution tracking.\nclass ValidationFindingDto(BaseModel):\n finding_id: str\n session_id: str\n area: FindingArea\n severity: FindingSeverity\n code: str\n title: str\n message: str\n resolution_state: ResolutionState\n resolution_note: Optional[str] = None\n caused_by_ref: Optional[str] = None\n created_at: datetime\n resolved_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationFindingDto:Class]\n" }, @@ -68906,11 +70584,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Semantic source DTO with trust level and status." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SemanticSourceDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic source DTO with trust level and status.\nclass SemanticSourceDto(BaseModel):\n source_id: str\n session_id: str\n source_type: SemanticSourceType\n source_ref: str\n source_version: str\n display_name: str\n trust_level: TrustLevel\n schema_overlap_score: Optional[float] = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticSourceDto:Class]\n" }, @@ -68923,11 +70611,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Semantic candidate DTO with match type and confidence score." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SemanticCandidateDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: Optional[str] = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: Optional[str] = None\n proposed_description: Optional[str] = None\n proposed_display_format: Optional[str] = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticCandidateDto:Class]\n" }, @@ -68940,7 +70638,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Semantic field entry DTO with nested candidates and session version." }, "relations": [], @@ -68957,11 +70655,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Imported filter DTO with confidence and recovery status." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ImportedFilterDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Imported filter DTO with confidence and recovery status.\nclass ImportedFilterDto(BaseModel):\n filter_id: str\n session_id: str\n filter_name: str\n display_name: Optional[str] = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Optional[Any] = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: Optional[str] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ImportedFilterDto:Class]\n" }, @@ -68974,11 +70682,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Template variable DTO with mapping status." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:TemplateVariableDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Template variable DTO with mapping status.\nclass TemplateVariableDto(BaseModel):\n variable_id: str\n session_id: str\n variable_name: str\n expression_source: str\n variable_kind: VariableKind\n is_required: bool\n default_value: Optional[Any] = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:TemplateVariableDto:Class]\n" }, @@ -68991,11 +70709,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Execution mapping DTO with approval state and session version." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ExecutionMappingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: Optional[int] = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Optional[Any] = None\n transformation_note: Optional[str] = None\n warning_level: Optional[MappingWarningLevel] = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: Optional[str] = None\n approved_at: Optional[datetime] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ExecutionMappingDto:Class]\n" }, @@ -69008,7 +70736,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Pydantic schemas for dashboard health summary.", "SEMANTICS": [ @@ -69041,7 +70769,17 @@ "PURPOSE": "Represents the latest health status of a single dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DashboardHealthItem:Class]\n# @PURPOSE: Represents the latest health status of a single dashboard.\nclass DashboardHealthItem(BaseModel):\n record_id: Optional[str] = None\n dashboard_id: str\n dashboard_slug: Optional[str] = None\n dashboard_title: Optional[str] = None\n environment_id: str\n status: str = Field(..., pattern=\"^(PASS|WARN|FAIL|UNKNOWN)$\")\n last_check: datetime\n task_id: Optional[str] = None\n summary: Optional[str] = None\n\n\n# [/DEF:DashboardHealthItem:Class]\n" }, @@ -69057,7 +70795,17 @@ "PURPOSE": "Aggregated health summary for all dashboards." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:HealthSummaryResponse:Class]\n# @PURPOSE: Aggregated health summary for all dashboards.\nclass HealthSummaryResponse(BaseModel):\n items: List[DashboardHealthItem]\n pass_count: int\n warn_count: int\n fail_count: int\n unknown_count: int\n\n\n# [/DEF:HealthSummaryResponse:Class]\n" }, @@ -69070,7 +70818,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Schema shapes stay stable for profile UI states and backend preference contracts.", "LAYER": "API", "PURPOSE": "Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.", @@ -69103,20 +70851,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -69131,7 +70865,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Represents one permission badge state for profile read-only security view." }, "relations": [ @@ -69155,7 +70889,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Read-only security and access snapshot for current user." }, "relations": [ @@ -69179,7 +70913,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Represents persisted profile preference for a single authenticated user." }, "relations": [ @@ -69203,7 +70937,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Request payload for updating current user's profile settings." }, "relations": [ @@ -69227,7 +70961,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Response envelope for profile preference read/update endpoints." }, "relations": [ @@ -69251,7 +70985,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Query contract for Superset account lookup by selected environment." }, "relations": [ @@ -69275,7 +71009,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Canonical account candidate projected from Superset users payload." }, "relations": [ @@ -69299,7 +71033,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Response envelope for Superset account lookup (success or degraded mode)." }, "relations": [ @@ -69323,7 +71057,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Pydantic schemas for application settings and automation policies.", "SEMANTICS": [ @@ -69357,7 +71091,17 @@ "PURPOSE": "Structured notification channel definition for policy-level custom routing." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:NotificationChannel:Class]\n# @PURPOSE: Structured notification channel definition for policy-level custom routing.\nclass NotificationChannel(BaseModel):\n type: str = Field(\n ..., description=\"Notification channel type (e.g., SLACK, SMTP, TELEGRAM)\"\n )\n target: str = Field(\n ..., description=\"Notification destination (e.g., #alerts, chat id, email)\"\n )\n\n\n# [/DEF:NotificationChannel:Class]\n" }, @@ -69373,7 +71117,17 @@ "PURPOSE": "Base schema for validation policy data." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationPolicyBase:Class]\n# @PURPOSE: Base schema for validation policy data.\nclass ValidationPolicyBase(BaseModel):\n name: str = Field(..., description=\"Name of the policy\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n is_active: bool = Field(True, description=\"Whether the policy is currently active\")\n dashboard_ids: List[str] = Field(\n ..., description=\"List of dashboard IDs to validate\"\n )\n schedule_days: List[int] = Field(\n ..., description=\"Days of the week (0-6, 0=Sunday) to run\"\n )\n window_start: time = Field(..., description=\"Start of the execution window\")\n window_end: time = Field(..., description=\"End of the execution window\")\n notify_owners: bool = Field(\n True, description=\"Whether to notify dashboard owners on failure\"\n )\n custom_channels: Optional[List[NotificationChannel]] = Field(\n None,\n description=\"List of additional structured notification channels\",\n )\n alert_condition: str = Field(\n \"FAIL_ONLY\",\n description=\"Condition to trigger alerts: FAIL_ONLY, WARN_AND_FAIL, ALWAYS\",\n )\n\n\n# [/DEF:ValidationPolicyBase:Class]\n" }, @@ -69389,7 +71143,17 @@ "PURPOSE": "Schema for creating a new validation policy." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationPolicyCreate:Class]\n# @PURPOSE: Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# [/DEF:ValidationPolicyCreate:Class]\n" }, @@ -69405,7 +71169,17 @@ "PURPOSE": "Schema for updating an existing validation policy." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationPolicyUpdate:Class]\n# @PURPOSE: Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: Optional[str] = None\n environment_id: Optional[str] = None\n is_active: Optional[bool] = None\n dashboard_ids: Optional[List[str]] = None\n schedule_days: Optional[List[int]] = None\n window_start: Optional[time] = None\n window_end: Optional[time] = None\n notify_owners: Optional[bool] = None\n custom_channels: Optional[List[NotificationChannel]] = None\n alert_condition: Optional[str] = None\n\n\n# [/DEF:ValidationPolicyUpdate:Class]\n" }, @@ -69421,7 +71195,17 @@ "PURPOSE": "Schema for validation policy response data." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationPolicyResponse:Class]\n# @PURPOSE: Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationPolicyResponse:Class]\n" }, @@ -69434,28 +71218,12 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Pydantic v2 schemas for translation API request/response serialization.", "COMPLEXITY": 2, - "LAYER": "Domain" + "LAYER": "Domain", + "PURPOSE": "Pydantic v2 schemas for translation API request/response serialization." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateSchemas [C:2] [TYPE Module]\n# @BRIEF Pydantic v2 schemas for translation API request/response serialization.\n# @LAYER Domain\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import BaseModel, Field\nimport json\n\n\n# #region TranslateJobCreate [C:1] [TYPE Class]\nclass TranslateJobCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: Optional[str] = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: Optional[str] = Field(None, description=\"Superset datasource ID\")\n source_table: Optional[str] = Field(None, description=\"Source table name\")\n target_schema: Optional[str] = Field(None, description=\"Target table schema\")\n target_table: Optional[str] = Field(None, description=\"Target table name\")\n source_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Target key column names\")\n translation_column: Optional[str] = Field(None, description=\"Column to translate\")\n context_columns: Optional[List[str]] = Field(default_factory=list, description=\"Context column names\")\n target_language: Optional[str] = Field(None, description=\"Target language code\")\n provider_id: Optional[str] = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: Optional[List[str]] = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: Optional[str] = Field(None, description=\"Superset environment ID\")\n target_database_id: Optional[str] = Field(None, description=\"Superset database ID for INSERT via SQL Lab\")\n# #endregion TranslateJobCreate\n\n\n# #region TranslateJobUpdate [C:1] [TYPE Class]\nclass TranslateJobUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n source_dialect: Optional[str] = None\n target_dialect: Optional[str] = None\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: Optional[int] = None\n upsert_strategy: Optional[str] = None\n status: Optional[str] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n# #endregion TranslateJobUpdate\n\n\n# #region TranslateJobResponse [C:1] [TYPE Class]\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n\n\n# #region DatasourceColumnResponse [C:1] [TYPE Class]\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: Optional[str] = None\n is_physical: bool = True\n is_dttm: bool = False\n description: Optional[str] = None\n# #endregion DatasourceColumnResponse\n\n\n# #region DatasourceColumnsResponse [C:1] [TYPE Class]\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: Optional[str] = None\n schema_name: Optional[str] = None\n database_dialect: str\n columns: List[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n\n\n# #region DuplicateJobResponse [C:1] [TYPE Class]\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n\n\n# #region DictionaryCreate [C:1] [TYPE Class]\nclass DictionaryCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n\n\n# #region DictionaryImport [C:1] [TYPE Class]\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: Optional[str] = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n\n\n# #region DictionaryResponse [C:1] [TYPE Class]\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n entry_count: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n\n\n# #region DictionaryEntryCreate [C:1] [TYPE Class]\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: Optional[str] = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n\n\n# #region DictionaryEntryResponse [C:1] [TYPE Class]\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n\n\n# #region DictionaryImportResult [C:1] [TYPE Class]\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: List[Dict[str, Any]] = Field(default_factory=list)\n preview: List[Dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n\n\n# #region PreviewRequest [C:1] [TYPE Class]\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: Optional[str] = Field(None, description=\"Optional custom prompt template\")\n env_id: Optional[str] = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n\n\n# #region PreviewRowUpdate [C:1] [TYPE Class]\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: Optional[str] = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: Optional[str] = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n\n\n# #region PreviewAcceptResponse [C:1] [TYPE Class]\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n\n\n# #region CostEstimate [C:1] [TYPE Class]\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n\n\n# #region TermCorrectionSubmit [C:1] [TYPE Class]\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: Optional[str] = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: Optional[str] = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: Optional[str] = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n\n\n# #region TermCorrectionBulkSubmit [C:1] [TYPE Class]\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: List[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n\n\n# #region CorrectionConflictResult [C:1] [TYPE Class]\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n\n\n# #region CorrectionSubmitResponse [C:1] [TYPE Class]\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: Optional[str] = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: Optional[CorrectionConflictResult] = None\n message: Optional[str] = None\n# #endregion CorrectionSubmitResponse\n\n\n# #region ScheduleConfig [C:1] [TYPE Class]\nclass ScheduleConfig(BaseModel):\n cron_expression: str = Field(..., description=\"Cron expression for scheduling (e.g. '0 2 * * *')\")\n timezone: str = Field(\"UTC\", description=\"Timezone for the cron schedule (e.g. 'UTC', 'Europe/Moscow')\")\n is_active: bool = True\n# #endregion ScheduleConfig\n\n\n# #region ScheduleResponse [C:1] [TYPE Class]\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n last_run_at: Optional[datetime] = None\n next_run_at: Optional[datetime] = None\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n\n\n# #region NextExecutionResponse [C:1] [TYPE Class]\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: List[str] = []\n# #endregion NextExecutionResponse\n\n\n# #region TranslationRunResponse [C:1] [TYPE Class]\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: Optional[str] = None\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n error_message: Optional[str] = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: Optional[str] = None\n superset_execution_id: Optional[str] = None\n config_snapshot: Optional[Dict[str, Any]] = None\n key_hash: Optional[str] = None\n config_hash: Optional[str] = None\n dict_snapshot_hash: Optional[str] = None\n created_by: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n\n\n# #region RunDetailResponse [C:1] [TYPE Class]\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: List[Dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: List[Dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: Optional[Dict[str, Any]] = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n\n\n# #region RunHistoryFilter [C:1] [TYPE Class]\nclass RunHistoryFilter(BaseModel):\n job_id: Optional[str] = None\n status: Optional[str] = None\n trigger_type: Optional[str] = None\n created_by: Optional[str] = None\n date_from: Optional[datetime] = None\n date_to: Optional[datetime] = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n\n\n# #region RunListResponse [C:1] [TYPE Class]\nclass RunListResponse(BaseModel):\n items: List[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n\n\n# #region AggregatedMetricsResponse [C:1] [TYPE Class]\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: Optional[int] = None\n cumulative_cost: Optional[float] = None\n avg_duration_ms: Optional[float] = None\n last_run_at: Optional[datetime] = None\n next_scheduled_run: Optional[datetime] = None\n# #endregion AggregatedMetricsResponse\n\n\n# #region PreviewRow [C:1] [TYPE Class]\nclass PreviewRow(BaseModel):\n id: str\n source_sql: Optional[str] = None\n target_sql: Optional[str] = None\n source_object_type: Optional[str] = None\n source_object_id: Optional[str] = None\n source_object_name: Optional[str] = None\n status: str = \"PENDING\"\n feedback: Optional[str] = None\n# #endregion PreviewRow\n\n\n# #region TranslationPreviewResponse [C:1] [TYPE Class]\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: Optional[str] = None\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n\n\n# #region TranslationBatchResponse [C:1] [TYPE Class]\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n\n\n# #region MetricsResponse [C:1] [TYPE Class]\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: Optional[int] = None\n p50_duration_ms: Optional[int] = None\n p95_duration_ms: Optional[int] = None\n p99_duration_ms: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n\n# #endregion TranslateSchemas\n" }, @@ -69471,17 +71239,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateJobCreate [C:1] [TYPE Class]\nclass TranslateJobCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: Optional[str] = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: Optional[str] = Field(None, description=\"Superset datasource ID\")\n source_table: Optional[str] = Field(None, description=\"Source table name\")\n target_schema: Optional[str] = Field(None, description=\"Target table schema\")\n target_table: Optional[str] = Field(None, description=\"Target table name\")\n source_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Target key column names\")\n translation_column: Optional[str] = Field(None, description=\"Column to translate\")\n context_columns: Optional[List[str]] = Field(default_factory=list, description=\"Context column names\")\n target_language: Optional[str] = Field(None, description=\"Target language code\")\n provider_id: Optional[str] = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: Optional[List[str]] = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: Optional[str] = Field(None, description=\"Superset environment ID\")\n target_database_id: Optional[str] = Field(None, description=\"Superset database ID for INSERT via SQL Lab\")\n# #endregion TranslateJobCreate\n" }, @@ -69497,17 +71255,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateJobUpdate [C:1] [TYPE Class]\nclass TranslateJobUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n source_dialect: Optional[str] = None\n target_dialect: Optional[str] = None\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: Optional[int] = None\n upsert_strategy: Optional[str] = None\n status: Optional[str] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n# #endregion TranslateJobUpdate\n" }, @@ -69523,17 +71271,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateJobResponse [C:1] [TYPE Class]\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n" }, @@ -69549,17 +71287,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DatasourceColumnResponse [C:1] [TYPE Class]\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: Optional[str] = None\n is_physical: bool = True\n is_dttm: bool = False\n description: Optional[str] = None\n# #endregion DatasourceColumnResponse\n" }, @@ -69575,17 +71303,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DatasourceColumnsResponse [C:1] [TYPE Class]\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: Optional[str] = None\n schema_name: Optional[str] = None\n database_dialect: str\n columns: List[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n" }, @@ -69601,17 +71319,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DuplicateJobResponse [C:1] [TYPE Class]\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n" }, @@ -69627,17 +71335,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryCreate [C:1] [TYPE Class]\nclass DictionaryCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n" }, @@ -69653,17 +71351,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryImport [C:1] [TYPE Class]\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: Optional[str] = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n" }, @@ -69679,17 +71367,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryResponse [C:1] [TYPE Class]\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n entry_count: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n" }, @@ -69705,17 +71383,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryEntryCreate [C:1] [TYPE Class]\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: Optional[str] = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n" }, @@ -69731,17 +71399,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryEntryResponse [C:1] [TYPE Class]\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n" }, @@ -69757,17 +71415,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryImportResult [C:1] [TYPE Class]\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: List[Dict[str, Any]] = Field(default_factory=list)\n preview: List[Dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n" }, @@ -69783,17 +71431,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region PreviewRequest [C:1] [TYPE Class]\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: Optional[str] = Field(None, description=\"Optional custom prompt template\")\n env_id: Optional[str] = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n" }, @@ -69809,17 +71447,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region PreviewRowUpdate [C:1] [TYPE Class]\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: Optional[str] = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: Optional[str] = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n" }, @@ -69835,17 +71463,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region PreviewAcceptResponse [C:1] [TYPE Class]\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n" }, @@ -69861,17 +71479,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region CostEstimate [C:1] [TYPE Class]\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n" }, @@ -69887,17 +71495,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TermCorrectionSubmit [C:1] [TYPE Class]\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: Optional[str] = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: Optional[str] = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: Optional[str] = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n" }, @@ -69913,17 +71511,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TermCorrectionBulkSubmit [C:1] [TYPE Class]\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: List[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n" }, @@ -69939,17 +71527,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region CorrectionConflictResult [C:1] [TYPE Class]\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n" }, @@ -69965,17 +71543,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region CorrectionSubmitResponse [C:1] [TYPE Class]\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: Optional[str] = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: Optional[CorrectionConflictResult] = None\n message: Optional[str] = None\n# #endregion CorrectionSubmitResponse\n" }, @@ -69991,17 +71559,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region ScheduleResponse [C:1] [TYPE Class]\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n last_run_at: Optional[datetime] = None\n next_run_at: Optional[datetime] = None\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n" }, @@ -70017,17 +71575,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region NextExecutionResponse [C:1] [TYPE Class]\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: List[str] = []\n# #endregion NextExecutionResponse\n" }, @@ -70043,17 +71591,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationRunResponse [C:1] [TYPE Class]\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: Optional[str] = None\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n error_message: Optional[str] = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: Optional[str] = None\n superset_execution_id: Optional[str] = None\n config_snapshot: Optional[Dict[str, Any]] = None\n key_hash: Optional[str] = None\n config_hash: Optional[str] = None\n dict_snapshot_hash: Optional[str] = None\n created_by: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n" }, @@ -70069,17 +71607,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region RunDetailResponse [C:1] [TYPE Class]\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: List[Dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: List[Dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: Optional[Dict[str, Any]] = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n" }, @@ -70095,17 +71623,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region RunHistoryFilter [C:1] [TYPE Class]\nclass RunHistoryFilter(BaseModel):\n job_id: Optional[str] = None\n status: Optional[str] = None\n trigger_type: Optional[str] = None\n created_by: Optional[str] = None\n date_from: Optional[datetime] = None\n date_to: Optional[datetime] = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n" }, @@ -70121,17 +71639,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region RunListResponse [C:1] [TYPE Class]\nclass RunListResponse(BaseModel):\n items: List[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n" }, @@ -70147,17 +71655,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region AggregatedMetricsResponse [C:1] [TYPE Class]\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: Optional[int] = None\n cumulative_cost: Optional[float] = None\n avg_duration_ms: Optional[float] = None\n last_run_at: Optional[datetime] = None\n next_scheduled_run: Optional[datetime] = None\n# #endregion AggregatedMetricsResponse\n" }, @@ -70173,17 +71671,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region PreviewRow [C:1] [TYPE Class]\nclass PreviewRow(BaseModel):\n id: str\n source_sql: Optional[str] = None\n target_sql: Optional[str] = None\n source_object_type: Optional[str] = None\n source_object_id: Optional[str] = None\n source_object_name: Optional[str] = None\n status: str = \"PENDING\"\n feedback: Optional[str] = None\n# #endregion PreviewRow\n" }, @@ -70199,17 +71687,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationPreviewResponse [C:1] [TYPE Class]\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: Optional[str] = None\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n" }, @@ -70225,17 +71703,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationBatchResponse [C:1] [TYPE Class]\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n" }, @@ -70251,17 +71719,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region MetricsResponse [C:1] [TYPE Class]\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: Optional[int] = None\n p50_duration_ms: Optional[int] = None\n p95_duration_ms: Optional[int] = None\n p99_duration_ms: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n" }, @@ -70290,7 +71748,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -70316,7 +71776,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Scripts", "PURPOSE": "Provide headless CLI commands for candidate registration, artifact import and manifest build.", "SEMANTICS": [ @@ -70335,22 +71795,7 @@ "target_ref": "ComplianceOrchestrator" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:CleanReleaseCliScript:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: cli, clean-release, candidate, artifacts, manifest\n# @PURPOSE: Provide headless CLI commands for candidate registration, artifact import and manifest build.\n# @LAYER: Scripts\n# @RELATION: CALLS -> ComplianceOrchestrator\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nfrom datetime import date, datetime, timezone\nfrom typing import Any, Dict, List, Optional\n\nfrom ..models.clean_release import CandidateArtifact, ReleaseCandidate\nfrom ..services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ..services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n)\nfrom ..services.clean_release.enums import CandidateStatus\nfrom ..services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\n\n\n# [DEF:build_parser:Function]\n# @PURPOSE: Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# [/DEF:build_parser:Function]\n\n\n# [DEF:run_candidate_register:Function]\n# @PURPOSE: Register candidate in repository via CLI command.\n# @PRE: Candidate ID must be unique.\n# @POST: Candidate is persisted in DRAFT status.\ndef run_candidate_register(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n existing = repository.get_candidate(args.candidate_id)\n if existing is not None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate already exists\"}))\n return 1\n\n candidate = ReleaseCandidate(\n id=args.candidate_id,\n version=args.version,\n source_snapshot_ref=args.source_snapshot_ref,\n created_by=args.created_by,\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n print(json.dumps({\"status\": \"ok\", \"candidate_id\": candidate.id}))\n return 0\n\n\n# [/DEF:run_candidate_register:Function]\n\n\n# [DEF:run_artifact_import:Function]\n# @PURPOSE: Import single artifact for existing candidate.\n# @PRE: Candidate must exist.\n# @POST: Artifact is persisted for candidate.\ndef run_artifact_import(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n candidate = repository.get_candidate(args.candidate_id)\n if candidate is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate not found\"}))\n return 1\n\n artifact = CandidateArtifact(\n id=args.artifact_id,\n candidate_id=args.candidate_id,\n path=args.path,\n sha256=args.sha256,\n size=args.size,\n )\n repository.save_artifact(artifact)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n print(json.dumps({\"status\": \"ok\", \"artifact_id\": artifact.id}))\n return 0\n\n\n# [/DEF:run_artifact_import:Function]\n\n\n# [DEF:run_manifest_build:Function]\n# @PURPOSE: Build immutable manifest snapshot for candidate.\n# @PRE: Candidate must exist.\n# @POST: New manifest version is persisted.\ndef run_manifest_build(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n from ..services.clean_release.manifest_service import build_manifest_snapshot\n\n repository = get_clean_release_repository()\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=args.candidate_id,\n created_by=args.created_by,\n )\n except ValueError as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 1\n\n print(\n json.dumps(\n {\n \"status\": \"ok\",\n \"manifest_id\": manifest.id,\n \"version\": manifest.manifest_version,\n }\n )\n )\n return 0\n\n\n# [/DEF:run_manifest_build:Function]\n\n\n# [DEF:run_compliance_run:Function]\n# @PURPOSE: Execute compliance run for candidate with optional manifest fallback.\n# @PRE: Candidate exists and trusted snapshots are configured.\n# @POST: Returns run payload and exit code 0 on success.\ndef run_compliance_run(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository, get_config_manager\n\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n service = ComplianceExecutionService(\n repository=repository, config_manager=config_manager\n )\n\n try:\n result = service.execute_run(\n candidate_id=args.candidate_id,\n requested_by=args.actor,\n manifest_id=args.manifest_id,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n payload = {\n \"status\": \"ok\",\n \"run_id\": result.run.id,\n \"candidate_id\": result.run.candidate_id,\n \"run_status\": result.run.status,\n \"final_status\": result.run.final_status,\n \"task_id\": getattr(result.run, \"task_id\", None),\n \"report_id\": getattr(result.run, \"report_id\", None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_run:Function]\n\n\n# [DEF:run_compliance_status:Function]\n# @PURPOSE: Read run status by run id.\n# @PRE: Run exists.\n# @POST: Returns run status payload.\ndef run_compliance_status(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n payload = {\n \"status\": \"ok\",\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n \"task_id\": getattr(run, \"task_id\", None),\n \"report_id\": getattr(run, \"report_id\", None) or (report.id if report else None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_status:Function]\n\n\n# [DEF:_to_payload:Function]\n# @PURPOSE: Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.\n# @PRE: value is serializable model or primitive object.\n# @POST: Returns dictionary payload without mutating value.\ndef _to_payload(value: Any) -> Dict[str, Any]:\n def _normalize(raw: Any) -> Any:\n if isinstance(raw, datetime):\n return raw.isoformat()\n if isinstance(raw, date):\n return raw.isoformat()\n if isinstance(raw, dict):\n return {str(key): _normalize(item) for key, item in raw.items()}\n if isinstance(raw, list):\n return [_normalize(item) for item in raw]\n if isinstance(raw, tuple):\n return [_normalize(item) for item in raw]\n return raw\n\n if hasattr(value, \"model_dump\"):\n return _normalize(value.model_dump())\n table = getattr(value, \"__table__\", None)\n if table is not None:\n row = {column.name: getattr(value, column.name) for column in table.columns}\n return _normalize(row)\n raise TypeError(f\"unsupported payload type: {type(value)!r}\")\n\n\n# [/DEF:_to_payload:Function]\n\n\n# [DEF:run_compliance_report:Function]\n# @PURPOSE: Read immutable report by run id.\n# @PRE: Run and report exist.\n# @POST: Returns report payload.\ndef run_compliance_report(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n if report is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"report not found\"}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"report\": _to_payload(report)}))\n return 0\n\n\n# [/DEF:run_compliance_report:Function]\n\n\n# [DEF:run_compliance_violations:Function]\n# @PURPOSE: Read run violations by run id.\n# @PRE: Run exists.\n# @POST: Returns violations payload.\ndef run_compliance_violations(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n violations = repository.get_violations_by_run(args.run_id)\n print(\n json.dumps(\n {\"status\": \"ok\", \"items\": [_to_payload(item) for item in violations]}\n )\n )\n return 0\n\n\n# [/DEF:run_compliance_violations:Function]\n\n\n# [DEF:run_approve:Function]\n# @PURPOSE: Approve candidate based on immutable PASSED report.\n# @PRE: Candidate and report exist; report is PASSED.\n# @POST: Persists APPROVED decision and returns success payload.\ndef run_approve(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_approve:Function]\n\n\n# [DEF:run_reject:Function]\n# @PURPOSE: Reject candidate without mutating compliance evidence.\n# @PRE: Candidate and report exist.\n# @POST: Persists REJECTED decision and returns success payload.\ndef run_reject(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_reject:Function]\n\n\n# [DEF:run_publish:Function]\n# @PURPOSE: Publish approved candidate to target channel.\n# @PRE: Candidate is approved and report belongs to candidate.\n# @POST: Appends ACTIVE publication record and returns payload.\ndef run_publish(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n published_by=args.actor,\n target_channel=args.target_channel,\n publication_ref=args.publication_ref,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_publish:Function]\n\n\n# [DEF:run_revoke:Function]\n# @PURPOSE: Revoke active publication record.\n# @PRE: Publication id exists and is ACTIVE.\n# @POST: Publication record status becomes REVOKED.\ndef run_revoke(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=args.publication_id,\n revoked_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_revoke:Function]\n\n\n# [DEF:main:Function]\n# @PURPOSE: CLI entrypoint for clean release commands.\ndef main(argv: Optional[List[str]] = None) -> int:\n parser = build_parser()\n args = parser.parse_args(argv)\n\n if args.command == \"candidate-register\":\n return run_candidate_register(args)\n if args.command == \"artifact-import\":\n return run_artifact_import(args)\n if args.command == \"manifest-build\":\n return run_manifest_build(args)\n if args.command == \"compliance-run\":\n return run_compliance_run(args)\n if args.command == \"compliance-status\":\n return run_compliance_status(args)\n if args.command == \"compliance-report\":\n return run_compliance_report(args)\n if args.command == \"compliance-violations\":\n return run_compliance_violations(args)\n if args.command == \"approve\":\n return run_approve(args)\n if args.command == \"reject\":\n return run_reject(args)\n if args.command == \"publish\":\n return run_publish(args)\n if args.command == \"revoke\":\n return run_revoke(args)\n\n print(json.dumps({\"status\": \"error\", \"message\": \"unknown command\"}))\n return 2\n\n\n# [/DEF:main:Function]\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n\n# [/DEF:CleanReleaseCliScript:Module]\n" }, @@ -70366,7 +71811,17 @@ "PURPOSE": "Build argparse parser for clean release CLI." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:build_parser:Function]\n# @PURPOSE: Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# [/DEF:build_parser:Function]\n" }, @@ -70402,6 +71857,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70439,6 +71903,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70476,6 +71949,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70513,6 +71995,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70550,6 +72041,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70587,6 +72087,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70624,6 +72133,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70661,6 +72179,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70698,6 +72225,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70735,6 +72271,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70772,6 +72317,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70809,6 +72363,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70823,7 +72386,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "TUI refuses startup in non-TTY environments; headless flow is CLI/API only.", "LAYER": "UI", "PURPOSE": "Interactive terminal interface for Enterprise Clean Release compliance validation.", @@ -70894,6 +72457,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -70915,16 +72487,53 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -70962,6 +72571,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -70976,7 +72594,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Admin user must have the \"Admin\" role.", "LAYER": "Scripts", "PURPOSE": "CLI tool for creating the initial admin user.", @@ -71018,20 +72636,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -71044,6 +72648,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -71061,6 +72666,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -71078,6 +72684,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -71126,6 +72733,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -71140,7 +72756,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Safe to run multiple times (idempotent).", "LAYER": "Scripts", "PURPOSE": "Initializes the auth database and creates the necessary tables.", @@ -71181,20 +72797,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -71217,7 +72819,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "auth.db is initialized with the correct schema and seeded permissions.", "PURPOSE": "Main entry point for the initialization script." }, @@ -71264,7 +72866,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Script is idempotent for task_records and app_configurations.", "LAYER": "Scripts", "PURPOSE": "Migrates legacy config and task history from SQLite/file storage to PostgreSQL.", @@ -71319,20 +72921,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -71345,6 +72933,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "READS_FROM" @@ -71362,6 +72951,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "READS_FROM" @@ -71379,6 +72969,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "WRITES_TO" @@ -71396,6 +72987,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "WRITES_TO" @@ -71413,6 +73005,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "WRITES_TO" @@ -71445,7 +73038,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns normalized Python object or original scalar value.", "PRE": "value is scalar JSON/text/list/dict or None.", "PURPOSE": "Parses JSON-like values from SQLite TEXT/JSON columns to Python objects." @@ -71495,7 +73088,17 @@ "PURPOSE": "Resolves the existing legacy config.json path from candidates." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_find_legacy_config_path:Function]\n# @PURPOSE: Resolves the existing legacy config.json path from candidates.\ndef _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]:\n with belief_scope(\"_find_legacy_config_path\"):\n if explicit_path:\n p = Path(explicit_path)\n return p if p.exists() else None\n\n candidates = [\n Path(\"backend/config.json\"),\n Path(\"config.json\"),\n ]\n for candidate in candidates:\n if candidate.exists():\n return candidate\n return None\n\n\n# [/DEF:_find_legacy_config_path:Function]\n" }, @@ -71511,7 +73114,17 @@ "PURPOSE": "Opens a SQLite connection with row factory." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_connect_sqlite:Function]\n# @PURPOSE: Opens a SQLite connection with row factory.\ndef _connect_sqlite(path: Path) -> sqlite3.Connection:\n with belief_scope(\"_connect_sqlite\"):\n conn = sqlite3.connect(str(path))\n conn.row_factory = sqlite3.Row\n return conn\n\n\n# [/DEF:_connect_sqlite:Function]\n" }, @@ -71527,7 +73140,17 @@ "PURPOSE": "Ensures required PostgreSQL tables exist before migration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_ensure_target_schema:Function]\n# @PURPOSE: Ensures required PostgreSQL tables exist before migration.\ndef _ensure_target_schema(engine) -> None:\n with belief_scope(\"_ensure_target_schema\"):\n stmts: Iterable[str] = (\n \"\"\"\n CREATE TABLE IF NOT EXISTS app_configurations (\n id TEXT PRIMARY KEY,\n payload JSONB NOT NULL,\n updated_at TIMESTAMPTZ DEFAULT NOW()\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n status TEXT NOT NULL,\n environment_id TEXT NULL,\n started_at TIMESTAMPTZ NULL,\n finished_at TIMESTAMPTZ NULL,\n logs JSONB NULL,\n error TEXT NULL,\n result JSONB NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n params JSONB NULL\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_logs (\n id INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n level VARCHAR(16) NOT NULL,\n source VARCHAR(64) NOT NULL DEFAULT 'system',\n message TEXT NOT NULL,\n metadata_json TEXT NULL,\n CONSTRAINT fk_task_logs_task\n FOREIGN KEY(task_id)\n REFERENCES task_records(id)\n ON DELETE CASCADE\n )\n \"\"\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_timestamp ON task_logs (task_id, timestamp)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_level ON task_logs (task_id, level)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_source ON task_logs (task_id, source)\",\n \"\"\"\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'task_logs_id_seq'\n ) THEN\n PERFORM 1;\n ELSE\n CREATE SEQUENCE task_logs_id_seq OWNED BY task_logs.id;\n END IF;\n END $$;\n \"\"\",\n \"ALTER TABLE task_logs ALTER COLUMN id SET DEFAULT nextval('task_logs_id_seq')\",\n )\n with engine.begin() as conn:\n for stmt in stmts:\n conn.execute(text(stmt))\n\n\n# [/DEF:_ensure_target_schema:Function]\n" }, @@ -71543,7 +73166,17 @@ "PURPOSE": "Migrates legacy config.json into app_configurations(global)." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_migrate_config:Function]\n# @PURPOSE: Migrates legacy config.json into app_configurations(global).\ndef _migrate_config(engine, legacy_config_path: Optional[Path]) -> int:\n with belief_scope(\"_migrate_config\"):\n if legacy_config_path is None:\n log.reason(\"No legacy config.json found, skipping migration\")\n return 0\n\n payload = json.loads(legacy_config_path.read_text(encoding=\"utf-8\"))\n with engine.begin() as conn:\n conn.execute(\n text(\n \"\"\"\n INSERT INTO app_configurations (id, payload, updated_at)\n VALUES ('global', CAST(:payload AS JSONB), NOW())\n ON CONFLICT (id)\n DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()\n \"\"\"\n ),\n {\"payload\": json.dumps(payload, ensure_ascii=True)},\n )\n logger.info(\n \"[_migrate_config][Coherence:OK] Config migrated from %s\",\n legacy_config_path,\n )\n return 1\n\n\n# [/DEF:_migrate_config:Function]\n" }, @@ -71559,7 +73192,17 @@ "PURPOSE": "Migrates task_records and task_logs from SQLite into PostgreSQL." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_migrate_tasks_and_logs:Function]\n# @PURPOSE: Migrates task_records and task_logs from SQLite into PostgreSQL.\ndef _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str, int]:\n with belief_scope(\"_migrate_tasks_and_logs\"):\n stats = {\n \"task_records_total\": 0,\n \"task_records_inserted\": 0,\n \"task_logs_total\": 0,\n \"task_logs_inserted\": 0,\n }\n\n rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, type, status, environment_id, started_at, finished_at, logs, error, result, created_at, params\n FROM task_records\n ORDER BY created_at ASC\n \"\"\"\n ).fetchall()\n stats[\"task_records_total\"] = len(rows)\n\n with engine.begin() as conn:\n existing_env_ids = {\n row[0]\n for row in conn.execute(text(\"SELECT id FROM environments\")).fetchall()\n }\n for row in rows:\n params_obj = _json_load_if_needed(row[\"params\"])\n result_obj = _json_load_if_needed(row[\"result\"])\n logs_obj = _json_load_if_needed(row[\"logs\"])\n environment_id = row[\"environment_id\"]\n if environment_id and environment_id not in existing_env_ids:\n # Legacy task may reference environments that were not migrated; keep task row and drop FK value.\n environment_id = None\n\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_records (\n id, type, status, environment_id, started_at, finished_at,\n logs, error, result, created_at, params\n ) VALUES (\n :id, :type, :status, :environment_id, :started_at, :finished_at,\n CAST(:logs AS JSONB), :error, CAST(:result AS JSONB), :created_at, CAST(:params AS JSONB)\n )\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"type\": row[\"type\"],\n \"status\": row[\"status\"],\n \"environment_id\": environment_id,\n \"started_at\": row[\"started_at\"],\n \"finished_at\": row[\"finished_at\"],\n \"logs\": json.dumps(logs_obj, ensure_ascii=True)\n if logs_obj is not None\n else None,\n \"error\": row[\"error\"],\n \"result\": json.dumps(result_obj, ensure_ascii=True)\n if result_obj is not None\n else None,\n \"created_at\": row[\"created_at\"],\n \"params\": json.dumps(params_obj, ensure_ascii=True)\n if params_obj is not None\n else None,\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_records_inserted\"] += int(res.rowcount)\n\n log_rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, task_id, timestamp, level, source, message, metadata_json\n FROM task_logs\n ORDER BY id ASC\n \"\"\"\n ).fetchall()\n stats[\"task_logs_total\"] = len(log_rows)\n\n with engine.begin() as conn:\n for row in log_rows:\n # Preserve original IDs to keep migration idempotent.\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_logs (id, task_id, timestamp, level, source, message, metadata_json)\n VALUES (:id, :task_id, :timestamp, :level, :source, :message, :metadata_json)\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"task_id\": row[\"task_id\"],\n \"timestamp\": row[\"timestamp\"],\n \"level\": row[\"level\"],\n \"source\": row[\"source\"] or \"system\",\n \"message\": row[\"message\"],\n \"metadata_json\": row[\"metadata_json\"],\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_logs_inserted\"] += int(res.rowcount)\n\n # Ensure sequence is aligned after explicit id inserts.\n conn.execute(\n text(\n \"\"\"\n SELECT setval(\n 'task_logs_id_seq',\n COALESCE((SELECT MAX(id) FROM task_logs), 1),\n TRUE\n )\n \"\"\"\n )\n )\n\n logger.info(\n \"[_migrate_tasks_and_logs][Coherence:OK] task_records=%s/%s task_logs=%s/%s\",\n stats[\"task_records_inserted\"],\n stats[\"task_records_total\"],\n stats[\"task_logs_inserted\"],\n stats[\"task_logs_total\"],\n )\n return stats\n\n\n# [/DEF:_migrate_tasks_and_logs:Function]\n" }, @@ -71575,7 +73218,17 @@ "PURPOSE": "Orchestrates migration from SQLite/file to PostgreSQL." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:run_migration:Function]\n# @PURPOSE: Orchestrates migration from SQLite/file to PostgreSQL.\ndef run_migration(\n sqlite_path: Path, target_url: str, legacy_config_path: Optional[Path]\n) -> Dict[str, int]:\n with belief_scope(\"run_migration\"):\n log.reason(\"Starting migration from SQLite to PostgreSQL\", payload={\"sqlite_path\": str(sqlite_path), \"target_url\": target_url})\n if not sqlite_path.exists():\n raise FileNotFoundError(f\"SQLite source not found: {sqlite_path}\")\n\n sqlite_conn = _connect_sqlite(sqlite_path)\n engine = create_engine(target_url, pool_pre_ping=True)\n try:\n _ensure_target_schema(engine)\n config_upserted = _migrate_config(engine, legacy_config_path)\n stats = _migrate_tasks_and_logs(engine, sqlite_conn)\n stats[\"config_upserted\"] = config_upserted\n return stats\n finally:\n sqlite_conn.close()\n\n\n# [/DEF:run_migration:Function]\n" }, @@ -71588,7 +73241,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Safe to run multiple times (idempotent).", "LAYER": "Scripts", "PURPOSE": "Populates the auth database with initial system permissions.", @@ -71635,20 +73288,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } } ], "anchor_syntax": "def", @@ -71663,7 +73302,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Canonical bootstrap permission tuples seeded into auth storage." }, "relations": [ @@ -71686,7 +73325,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -71711,7 +73352,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -71746,7 +73389,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "All INITIAL_PERMISSIONS exist in the DB.", "PURPOSE": "Inserts missing permissions into the database." }, @@ -71805,7 +73448,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Created chart and dashboard names are globally unique for one script run.", "LAYER": "Scripts", "PURPOSE": "Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.", @@ -71842,20 +73485,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -71868,6 +73497,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -71885,6 +73515,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -71926,6 +73557,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -71963,6 +73603,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72000,6 +73649,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72037,6 +73695,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72074,6 +73741,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72111,6 +73787,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72150,6 +73835,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -72195,6 +73889,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -72209,7 +73912,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Tests and inspects dataset-to-dashboard relationship responses from Superset API.", "SEMANTICS": [ "scripts", @@ -72221,17 +73924,7 @@ ] }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:test_dataset_dashboard_relations_script:Module]\n# @SEMANTICS: scripts, test, dataset, dashboard, superset, relations\n# @PURPOSE: Tests and inspects dataset-to-dashboard relationship responses from Superset API.\n# @COMPLEXITY: 2\n\"\"\"\nScript to test dataset-to-dashboard relationships from Superset API.\n\nUsage:\n cd backend && .venv/bin/python3 src/scripts/test_dataset_dashboard_relations.py\n\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\n\n# Add src to path (parent of scripts directory)\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.superset_client import SupersetClient\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\n\n\ndef test_dashboard_dataset_relations():\n \"\"\"Test fetching dataset-to-dashboard relationships.\"\"\"\n \n # Load environment from existing config\n config_manager = ConfigManager()\n environments = config_manager.get_environments()\n \n if not environments:\n logger.error(\"No environments configured!\")\n return\n \n # Use first available environment\n env = environments[0]\n logger.info(f\"Using environment: {env.name} ({env.url})\")\n \n client = SupersetClient(env)\n \n try:\n # Authenticate\n logger.info(\"Authenticating to Superset...\")\n client.authenticate()\n logger.info(\"Authentication successful!\")\n \n # Test dashboard ID 13\n dashboard_id = 13\n logger.info(f\"\\n=== Fetching Dashboard {dashboard_id} ===\")\n dashboard = client.network.request(method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\")\n \n print(\"\\nDashboard structure:\")\n print(f\" ID: {dashboard.get('id')}\")\n print(f\" Title: {dashboard.get('dashboard_title')}\")\n print(f\" Published: {dashboard.get('published')}\")\n \n # Check for slices/charts\n if 'slices' in dashboard:\n logger.info(f\"\\n Found {len(dashboard['slices'])} slices/charts in dashboard\")\n for i, slice_data in enumerate(dashboard['slices'][:5]): # Show first 5\n print(f\" Slice {i+1}:\")\n print(f\" ID: {slice_data.get('slice_id')}\")\n print(f\" Name: {slice_data.get('slice_name')}\")\n # Check for datasource_id\n if 'datasource_id' in slice_data:\n print(f\" Datasource ID: {slice_data['datasource_id']}\")\n if 'datasource_name' in slice_data:\n print(f\" Datasource Name: {slice_data['datasource_name']}\")\n if 'datasource_type' in slice_data:\n print(f\" Datasource Type: {slice_data['datasource_type']}\")\n else:\n logger.warning(\" No 'slices' field found in dashboard response\")\n logger.info(f\" Available fields: {list(dashboard.keys())}\")\n \n # Test dataset ID 26\n dataset_id = 26\n logger.info(f\"\\n=== Fetching Dataset {dataset_id} ===\")\n dataset = client.get_dataset(dataset_id)\n \n print(\"\\nDataset structure:\")\n print(f\" ID: {dataset.get('id')}\")\n print(f\" Table Name: {dataset.get('table_name')}\")\n print(f\" Schema: {dataset.get('schema')}\")\n print(f\" Database: {dataset.get('database', {}).get('database_name', 'Unknown')}\")\n \n # Check for dashboards that use this dataset\n logger.info(f\"\\n=== Finding Dashboards using Dataset {dataset_id} ===\")\n \n # Method: Use Superset's related_objects API\n try:\n logger.info(f\" Using /api/v1/dataset/{dataset_id}/related_objects endpoint...\")\n related_objects = client.network.request(\n method=\"GET\",\n endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n \n logger.info(f\" Related objects response type: {type(related_objects)}\")\n logger.info(f\" Related objects keys: {list(related_objects.keys()) if isinstance(related_objects, dict) else 'N/A'}\")\n \n # Check for dashboards in related objects\n if 'dashboards' in related_objects:\n dashboards = related_objects['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n \n for dash in dashboards:\n if isinstance(dash, dict):\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.info(f\" - Dashboard: {dash}\")\n elif 'result' in related_objects:\n # Some Superset versions use 'result' wrapper\n result = related_objects['result']\n if 'dashboards' in result:\n dashboards = result['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n \n for dash in dashboards:\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.warning(f\" No 'dashboards' key in result. Keys: {list(result.keys())}\")\n else:\n logger.warning(f\" No 'dashboards' key in response. Available keys: {list(related_objects.keys())}\")\n logger.info(f\" Full related_objects response:\")\n print(json.dumps(related_objects, indent=2, default=str)[:1000])\n \n except Exception as e:\n logger.error(f\" Error fetching related objects: {e}\")\n import traceback\n traceback.print_exc()\n \n # Method 2: Try to use the position_json from dashboard\n logger.info(f\"\\n=== Analyzing Dashboard Position JSON ===\")\n if 'position_json' in dashboard:\n position_data = json.loads(dashboard['position_json'])\n logger.info(f\" Position data type: {type(position_data)}\")\n \n # Look for datasource references\n datasource_ids = set()\n if isinstance(position_data, dict):\n for key, value in position_data.items():\n if 'datasource' in key.lower() or key == 'DASHBOARD_VERSION_KEY':\n logger.debug(f\" Key: {key}, Value type: {type(value)}\")\n elif isinstance(position_data, list):\n logger.info(f\" Position data has {len(position_data)} items\")\n for item in position_data[:3]: # Show first 3\n logger.debug(f\" Item: {type(item)}, keys: {list(item.keys()) if isinstance(item, dict) else 'N/A'}\")\n if isinstance(item, dict):\n if 'datasource_id' in item:\n datasource_ids.add(item['datasource_id'])\n \n if datasource_ids:\n logger.info(f\" Found datasource IDs: {datasource_ids}\")\n \n # Save full response for analysis\n output_file = Path(__file__).parent / \"dataset_dashboard_analysis.json\"\n with open(output_file, 'w') as f:\n json.dump({\n 'dashboard': dashboard,\n 'dataset': dataset\n }, f, indent=2, default=str)\n logger.info(f\"\\nFull response saved to: {output_file}\")\n \n except Exception as e:\n logger.error(f\"Error: {e}\", exc_info=True)\n raise\n\n\nif __name__ == \"__main__\":\n test_dashboard_dataset_relations()\n\n# [/DEF:test_dataset_dashboard_relations_script:Module]\n" }, @@ -72244,7 +73937,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "NOTE": "GitService, AuthService, LLMProviderService have circular import issues - import directly when needed", "PURPOSE": "Package initialization for services module" }, @@ -72255,15 +73948,6 @@ "tag": "NOTE", "message": "@NOTE is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } } ], "anchor_syntax": "def", @@ -72278,7 +73962,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Encrypt+decrypt roundtrip always returns original plaintext.", "LAYER": "Domain", "PURPOSE": "Unit tests for EncryptionManager encrypt/decrypt functionality.", @@ -72320,6 +74004,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -72369,6 +74054,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -72650,7 +74344,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Unit tests for HealthService aggregation logic." }, "relations": [ @@ -72661,17 +74355,7 @@ "target_ref": "[src.services.health_service.HealthService]" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:test_health_service:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for HealthService aggregation logic.\n# @RELATION: VERIFIES ->[src.services.health_service.HealthService]\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_aggregation():\n \"\"\"\n @TEST_SCENARIO: Verify that HealthService correctly aggregates the latest record per dashboard.\n \"\"\"\n # Setup: Mock DB session\n db = MagicMock()\n\n now = datetime.utcnow()\n\n # Dashboard 1: Old FAIL, New PASS\n rec1_old = ValidationRecord(\n id=\"rec-old\",\n dashboard_id=\"dash_1\",\n environment_id=\"env_1\",\n status=\"FAIL\",\n timestamp=now - timedelta(hours=1),\n summary=\"Old failure\",\n issues=[],\n )\n rec1_new = ValidationRecord(\n id=\"rec-new\",\n dashboard_id=\"dash_1\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=now,\n summary=\"New pass\",\n issues=[],\n )\n\n # Dashboard 2: Single WARN\n rec2 = ValidationRecord(\n id=\"rec-warn\",\n dashboard_id=\"dash_2\",\n environment_id=\"env_1\",\n status=\"WARN\",\n timestamp=now,\n summary=\"Warning\",\n issues=[],\n )\n\n # Mock the query chain\n # subquery = self.db.query(...).filter(...).group_by(...).subquery()\n # query = self.db.query(ValidationRecord).join(subquery, ...).all()\n\n mock_query = db.query.return_value\n mock_query.filter.return_value = mock_query\n mock_query.group_by.return_value = mock_query\n mock_query.subquery.return_value = MagicMock()\n\n db.query.return_value.join.return_value.all.return_value = [rec1_new, rec2]\n\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=\"env_1\")\n\n assert summary.pass_count == 1\n assert summary.warn_count == 1\n assert summary.fail_count == 0\n assert len(summary.items) == 2\n\n # Verify dash_1 has the latest status (PASS)\n dash_1_item = next(item for item in summary.items if item.dashboard_id == \"dash_1\")\n assert dash_1_item.status == \"PASS\"\n assert dash_1_item.summary == \"New pass\"\n assert dash_1_item.record_id == rec1_new.id\n assert dash_1_item.dashboard_slug == \"dash_1\"\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_empty():\n \"\"\"\n @TEST_SCENARIO: Verify behavior with no records.\n \"\"\"\n db = MagicMock()\n db.query.return_value.join.return_value.all.return_value = []\n\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=\"env_none\")\n\n assert summary.pass_count == 0\n assert len(summary.items) == 0\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_resolves_slug_and_title_from_superset():\n db = MagicMock()\n config_manager = MagicMock()\n config_manager.get_environments.return_value = [MagicMock(id=\"env_1\")]\n\n record = ValidationRecord(\n id=\"rec-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n )\n db.query.return_value.join.return_value.all.return_value = [record]\n\n with patch(\"src.services.health_service.SupersetClient\") as mock_client_cls:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [\n {\"id\": 42, \"slug\": \"ops-overview\", \"title\": \"Ops Overview\"}\n ]\n mock_client_cls.return_value = mock_client\n\n service = HealthService(db, config_manager=config_manager)\n summary = await service.get_health_summary(environment_id=\"env_1\")\n\n assert summary.items[0].dashboard_slug == \"ops-overview\"\n assert summary.items[0].dashboard_title == \"Ops Overview\"\n mock_client.get_dashboards_summary.assert_called_once_with()\n\n\n@pytest.mark.anyio\nasync def test_get_health_summary_reuses_dashboard_metadata_cache_across_service_instances():\n HealthService._dashboard_summary_cache.clear()\n\n db = MagicMock()\n config_manager = MagicMock()\n config_manager.get_environments.return_value = [MagicMock(id=\"env_1\")]\n record = ValidationRecord(\n id=\"rec-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n )\n db.query.return_value.join.return_value.all.return_value = [record]\n\n with patch(\"src.services.health_service.SupersetClient\") as mock_client_cls:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [\n {\"id\": 42, \"slug\": \"ops-overview\", \"title\": \"Ops Overview\"}\n ]\n mock_client_cls.return_value = mock_client\n\n first_service = HealthService(db, config_manager=config_manager)\n second_service = HealthService(db, config_manager=config_manager)\n\n first_summary = await first_service.get_health_summary(environment_id=\"env_1\")\n second_summary = await second_service.get_health_summary(environment_id=\"env_1\")\n\n assert first_summary.items[0].dashboard_slug == \"ops-overview\"\n assert second_summary.items[0].dashboard_slug == \"ops-overview\"\n mock_client.get_dashboards_summary.assert_called_once_with()\n HealthService._dashboard_summary_cache.clear()\n\n\n# [DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks.\ndef test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object(), \"task-2\": object(), \"task-3\": object()}\n\n target_record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n older_peer = ValidationRecord(\n id=\"rec-2\",\n task_id=\"task-2\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"FAIL\",\n timestamp=datetime.utcnow() - timedelta(hours=1),\n summary=\"Older\",\n issues=[],\n screenshot_path=None,\n )\n other_environment = ValidationRecord(\n id=\"rec-3\",\n task_id=\"task-3\",\n dashboard_id=\"42\",\n environment_id=\"env_2\",\n status=\"WARN\",\n timestamp=datetime.utcnow(),\n summary=\"Other environment\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = target_record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [target_record, older_peer]\n\n db.query.side_effect = [first_query, peer_query]\n\n with patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls:\n cleanup_instance = MagicMock()\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n assert db.delete.call_count == 2\n db.delete.assert_any_call(target_record)\n db.delete.assert_any_call(older_peer)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-1\")\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-2\")\n assert cleanup_instance.delete_task_with_logs.call_count == 2\n assert \"task-1\" not in task_manager.tasks\n assert \"task-2\" not in task_manager.tasks\n assert \"task-3\" in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n\n\n# [DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete returns False when validation record does not exist.\ndef test_delete_validation_report_returns_false_for_unknown_record():\n db = MagicMock()\n db.query.return_value.filter.return_value.first.return_value = None\n\n service = HealthService(db, config_manager=MagicMock())\n\n assert service.delete_validation_report(\"missing\") is False\n\n\n# [/DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n\n\n# [DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks.\ndef test_delete_validation_report_swallows_linked_task_cleanup_failure():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object()}\n\n record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [record]\n\n db.query.side_effect = [first_query, peer_query]\n\n with (\n patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls,\n patch(\"src.services.health_service.logger\") as mock_logger,\n ):\n cleanup_instance = MagicMock()\n cleanup_instance.delete_task_with_logs.side_effect = RuntimeError(\n \"cleanup exploded\"\n )\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n db.delete.assert_called_once_with(record)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_called_once_with(\"task-1\")\n mock_logger.warning.assert_called_once()\n assert \"task-1\" not in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n# [/DEF:test_health_service:Module]\n" }, @@ -72797,7 +74481,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Regression test for ValidationRecord persistence fields populated from task context." }, "relations": [ @@ -72808,17 +74492,7 @@ "target_ref": "[DashboardValidationPlugin:Class]" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:test_llm_plugin_persistence:Module]\n# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.\n\nimport types\nimport pytest\n\nfrom src.plugins.llm_analysis import plugin as plugin_module\n\n\n# [DEF:_DummyLogger:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.\n# @INVARIANT: Logging methods are no-ops and must not mutate test state.\nclass _DummyLogger:\n def with_source(self, _source: str):\n return self\n\n def info(self, *_args, **_kwargs):\n return None\n\n def debug(self, *_args, **_kwargs):\n return None\n\n def warning(self, *_args, **_kwargs):\n return None\n\n def error(self, *_args, **_kwargs):\n return None\n\n\n# [/DEF:_DummyLogger:Class]\n\n\n# [DEF:_FakeDBSession:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.\n# @INVARIANT: add/commit/close provide only persistence signals asserted by this test.\nclass _FakeDBSession:\n def __init__(self):\n self.added = None\n self.committed = False\n self.closed = False\n\n def add(self, obj):\n self.added = obj\n\n def commit(self):\n self.committed = True\n\n def close(self):\n self.closed = True\n\n\n# [/DEF:_FakeDBSession:Class]\n\n\n# [DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.\n# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.\n@pytest.mark.asyncio\nasync def test_dashboard_validation_plugin_persists_task_and_environment_ids(\n tmp_path, monkeypatch\n):\n fake_db = _FakeDBSession()\n\n env = types.SimpleNamespace(id=\"env-42\")\n provider = types.SimpleNamespace(\n id=\"provider-1\",\n name=\"Main LLM\",\n provider_type=\"openai\",\n base_url=\"https://example.invalid/v1\",\n default_model=\"gpt-4o\",\n is_active=True,\n )\n\n # [DEF:_FakeProviderService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.\n # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.\n class _FakeProviderService:\n def __init__(self, _db):\n return None\n\n def get_provider(self, _provider_id):\n return provider\n\n def get_decrypted_api_key(self, _provider_id):\n return \"a\" * 32\n\n # [/DEF:_FakeProviderService:Class]\n\n # [DEF:_FakeScreenshotService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.\n # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.\n class _FakeScreenshotService:\n def __init__(self, _env):\n return None\n\n async def capture_dashboard(self, _dashboard_id, _screenshot_path):\n return None\n\n # [/DEF:_FakeScreenshotService:Class]\n\n # [DEF:_FakeLLMClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.\n # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.\n class _FakeLLMClient:\n \"\"\"Fake LLM client for persistence tests.\n\n Always returns PASS status. FAIL and UNKNOWN persistence branches are NOT\n covered by this fake — see coverage-planner findings.\n \"\"\"\n\n def __init__(self, **_kwargs):\n return None\n\n async def analyze_dashboard(self, *_args, **_kwargs):\n return {\n \"status\": \"PASS\",\n \"summary\": \"Dashboard healthy\",\n \"issues\": [],\n }\n\n # [/DEF:_FakeLLMClient:Class]\n\n # [DEF:_FakeNotificationService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.\n # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.\n class _FakeNotificationService:\n def __init__(self, *_args, **_kwargs):\n return None\n\n async def dispatch_report(self, **_kwargs):\n return None\n\n # [/DEF:_FakeNotificationService:Class]\n\n # [DEF:_FakeConfigManager:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.\n # @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.\n class _FakeConfigManager:\n def get_environment(self, _env_id):\n return env\n\n def get_config(self):\n return types.SimpleNamespace(\n settings=types.SimpleNamespace(\n storage=types.SimpleNamespace(root_path=str(tmp_path)),\n llm={},\n )\n )\n\n # [/DEF:_FakeConfigManager:Class]\n\n # [DEF:_FakeSupersetClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.\n # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.\n class _FakeSupersetClient:\n def __init__(self, _env):\n self.network = types.SimpleNamespace(\n request=lambda **_kwargs: {\"result\": []}\n )\n\n # [/DEF:_FakeSupersetClient:Class]\n\n monkeypatch.setattr(plugin_module, \"SessionLocal\", lambda: fake_db)\n monkeypatch.setattr(plugin_module, \"LLMProviderService\", _FakeProviderService)\n monkeypatch.setattr(plugin_module, \"ScreenshotService\", _FakeScreenshotService)\n monkeypatch.setattr(plugin_module, \"LLMClient\", _FakeLLMClient)\n monkeypatch.setattr(plugin_module, \"NotificationService\", _FakeNotificationService)\n monkeypatch.setattr(plugin_module, \"SupersetClient\", _FakeSupersetClient)\n monkeypatch.setattr(\n \"src.dependencies.get_config_manager\", lambda: _FakeConfigManager()\n )\n\n context = types.SimpleNamespace(\n task_id=\"task-999\",\n logger=_DummyLogger(),\n background_tasks=None,\n )\n\n plugin = plugin_module.DashboardValidationPlugin()\n result = await plugin.execute(\n {\n \"dashboard_id\": \"11\",\n \"environment_id\": \"env-42\",\n \"provider_id\": \"provider-1\",\n },\n context=context,\n )\n\n assert result[\"environment_id\"] == \"env-42\"\n assert fake_db.committed is True\n assert fake_db.closed is True\n assert fake_db.added is not None\n assert fake_db.added.task_id == \"task-999\"\n assert fake_db.added.environment_id == \"env-42\"\n\n\n# [/DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n\n\n# [/DEF:test_llm_plugin_persistence:Module]\n" }, @@ -72831,7 +74505,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Logging methods are no-ops and must not mutate test state.", "PURPOSE": "Minimal logger shim for TaskContext-like objects used in tests." }, @@ -72853,6 +74527,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -72875,7 +74558,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "add/commit/close provide only persistence signals asserted by this test.", "PURPOSE": "Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin." }, @@ -72919,7 +74602,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.", "PURPOSE": "Ensure db ValidationRecord includes context.task_id and params.environment_id." }, @@ -72969,7 +74652,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Returns same provider and key regardless of provider_id argument; no lookup logic.", "PURPOSE": "LLM provider service stub returning deterministic provider and decrypted API key for plugin tests." }, @@ -72991,6 +74674,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -73013,7 +74705,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.", "PURPOSE": "Screenshot service stub that accepts capture_dashboard calls without side effects." }, @@ -73035,6 +74727,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -73057,7 +74758,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "analyze_dashboard is side-effect free and returns schema-compatible PASS result.", "PURPOSE": "Deterministic LLM client double returning canonical analysis payload for persistence-path assertions." }, @@ -73101,7 +74802,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.", "PURPOSE": "Notification service stub that accepts plugin dispatch_report payload without introducing side effects." }, @@ -73123,6 +74824,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -73145,7 +74855,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.", "PURPOSE": "Superset client stub exposing network.request as a lambda that returns empty result list." }, @@ -73167,6 +74877,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -73189,7 +74908,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All required prompt keys remain available after normalization.", "LAYER": "Domain Tests", "PURPOSE": "Validate normalization and rendering behavior for configurable LLM prompt templates.", @@ -73218,20 +74937,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain Tests" - } } ], "anchor_syntax": "def", @@ -73246,7 +74951,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returned structure includes required prompt templates with fallback defaults.", "PRE": "Input llm settings do not contain complete prompts object.", "PURPOSE": "Ensure legacy/partial llm settings are expanded with all prompt defaults." @@ -73291,7 +74996,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Custom prompt value remains unchanged in normalized output.", "PRE": "Input llm settings contain custom prompt override.", "PURPOSE": "Ensure user-customized prompt values are preserved during normalization." @@ -73336,7 +75041,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Rendered prompt string contains substituted values.", "PRE": "Template contains placeholders matching provided variables.", "PURPOSE": "Ensure template placeholders are deterministically replaced." @@ -73381,7 +75086,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Ensure multimodal model detection recognizes common vision-capable model names." }, "relations": [ @@ -73415,7 +75120,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Verify provider binding resolution priority." }, "relations": [ @@ -73449,7 +75154,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Ensure assistant planner provider/model fields are preserved and normalized." }, "relations": [ @@ -73483,7 +75188,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Contract testing for LLMProviderService and EncryptionManager", "SEMANTICS": [ "tests", @@ -73500,17 +75205,7 @@ "target_ref": "[src.services.llm_provider:Module]" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:test_llm_provider:Module]\n# @RELATION: VERIFIES -> [src.services.llm_provider:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, llm-provider, encryption, contract\n# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager\n# [/DEF:test_llm_provider:Module]\n" }, @@ -73546,7 +75241,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -73703,7 +75400,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.", "PURPOSE": "MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.", "RISK": "query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety." @@ -73728,7 +75425,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -73751,7 +75450,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -73776,7 +75478,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -73817,7 +75521,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "LLMProviderService fixture wired to mock_db for provider CRUD tests." }, "relations": [ @@ -73840,7 +75544,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -73865,7 +75571,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -74065,7 +75773,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Synchronization adds only missing normalized permission pairs.", "LAYER": "Service Tests", "PURPOSE": "Verifies RBAC permission catalog discovery and idempotent synchronization behavior.", @@ -74096,20 +75804,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service Tests" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -74122,6 +75816,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -74352,7 +76047,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Resource summaries preserve task linkage and status projection behavior.", "LAYER": "Service", "PURPOSE": "Unit tests for ResourceService", @@ -74381,20 +76076,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service" - } } ], "anchor_syntax": "def", @@ -75129,7 +76810,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "[Credentials | ADFSClaims] -> [UserEntity | SessionToken]", "INVARIANT": "Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.", "LAYER": "Domain", @@ -75180,6 +76861,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -75192,6 +76891,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75209,6 +76909,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75226,6 +76927,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75243,6 +76945,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75260,6 +76963,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75278,7 +76982,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Provides high-level authentication services." }, "relations": [ @@ -75314,6 +77018,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75331,6 +77036,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75348,6 +77054,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75366,7 +77073,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "DATA_CONTRACT": "Input(Session) -> Model(AuthRepository)", "PARAM": "db (Session) - SQLAlchemy session.", "POST": "self.repo is initialized and ready for auth user/role CRUD operations.", @@ -75409,6 +77116,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -75431,7 +77147,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input(str username, str password) -> Output(User | None)", "PARAM": "password (str) - The plain password.", "POST": "Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.", @@ -75521,6 +77237,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75538,6 +77255,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -75555,6 +77273,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75573,7 +77292,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input(User) -> Output(Dict[str, str]{access_token, token_type})", "PARAM": "user (User) - The authenticated user.", "POST": "Returns session dict with non-empty access_token and token_type='bearer'.", @@ -75663,6 +77382,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -75680,6 +77400,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75697,6 +77418,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75715,7 +77437,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)", "PARAM": "user_info (Dict[str, Any]) - Claims from ADFS token.", "POST": "Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.", @@ -75805,6 +77527,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75822,6 +77545,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75839,6 +77563,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75857,7 +77582,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Publish the canonical semantic root for the clean-release backend service cluster." }, @@ -75900,6 +77625,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75917,6 +77643,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75934,6 +77661,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75951,6 +77679,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -75969,7 +77698,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Validate audit hooks emit expected log patterns for clean release lifecycle.", "SEMANTICS": [ @@ -76000,6 +77729,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -76117,7 +77847,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Failed mandatory stage forces BLOCKED terminal status.", "LAYER": "Domain", "PURPOSE": "Validate compliance orchestrator stage transitions and final status derivation.", @@ -76158,6 +77888,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -76308,7 +78039,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "TestInput -> TestOutput", "INVARIANT": "Same input artifacts produce identical deterministic hash.", "LAYER": "Domain", @@ -76332,6 +78063,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -76344,6 +78093,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -76427,9 +78177,9 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -76456,6 +78206,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -76554,12 +78305,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -76681,7 +78426,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Candidate preparation always persists manifest and candidate status deterministically.", "LAYER": "Domain", "PURPOSE": "Validate release candidate preparation flow, including policy evaluation and manifest persisting.", @@ -76722,6 +78467,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -76751,6 +78497,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -76784,6 +78539,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -76817,6 +78581,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -76875,18 +78648,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -76945,18 +78706,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77015,18 +78764,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77085,18 +78822,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77119,7 +78844,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "blocked run requires at least one blocking violation.", "LAYER": "Domain", "PURPOSE": "Validate compliance report builder counter integrity and blocked-run constraints.", @@ -77160,6 +78885,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -77189,6 +78915,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77222,6 +78957,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77376,7 +79120,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "External endpoints always produce blocking violations.", "LAYER": "Domain", "PURPOSE": "Verify internal source registry validation behavior.", @@ -77417,6 +79161,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -77444,15 +79189,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -77541,7 +79277,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Validate final status derivation logic from stage results.", "SEMANTICS": [ @@ -77572,6 +79308,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -77745,6 +79482,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77782,6 +79528,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77819,6 +79574,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77856,6 +79620,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77893,6 +79666,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77930,6 +79712,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77967,6 +79758,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -77981,7 +79781,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]", "INVARIANT": "A run binds to exactly one candidate/manifest/policy/registry snapshot set.", "LAYER": "Domain", @@ -78023,7 +79823,26 @@ "target_ref": "ReportBuilder" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceExecutionService:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, compliance, execution, stages, immutable-evidence\n# @PURPOSE: Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> RepositoryRelations\n# @RELATION: DEPENDS_ON -> PolicyResolutionService\n# @RELATION: DEPENDS_ON -> ComplianceStages\n# @RELATION: DEPENDS_ON -> ReportBuilder\n# @PRE: Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.\n# @POST: Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.\n# @SIDE_EFFECT: Persists runs, stage results, violations, and reports through repository adapters and audit helpers.\n# @DATA_CONTRACT: Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]\n# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom typing import Any, Iterable, List, Optional, cast\nfrom uuid import uuid4\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n)\nfrom .audit_service import audit_check_run, audit_report, audit_violation\nfrom .enums import ComplianceDecision, RunStatus\nfrom .exceptions import ComplianceRunError, PolicyResolutionError\nfrom .policy_resolution_service import resolve_trusted_policy_snapshots\nfrom .report_builder import ComplianceReportBuilder\nfrom .repository import CleanReleaseRepository\nfrom .stages import build_default_stages, derive_final_status\nfrom .stages.base import ComplianceStage, ComplianceStageContext, build_stage_run_record\n\n\nbelief_logger = cast(Any, logger)\n\n\n# [DEF:ComplianceExecutionResult:Class]\n# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: Optional[ComplianceReport]\n stage_runs: List[ComplianceStageRun]\n violations: List[ComplianceViolation]\n\n\n# [/DEF:ComplianceExecutionResult:Class]\n\n\n# [DEF:ComplianceExecutionService:Class]\n# @PURPOSE: Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence.\n# @PRE: Database session active, candidate registered\n# @POST: Returns ComplianceReport with pass/fail status and violation details\n# @SIDE_EFFECT: Updates compliance status in database, logs violations\n# @DATA_CONTRACT: ComplianceCheckResult, ComplianceReport, Violation\nclass ComplianceExecutionService:\n TASK_PLUGIN_ID = \"clean-release-compliance\"\n\n def __init__(\n self,\n *,\n repository: CleanReleaseRepository,\n config_manager,\n stages: Optional[Iterable[ComplianceStage]] = None,\n ):\n self.repository = repository\n self.config_manager = config_manager\n self.stages = list(stages) if stages is not None else build_default_stages()\n self.report_builder = ComplianceReportBuilder(repository)\n\n # [DEF:_resolve_manifest:Function]\n # @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.\n # @PRE: candidate exists.\n # @POST: Returns manifest snapshot or raises ComplianceRunError.\n def _resolve_manifest(\n self, candidate_id: str, manifest_id: Optional[str]\n ) -> DistributionManifest:\n with belief_scope(\"ComplianceExecutionService._resolve_manifest\"):\n manifest_id_value = cast(Optional[str], manifest_id)\n if manifest_id_value is not None and manifest_id_value != \"\":\n manifest = self.repository.get_manifest(manifest_id_value)\n if manifest is None:\n raise ComplianceRunError(\n f\"manifest '{manifest_id_value}' not found\"\n )\n if str(getattr(manifest, \"candidate_id\")) != candidate_id:\n raise ComplianceRunError(\"manifest does not belong to candidate\")\n return manifest\n\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ComplianceRunError(f\"candidate '{candidate_id}' has no manifest\")\n return sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n\n # [/DEF:_resolve_manifest:Function]\n\n # [DEF:_persist_stage_run:Function]\n # @PURPOSE: Persist stage run if repository supports stage records.\n # @POST: Stage run is persisted when adapter is available, otherwise no-op.\n def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:\n self.repository.save_stage_run(stage_run)\n\n # [/DEF:_persist_stage_run:Function]\n\n # [DEF:_persist_violations:Function]\n # @PURPOSE: Persist stage violations via repository adapters.\n # @POST: Violations are appended to repository evidence store.\n def _persist_violations(self, violations: List[ComplianceViolation]) -> None:\n for violation in violations:\n self.repository.save_violation(violation)\n\n # [/DEF:_persist_violations:Function]\n\n # [DEF:execute_run:Function]\n # @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.\n # @PRE: candidate exists and trusted policy/registry snapshots are resolvable.\n # @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.\n def execute_run(\n self,\n *,\n candidate_id: str,\n requested_by: str,\n manifest_id: Optional[str] = None,\n ) -> ComplianceExecutionResult:\n with belief_scope(\"ComplianceExecutionService.execute_run\"):\n belief_logger.reason(\n f\"Starting compliance execution candidate_id={candidate_id}\"\n )\n\n candidate = self.repository.get_candidate(candidate_id)\n if candidate is None:\n raise ComplianceRunError(f\"candidate '{candidate_id}' not found\")\n\n manifest = self._resolve_manifest(candidate_id, manifest_id)\n\n try:\n policy_snapshot, registry_snapshot = resolve_trusted_policy_snapshots(\n config_manager=self.config_manager,\n repository=self.repository,\n )\n except PolicyResolutionError as exc:\n raise ComplianceRunError(str(exc)) from exc\n\n run = ComplianceRun(\n id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=str(getattr(manifest, \"id\")),\n manifest_digest=str(getattr(manifest, \"manifest_digest\")),\n policy_snapshot_id=str(getattr(policy_snapshot, \"id\")),\n registry_snapshot_id=str(getattr(registry_snapshot, \"id\")),\n requested_by=requested_by,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING.value,\n )\n self.repository.save_check_run(run)\n\n stage_runs: List[ComplianceStageRun] = []\n violations: List[ComplianceViolation] = []\n report: Optional[ComplianceReport] = None\n\n context = ComplianceStageContext(\n run=run,\n candidate=candidate,\n manifest=manifest,\n policy=policy_snapshot,\n registry=registry_snapshot,\n )\n\n try:\n for stage in self.stages:\n started = datetime.now(timezone.utc)\n result = stage.execute(context)\n finished = datetime.now(timezone.utc)\n\n stage_run = build_stage_run_record(\n run_id=str(getattr(run, \"id\")),\n stage_name=stage.stage_name,\n result=result,\n started_at=started,\n finished_at=finished,\n )\n self._persist_stage_run(stage_run)\n stage_runs.append(stage_run)\n\n if result.violations:\n self._persist_violations(result.violations)\n violations.extend(result.violations)\n\n setattr(run, \"final_status\", derive_final_status(stage_runs).value)\n setattr(run, \"status\", RunStatus.SUCCEEDED.value)\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n\n report = self.report_builder.build_report_payload(run, violations)\n report = self.report_builder.persist_report(report)\n setattr(run, \"report_id\", str(getattr(report, \"id\")))\n self.repository.save_check_run(run)\n belief_logger.reflect(\n f\"[REFLECT] Compliance run completed run_id={getattr(run, 'id')} final_status={getattr(run, 'final_status', None)}\"\n )\n except Exception as exc: # noqa: BLE001\n setattr(run, \"status\", RunStatus.FAILED.value)\n setattr(run, \"final_status\", ComplianceDecision.ERROR.value)\n setattr(run, \"failure_reason\", str(exc))\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n belief_logger.explore(\n f\"[EXPLORE] Compliance run failed run_id={getattr(run, 'id')}: {exc}\"\n )\n\n return ComplianceExecutionResult(\n run=run,\n report=report,\n stage_runs=stage_runs,\n violations=violations,\n )\n\n # [/DEF:execute_run:Function]\n\n\n# [/DEF:ComplianceExecutionService:Class]\n\n# [/DEF:ComplianceExecutionService:Module]\n" }, @@ -78039,7 +79858,17 @@ "PURPOSE": "Return envelope for compliance execution with run/report and persisted stage artifacts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceExecutionResult:Class]\n# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: Optional[ComplianceReport]\n stage_runs: List[ComplianceStageRun]\n violations: List[ComplianceViolation]\n\n\n# [/DEF:ComplianceExecutionResult:Class]\n" }, @@ -78075,6 +79904,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78102,6 +79940,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78129,6 +79976,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78166,6 +80022,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78180,7 +80045,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Manifest -> ComplianceReport", "INVARIANT": "COMPLIANT is impossible when any mandatory stage fails.", "LAYER": "Domain", @@ -78283,10 +80148,22 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -78300,6 +80177,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -78317,6 +80195,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -78334,6 +80213,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -78355,7 +80235,17 @@ "PURPOSE": "Coordinate clean-release compliance verification stages." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CleanComplianceOrchestrator:Class]\n# @PURPOSE: Coordinate clean-release compliance verification stages.\nclass CleanComplianceOrchestrator:\n # [DEF:__init__:Function]\n # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.\n # @PRE: repository is a valid CleanReleaseRepository instance with required methods.\n # @POST: self.repository is assigned and used by all orchestration steps.\n # @SIDE_EFFECT: Stores repository reference on orchestrator instance.\n # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None\n def __init__(self, repository: CleanReleaseRepository):\n with belief_scope(\"CleanComplianceOrchestrator.__init__\"):\n self.repository = repository\n\n # [/DEF:__init__:Function]\n\n # [DEF:start_check_run:Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # [/DEF:start_check_run:Function]\n\n # [DEF:execute_stages:Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: Optional[List[ComplianceStageRun]] = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n\n return self.repository.save_check_run(check_run)\n\n # [/DEF:execute_stages:Function]\n\n # [DEF:finalize_run:Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n # [/DEF:finalize_run:Function]\n\n\n# [/DEF:CleanComplianceOrchestrator:Class]\n" }, @@ -78403,6 +80293,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -78460,6 +80359,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -78517,6 +80425,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -78574,6 +80491,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -78596,7 +80522,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Demo and real namespaces must never collide for generated physical identifiers.", "LAYER": "Domain", "PURPOSE": "Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.", @@ -78638,6 +80564,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -78679,6 +80606,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78716,6 +80652,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78753,6 +80698,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78767,7 +80721,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Application", "PURPOSE": "Data Transfer Objects for clean release compliance subsystem." }, @@ -78779,22 +80733,7 @@ "target_ref": "pydantic" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:clean_release_dto:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Data Transfer Objects for clean release compliance subsystem.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> pydantic\n\nfrom datetime import datetime\nfrom typing import List, Optional, Dict, Any\nfrom pydantic import BaseModel, Field\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision\n\nclass CandidateDTO(BaseModel):\n \"\"\"DTO for ReleaseCandidate.\"\"\"\n id: str\n version: str\n source_snapshot_ref: str\n build_id: Optional[str] = None\n created_at: datetime\n created_by: str\n status: CandidateStatus\n\nclass ArtifactDTO(BaseModel):\n \"\"\"DTO for CandidateArtifact.\"\"\"\n id: str\n candidate_id: str\n path: str\n sha256: str\n size: int\n detected_category: Optional[str] = None\n declared_category: Optional[str] = None\n source_uri: Optional[str] = None\n source_host: Optional[str] = None\n metadata: Dict[str, Any] = Field(default_factory=dict)\n\nclass ManifestDTO(BaseModel):\n \"\"\"DTO for DistributionManifest.\"\"\"\n id: str\n candidate_id: str\n manifest_version: int\n manifest_digest: str\n artifacts_digest: str\n created_at: datetime\n created_by: str\n source_snapshot_ref: str\n content_json: Dict[str, Any]\n\nclass ComplianceRunDTO(BaseModel):\n \"\"\"DTO for ComplianceRun status tracking.\"\"\"\n run_id: str\n candidate_id: str\n status: RunStatus\n final_status: Optional[ComplianceDecision] = None\n report_id: Optional[str] = None\n task_id: Optional[str] = None\n\nclass ReportDTO(BaseModel):\n \"\"\"Compact report view.\"\"\"\n report_id: str\n candidate_id: str\n final_status: ComplianceDecision\n policy_version: str\n manifest_digest: str\n violation_count: int\n generated_at: datetime\n\nclass CandidateOverviewDTO(BaseModel):\n \"\"\"Read model for candidate overview.\"\"\"\n candidate_id: str\n version: str\n source_snapshot_ref: str\n status: CandidateStatus\n latest_manifest_id: Optional[str] = None\n latest_manifest_digest: Optional[str] = None\n latest_run_id: Optional[str] = None\n latest_run_status: Optional[RunStatus] = None\n latest_report_id: Optional[str] = None\n latest_report_final_status: Optional[ComplianceDecision] = None\n latest_policy_snapshot_id: Optional[str] = None\n latest_policy_version: Optional[str] = None\n latest_registry_snapshot_id: Optional[str] = None\n latest_registry_version: Optional[str] = None\n latest_approval_decision: Optional[str] = None\n latest_publication_id: Optional[str] = None\n latest_publication_status: Optional[str] = None\n\n# [/DEF:clean_release_dto:Module]\n" }, @@ -78807,7 +80746,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Canonical enums for clean release lifecycle and compliance." }, @@ -78832,7 +80771,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Domain exceptions for clean release compliance subsystem." }, @@ -78857,7 +80796,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Application", "PURPOSE": "Unified entry point for clean release operations." }, @@ -78869,22 +80808,7 @@ "target_ref": "ComplianceOrchestrator" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:clean_release_facade:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unified entry point for clean release operations.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> ComplianceOrchestrator\n\nfrom typing import List, Optional\nfrom src.services.clean_release.repositories import (\n CandidateRepository, ArtifactRepository, ManifestRepository,\n PolicyRepository, ComplianceRepository, ReportRepository,\n ApprovalRepository, PublicationRepository, AuditRepository\n)\nfrom src.services.clean_release.dto import (\n CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,\n ReportDTO, CandidateOverviewDTO\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.core.logger import belief_scope\nfrom src.core.config_manager import ConfigManager\n\nclass CleanReleaseFacade:\n \"\"\"\n @PURPOSE: Orchestrates repositories and services to provide a clean API for UI/CLI.\n \"\"\"\n def __init__(\n self,\n candidate_repo: CandidateRepository,\n artifact_repo: ArtifactRepository,\n manifest_repo: ManifestRepository,\n policy_repo: PolicyRepository,\n compliance_repo: ComplianceRepository,\n report_repo: ReportRepository,\n approval_repo: ApprovalRepository,\n publication_repo: PublicationRepository,\n audit_repo: AuditRepository,\n config_manager: ConfigManager\n ):\n self.candidate_repo = candidate_repo\n self.artifact_repo = artifact_repo\n self.manifest_repo = manifest_repo\n self.policy_repo = policy_repo\n self.compliance_repo = compliance_repo\n self.report_repo = report_repo\n self.approval_repo = approval_repo\n self.publication_repo = publication_repo\n self.audit_repo = audit_repo\n self.config_manager = config_manager\n\n def resolve_active_policy_snapshot(self) -> Optional[CleanPolicySnapshot]:\n \"\"\"\n @PURPOSE: Resolve the active policy snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_policy_snapshot\"):\n config = self.config_manager.get_config()\n policy_id = config.settings.clean_release.active_policy_id\n if not policy_id:\n return None\n return self.policy_repo.get_policy_snapshot(policy_id)\n\n def resolve_active_registry_snapshot(self) -> Optional[SourceRegistrySnapshot]:\n \"\"\"\n @PURPOSE: Resolve the active registry snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_registry_snapshot\"):\n config = self.config_manager.get_config()\n registry_id = config.settings.clean_release.active_registry_id\n if not registry_id:\n return None\n return self.policy_repo.get_registry_snapshot(registry_id)\n\n def get_candidate_overview(self, candidate_id: str) -> Optional[CandidateOverviewDTO]:\n \"\"\"\n @PURPOSE: Build a comprehensive overview for a candidate.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.get_candidate_overview\"):\n candidate = self.candidate_repo.get_by_id(candidate_id)\n if not candidate:\n return None\n\n manifest = self.manifest_repo.get_latest_for_candidate(candidate_id)\n runs = self.compliance_repo.list_runs_by_candidate(candidate_id)\n latest_run = runs[-1] if runs else None\n \n report = None\n if latest_run:\n report = self.report_repo.get_by_run(latest_run.id)\n\n approval = self.approval_repo.get_latest_for_candidate(candidate_id)\n publication = self.publication_repo.get_latest_for_candidate(candidate_id)\n \n active_policy = self.resolve_active_policy_snapshot()\n active_registry = self.resolve_active_registry_snapshot()\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=manifest.id if manifest else None,\n latest_manifest_digest=manifest.manifest_digest if manifest else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=report.id if report else None,\n latest_report_final_status=ComplianceDecision(report.final_status) if report else None,\n latest_policy_snapshot_id=active_policy.id if active_policy else None,\n latest_policy_version=active_policy.policy_version if active_policy else None,\n latest_registry_snapshot_id=active_registry.id if active_registry else None,\n latest_registry_version=active_registry.registry_version if active_registry else None,\n latest_approval_decision=approval.decision if approval else None,\n latest_publication_id=publication.id if publication else None,\n latest_publication_status=publication.status if publication else None\n )\n\n def list_candidates(self) -> List[CandidateOverviewDTO]:\n \"\"\"\n @PURPOSE: List all candidates with their current status.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.list_candidates\"):\n candidates = self.candidate_repo.list_all()\n return [self.get_candidate_overview(c.id) for c in candidates]\n\n# [/DEF:clean_release_facade:Module]\n" }, @@ -78897,7 +80821,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Equal semantic artifact sets produce identical deterministic hash values.", "LAYER": "Domain", "PURPOSE": "Build deterministic distribution manifest from classified artifact input.", @@ -78938,6 +80862,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -78979,6 +80904,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -78993,7 +80927,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Manifest -> ManifestRecord; Candidate -> ManifestRecord", "INVARIANT": "Existing manifests are never mutated.", "LAYER": "Domain", @@ -79030,6 +80964,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -79042,6 +80994,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79059,6 +81012,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79076,6 +81030,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79117,6 +81072,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79131,7 +81095,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Application", "PURPOSE": "Map between domain entities (SQLAlchemy models) and DTOs." }, @@ -79143,22 +81107,7 @@ "target_ref": "clean_release_dto" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:clean_release_mappers:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Map between domain entities (SQLAlchemy models) and DTOs.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> clean_release_dto\n\nfrom typing import List\nfrom src.models.clean_release import (\n ReleaseCandidate, DistributionManifest, ComplianceRun,\n ComplianceStageRun, ComplianceViolation, ComplianceReport,\n CleanPolicySnapshot, SourceRegistrySnapshot, ApprovalDecision,\n PublicationRecord\n)\nfrom src.services.clean_release.dto import (\n CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,\n ReportDTO\n)\nfrom src.services.clean_release.enums import (\n CandidateStatus, RunStatus, ComplianceDecision,\n ViolationSeverity, ViolationCategory\n)\n\ndef map_candidate_to_dto(candidate: ReleaseCandidate) -> CandidateDTO:\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n build_id=candidate.build_id,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status)\n )\n\ndef map_manifest_to_dto(manifest: DistributionManifest) -> ManifestDTO:\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json or {}\n )\n\ndef map_run_to_dto(run: ComplianceRun) -> ComplianceRunDTO:\n return ComplianceRunDTO(\n run_id=run.id,\n candidate_id=run.candidate_id,\n status=RunStatus(run.status),\n final_status=ComplianceDecision(run.final_status) if run.final_status else None,\n task_id=run.task_id\n )\n\ndef map_report_to_dto(report: ComplianceReport) -> ReportDTO:\n # Note: ReportDTO in dto.py is a compact view\n return ReportDTO(\n report_id=report.id,\n candidate_id=report.candidate_id,\n final_status=ComplianceDecision(report.final_status),\n policy_version=\"unknown\", # Would need to resolve from run/snapshot\n manifest_digest=\"unknown\", # Would need to resolve from run/manifest\n violation_count=0, # Would need to resolve from violations\n generated_at=report.generated_at\n )\n\n# [/DEF:clean_release_mappers:Module]\n" }, @@ -79171,7 +81120,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Candidate -> PolicyDecision", "INVARIANT": "Enterprise-clean policy always treats non-registry sources as violations.", "LAYER": "Domain", @@ -79201,6 +81150,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -79213,6 +81180,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79230,6 +81198,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79339,21 +81308,42 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_INVARIANT is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Module", + "Function" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_INVARIANT", + "message": "@TEST_INVARIANT is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Class' at C1", "detail": { "actual_complexity": 1, "contract_type": "Class" @@ -79372,7 +81362,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "PolicyRequest -> ResolutionResult", "INVARIANT": "Trusted snapshot resolution is based only on ConfigManager active identifiers.", "LAYER": "Domain", @@ -79409,6 +81399,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -79421,6 +81429,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79438,6 +81447,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79455,6 +81465,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79498,6 +81509,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -79520,7 +81540,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Candidate preparation always persists manifest and candidate status deterministically.", "LAYER": "Domain", "PURPOSE": "Prepare release candidate by policy evaluation and deterministic manifest creation.", @@ -79573,6 +81593,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79590,6 +81611,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79607,6 +81629,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79648,6 +81671,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79685,6 +81717,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79722,6 +81763,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79759,6 +81809,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79796,6 +81855,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79833,6 +81901,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -79847,7 +81924,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]", "INVARIANT": "blocking_violations_count never exceeds violations_count.", "LAYER": "Domain", @@ -79945,10 +82022,22 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -79962,6 +82051,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79979,6 +82069,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -79997,7 +82088,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Export all clean release repositories." }, "relations": [ @@ -80008,17 +82099,7 @@ "target_ref": "sqlalchemy" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:clean_release_repositories:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Export all clean release repositories.\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom .candidate_repository import CandidateRepository\nfrom .artifact_repository import ArtifactRepository\nfrom .manifest_repository import ManifestRepository\nfrom .policy_repository import PolicyRepository\nfrom .compliance_repository import ComplianceRepository\nfrom .report_repository import ReportRepository\nfrom .approval_repository import ApprovalRepository\nfrom .publication_repository import PublicationRepository\nfrom .audit_repository import AuditRepository, CleanReleaseAuditLog\n\n__all__ = [\n \"CandidateRepository\",\n \"ArtifactRepository\",\n \"ManifestRepository\",\n \"PolicyRepository\",\n \"ComplianceRepository\",\n \"ReportRepository\",\n \"ApprovalRepository\",\n \"PublicationRepository\",\n \"AuditRepository\",\n \"CleanReleaseAuditLog\"\n]\n\n# [/DEF:clean_release_repositories:Module]\n" }, @@ -80031,7 +82112,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query approval decisions." }, @@ -80056,7 +82137,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query candidate artifacts." }, @@ -80081,7 +82162,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query audit logs for clean release operations." }, @@ -80106,7 +82187,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query release candidates." }, @@ -80131,7 +82212,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query compliance runs, stage runs, and violations." }, @@ -80156,7 +82237,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query distribution manifests." }, @@ -80193,7 +82274,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Encapsulates database CRUD operations for DistributionManifest entities." }, "relations": [ @@ -80223,7 +82304,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Repository is ready for database operations.", "PRE": "db is a valid SQLAlchemy Session instance.", "PURPOSE": "Initialize repository with an active SQLAlchemy session." @@ -80247,6 +82328,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -80261,7 +82351,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Manifest is committed to database and refreshed with generated ID.", "PRE": "manifest is a valid DistributionManifest instance with required fields populated.", "PURPOSE": "Persist a DistributionManifest to the database.", @@ -80316,7 +82406,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns DistributionManifest if found, None otherwise.", "PRE": "manifest_id is a valid string identifier.", "PURPOSE": "Retrieve a single DistributionManifest by its primary key." @@ -80370,7 +82460,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns the highest manifest_version manifest for the candidate, or None.", "PRE": "candidate_id is a valid string identifier.", "PURPOSE": "Retrieve the most recent manifest version for a given candidate." @@ -80415,7 +82505,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns a list of DistributionManifest instances (may be empty).", "PRE": "candidate_id is a valid string identifier.", "PURPOSE": "List all manifests for a specific candidate, ordered by version." @@ -80469,7 +82559,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query policy and registry snapshots." }, @@ -80494,7 +82584,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query publication records." }, @@ -80519,7 +82609,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Persist and query compliance reports." }, @@ -80544,7 +82634,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Repository operations are side-effect free outside explicit save/update calls.", "LAYER": "Infra", "PURPOSE": "Provide repository adapter for clean release entities with deterministic access methods.", @@ -80585,6 +82675,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80606,7 +82697,17 @@ "PURPOSE": "Data access object for clean release lifecycle." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CleanReleaseRepository:Class]\n# @PURPOSE: Data access object for clean release lifecycle.\n@dataclass\nclass CleanReleaseRepository:\n candidates: Dict[str, ReleaseCandidate] = field(default_factory=dict)\n policies: Dict[str, CleanPolicySnapshot] = field(default_factory=dict)\n registries: Dict[str, SourceRegistrySnapshot] = field(default_factory=dict)\n artifacts: Dict[str, object] = field(default_factory=dict)\n manifests: Dict[str, DistributionManifest] = field(default_factory=dict)\n check_runs: Dict[str, ComplianceRun] = field(default_factory=dict)\n stage_runs: Dict[str, ComplianceStageRun] = field(default_factory=dict)\n reports: Dict[str, ComplianceReport] = field(default_factory=dict)\n violations: Dict[str, ComplianceViolation] = field(default_factory=dict)\n audit_events: List[Dict[str, Any]] = field(default_factory=list)\n\n def _entity_id(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"id\")))\n\n def _candidate_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"candidate_id\")))\n\n def _run_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"run_id\")))\n\n def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n cast(Dict[str, ReleaseCandidate], self.candidates)[\n self._entity_id(candidate)\n ] = candidate\n return candidate\n\n def get_candidate(self, candidate_id: str) -> Optional[ReleaseCandidate]:\n return self.candidates.get(candidate_id)\n\n def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:\n cast(Dict[str, CleanPolicySnapshot], self.policies)[self._entity_id(policy)] = (\n policy\n )\n return policy\n\n def get_policy(self, policy_id: str) -> Optional[CleanPolicySnapshot]:\n return self.policies.get(policy_id)\n\n def get_active_policy(self) -> Optional[CleanPolicySnapshot]:\n # In-memory repo doesn't track 'active' flag on snapshot,\n # this should be resolved by facade using ConfigManager.\n return next(iter(self.policies.values()), None)\n\n def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n cast(Dict[str, SourceRegistrySnapshot], self.registries)[\n self._entity_id(registry)\n ] = registry\n return registry\n\n def get_registry(self, registry_id: str) -> Optional[SourceRegistrySnapshot]:\n return self.registries.get(registry_id)\n\n def save_artifact(self, artifact) -> object:\n cast(Dict[str, object], self.artifacts)[self._entity_id(artifact)] = artifact\n return artifact\n\n def get_artifacts_by_candidate(self, candidate_id: str) -> List[object]:\n artifacts = cast(List[Any], list(self.artifacts.values()))\n return [\n artifact\n for artifact in artifacts\n if self._candidate_ref(artifact) == candidate_id\n ]\n\n def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:\n cast(Dict[str, DistributionManifest], self.manifests)[\n self._entity_id(manifest)\n ] = manifest\n return manifest\n\n def get_manifest(self, manifest_id: str) -> Optional[DistributionManifest]:\n return self.manifests.get(manifest_id)\n\n def save_distribution_manifest(\n self, manifest: DistributionManifest\n ) -> DistributionManifest:\n return self.save_manifest(manifest)\n\n def get_distribution_manifest(\n self, manifest_id: str\n ) -> Optional[DistributionManifest]:\n return self.get_manifest(manifest_id)\n\n def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:\n cast(Dict[str, ComplianceRun], self.check_runs)[self._entity_id(check_run)] = (\n check_run\n )\n return check_run\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n cast(Dict[str, ComplianceStageRun], self.stage_runs)[\n self._entity_id(stage_run)\n ] = stage_run\n return stage_run\n\n def get_check_run(self, check_run_id: str) -> Optional[ComplianceRun]:\n return self.check_runs.get(check_run_id)\n\n def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:\n return self.save_check_run(run)\n\n def get_compliance_run(self, run_id: str) -> Optional[ComplianceRun]:\n return self.get_check_run(run_id)\n\n def save_report(self, report: ComplianceReport) -> ComplianceReport:\n report_id = self._entity_id(report)\n store = cast(Dict[str, ComplianceReport], self.reports)\n existing = store.get(report_id)\n if existing is not None:\n raise ValueError(\n f\"immutable report snapshot already exists for id={report_id}\"\n )\n store[report_id] = report\n return report\n\n def get_report(self, report_id: str) -> Optional[ComplianceReport]:\n return self.reports.get(report_id)\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n cast(Dict[str, ComplianceViolation], self.violations)[\n self._entity_id(violation)\n ] = violation\n return violation\n\n def get_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:\n violations = cast(List[ComplianceViolation], list(self.violations.values()))\n return [\n violation for violation in violations if self._run_ref(violation) == run_id\n ]\n\n def get_manifests_by_candidate(\n self, candidate_id: str\n ) -> List[DistributionManifest]:\n manifests = cast(List[DistributionManifest], list(self.manifests.values()))\n return [\n manifest\n for manifest in manifests\n if self._candidate_ref(manifest) == candidate_id\n ]\n\n def append_audit_event(self, payload: Dict[str, Any]) -> None:\n self.audit_events.append(payload)\n\n def clear_history(self) -> None:\n self.check_runs.clear()\n self.reports.clear()\n self.violations.clear()\n\n\n# [/DEF:CleanReleaseRepository:Class]\n" }, @@ -80619,7 +82720,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Any endpoint outside enabled registry entries is treated as external-source violation.", "LAYER": "Domain", "PURPOSE": "Validate that all resource endpoints belong to the approved internal source registry.", @@ -80660,6 +82761,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80678,7 +82780,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Stage order remains deterministic for all compliance runs.", "LAYER": "Domain", "PURPOSE": "Define compliance stage order and helper functions for deterministic run-state evaluation.", @@ -80725,6 +82827,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80742,6 +82845,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80783,6 +82887,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -80820,6 +82933,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -80857,6 +82979,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -80894,6 +83025,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -80908,7 +83048,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Stage execution is deterministic for equal input context.", "LAYER": "Domain", "PURPOSE": "Define shared contracts and helpers for pluggable clean-release compliance stages.", @@ -80956,6 +83096,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80973,6 +83114,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -80994,7 +83136,17 @@ "PURPOSE": "Immutable input envelope passed to each compliance stage." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceStageContext:Class]\n# @PURPOSE: Immutable input envelope passed to each compliance stage.\n@dataclass(frozen=True)\nclass ComplianceStageContext:\n run: ComplianceRun\n candidate: ReleaseCandidate\n manifest: DistributionManifest\n policy: CleanPolicySnapshot\n registry: SourceRegistrySnapshot\n\n\n# [/DEF:ComplianceStageContext:Class]\n" }, @@ -81010,7 +83162,17 @@ "PURPOSE": "Structured stage output containing decision, details and violations." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:StageExecutionResult:Class]\n# @PURPOSE: Structured stage output containing decision, details and violations.\n@dataclass\nclass StageExecutionResult:\n decision: ComplianceDecision\n details_json: Dict[str, Any] = field(default_factory=dict)\n violations: List[ComplianceViolation] = field(default_factory=list)\n\n\n# [/DEF:StageExecutionResult:Class]\n" }, @@ -81026,7 +83188,17 @@ "PURPOSE": "Protocol for pluggable stage implementations." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceStage:Class]\n# @PURPOSE: Protocol for pluggable stage implementations.\nclass ComplianceStage(Protocol):\n stage_name: ComplianceStageName\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ...\n\n\n# [/DEF:ComplianceStage:Class]\n" }, @@ -81062,6 +83234,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -81099,6 +83280,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -81113,7 +83303,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "prohibited_detected_count > 0 always yields BLOCKED stage decision.", "LAYER": "Domain", "PURPOSE": "Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.", @@ -81159,6 +83349,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[IMPLEMENTS]" @@ -81176,6 +83367,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -81217,6 +83409,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -81231,7 +83432,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.", "LAYER": "Domain", "PURPOSE": "Verify manifest-declared sources belong to trusted internal registry allowlist.", @@ -81278,6 +83479,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[IMPLEMENTS]" @@ -81295,6 +83497,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -81336,6 +83539,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -81350,7 +83562,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Digest mismatch between run and manifest yields ERROR with blocking violation evidence.", "LAYER": "Domain", "PURPOSE": "Ensure run is bound to the exact manifest snapshot and digest used at run creation time.", @@ -81398,6 +83610,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[IMPLEMENTS]" @@ -81415,6 +83628,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -81456,6 +83670,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -81470,7 +83693,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Endpoint outside allowed scheme/host always yields BLOCKED stage decision.", "LAYER": "Domain", "PURPOSE": "Block manifest payloads that expose external endpoints outside trusted schemes and hosts.", @@ -81517,6 +83740,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[IMPLEMENTS]" @@ -81534,6 +83758,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -81575,6 +83800,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -81607,17 +83841,12 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Services' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Services" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -81641,6 +83870,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -81659,7 +83889,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]", "INVARIANT": "Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.", "LAYER": "Domain", @@ -81734,6 +83964,24 @@ "actual_complexity": 4, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -81748,7 +83996,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed active-question payload returned to the API layer." }, "relations": [], @@ -81765,7 +84013,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Clarification state result carrying the current session, active payload, and changed findings." }, "relations": [], @@ -81782,7 +84030,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed answer command for clarification state mutation." }, "relations": [], @@ -81799,7 +84047,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Bind repository dependency for clarification persistence operations." }, "relations": [], @@ -81816,7 +84064,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns exactly one active/open question payload or None when no unresolved question remains.", "PRE": "Session contains unresolved clarification state or a resumable clarification session.", "PURPOSE": "Return the one active highest-priority clarification question payload.", @@ -81846,7 +84094,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Answer row is persisted before current-question pointer advances.", "PRE": "Target question belongs to the session's active clarification session and is still open.", "PURPOSE": "Persist one clarification answer before any pointer/readiness mutation.", @@ -81876,11 +84124,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Produce a compact progress summary for pause/resume and completion UX." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:summarize_progress:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.\n def summarize_progress(self, clarification_session: ClarificationSession) -> str:\n resolved = count_resolved_questions(clarification_session)\n remaining = count_remaining_questions(clarification_session)\n return f\"{resolved} resolved, {remaining} unresolved\"\n\n # [/DEF:summarize_progress:Function]\n" }, @@ -81893,7 +84151,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Select the latest clarification session for the current dataset review aggregate." }, "relations": [], @@ -81910,11 +84168,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Resolve a clarification question from the active clarification aggregate." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_find_question:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # [/DEF:_find_question:Function]\n" }, @@ -81927,7 +84195,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation." }, @@ -81952,7 +84220,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Select the next unresolved question in deterministic priority order." }, "relations": [], @@ -81969,11 +84237,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Count questions whose answers fully resolved the ambiguity." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:count_resolved_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions whose answers fully resolved the ambiguity.\ndef count_resolved_questions(clarification_session: ClarificationSession) -> int:\n return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED)\n\n\n# [/DEF:count_resolved_questions:Function]\n" }, @@ -81986,11 +84264,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Count questions still unresolved or deferred after clarification interaction." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:count_remaining_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions still unresolved or deferred after clarification interaction.\ndef count_remaining_questions(clarification_session: ClarificationSession) -> int:\n return sum(\n 1\n for q in clarification_session.questions\n if q.state in {QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW}\n )\n\n\n# [/DEF:count_remaining_questions:Function]\n" }, @@ -82003,7 +84291,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Validate and normalize answer payload based on answer kind and active question options." }, "relations": [], @@ -82020,11 +84308,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Build a compact audit note describing how the clarification answer affects session state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:build_impact_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build a compact audit note describing how the clarification answer affects session state.\ndef build_impact_summary(\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n) -> str:\n if answer_kind == AnswerKind.SKIPPED:\n return f\"Clarification for {question.topic_ref} was skipped and remains unresolved.\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return f\"Clarification for {question.topic_ref} was deferred for expert review.\"\n return f\"Clarification for {question.topic_ref} recorded as '{answer_value}'.\"\n\n\n# [/DEF:build_impact_summary:Function]\n" }, @@ -82037,7 +84335,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules." }, "relations": [ @@ -82061,7 +84359,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Recompute readiness after clarification mutation while preserving unresolved visibility semantics." }, "relations": [], @@ -82078,7 +84376,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Recompute next-action guidance after clarification mutations." }, "relations": [], @@ -82095,7 +84393,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", "LAYER": "Domain", "POST": "Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.", @@ -82146,6 +84444,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -82163,6 +84462,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -82182,17 +84482,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:SessionEventLoggerImports:Block]\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import DatasetReviewSession, SessionEvent\n# [/DEF:SessionEventLoggerImports:Block]\n" }, @@ -82205,7 +84495,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed input contract for one persisted dataset-review session audit event." }, "relations": [], @@ -82222,7 +84512,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", "POST": "Returns the committed session event row with a stable identifier and stored detail payload.", "PRE": "The database session is live and payload identifiers are non-empty.", @@ -82265,6 +84555,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -82282,6 +84573,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -82300,7 +84592,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Bind a live SQLAlchemy session to the session-event logger." }, "relations": [], @@ -82317,7 +84609,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", "POST": "Returns the committed SessionEvent record with normalized detail payload.", "PRE": "session_id, actor_user_id, event_type, and event_summary are non-empty.", @@ -82354,6 +84646,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -82372,7 +84665,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Convenience wrapper for logging an event directly from a session aggregate root." }, "relations": [ @@ -82405,6 +84698,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -82423,7 +84717,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]", "INVARIANT": "Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.", "LAYER": "Domain", @@ -82498,7 +84792,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Instance holds collaborator references used by start/preview/launch orchestration methods.", "PRE": "repository/config_manager are valid collaborators for the current request scope.", "PURPOSE": "Bind repository, config, and task dependencies required by the orchestration boundary." @@ -82545,7 +84839,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[PreparePreviewCommand] -> Output[PreparePreviewResult]", "POST": "returns preview artifact in pending, ready, failed, or stale state.", "PRE": "all required variables have candidate values or explicitly accepted defaults.", @@ -82583,7 +84877,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.", "PRE": "session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.", "PURPOSE": "Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.", @@ -82613,7 +84907,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "returns task identifier when a task could be enqueued, otherwise None.", "PRE": "session is already persisted.", "PURPOSE": "Link session start to observable async recovery when task infrastructure is available.", @@ -82670,7 +84964,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain", "PURPOSE": "Typed command and result dataclasses for dataset review orchestration boundary." }, @@ -82711,7 +85005,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed input contract for starting a dataset review session." }, "relations": [], @@ -82728,7 +85022,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Session-start result carrying the persisted session and intake recovery metadata." }, "relations": [], @@ -82745,7 +85039,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed input contract for compiling one Superset-backed session preview." }, "relations": [], @@ -82762,7 +85056,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Result contract for one persisted compiled preview attempt." }, "relations": [], @@ -82779,7 +85073,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed input contract for launching one dataset-review session into SQL Lab." }, "relations": [], @@ -82796,7 +85090,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Launch result carrying immutable run context and any gate blockers." }, "relations": [], @@ -82813,7 +85107,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Domain", "POST": "Helper results are deterministic and do not mutate persistence directly.", "PRE": "Caller provides a loaded session aggregate with hydrated child collections.", @@ -82856,7 +85150,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize dataset-selection payload into canonical session references." }, "relations": [], @@ -82873,7 +85167,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Create the first profile snapshot so exports and detail views remain usable immediately after intake." }, "relations": [], @@ -82890,7 +85184,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns warning-level findings that preserve usable but incomplete state.", "PRE": "parsed_context.partial_recovery is true.", "PURPOSE": "Project partial Superset intake recovery into explicit findings without blocking session usability." @@ -82937,7 +85231,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Separate normalized filter payload metadata from the user-facing effective filter value." }, "relations": [], @@ -82954,7 +85248,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns deterministic execution snapshot for current session state without mutating persistence.", "PRE": "Session aggregate includes imported filters, template variables, and current execution mappings.", "PURPOSE": "Build effective filters, template params, approvals, and fingerprint for preview and launch gating." @@ -82992,7 +85286,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns explicit blocker codes for every unmet launch invariant.", "PRE": "execution_snapshot was computed from current session state.", "PURPOSE": "Enforce launch gates from findings, approvals, and current preview truth." @@ -83039,7 +85333,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve the current latest preview snapshot for one session aggregate." }, "relations": [], @@ -83056,11 +85350,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Produce deterministic execution fingerprint for preview truth and staleness checks." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:compute_preview_fingerprint:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Produce deterministic execution fingerprint for preview truth and staleness checks.\ndef compute_preview_fingerprint(payload: Dict[str, Any]) -> str:\n serialized = json.dumps(payload, sort_keys=True, default=str)\n return hashlib.sha256(serialized.encode(\"utf-8\")).hexdigest()\n\n\n# [/DEF:compute_preview_fingerprint:Function]\n" }, @@ -83073,7 +85377,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Unit tests for DatasetReviewSessionRepository." }, "relations": [ @@ -83085,15 +85389,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -83115,6 +85410,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -83133,7 +85429,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows." }, "relations": [ @@ -83563,7 +85859,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Domain", "POST": "Session aggregate writes preserve ownership and version semantics.", "PRE": "All mutations execute within authenticated request or task scope.", @@ -83606,7 +85902,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[SessionMutation] -> Output[PersistedSessionAggregate]", "INVARIANT": "answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.", "LAYER": "Domain", @@ -83662,7 +85958,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Signal optimistic-lock conflicts for dataset review session mutations." }, "relations": [], @@ -83679,7 +85975,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Repository instance initialized with valid session", "PRE": "db_session is not None", "PURPOSE": "Bind one live SQLAlchemy session to the repository instance." @@ -83717,7 +86013,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "returns the owned session or raises a deterministic access error.", "PRE": "session_id and user_id are non-empty identifiers from the authenticated ownership scope.", "PURPOSE": "Resolve one owner-scoped dataset review session for mutation paths." @@ -83764,7 +86060,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "session is committed, refreshed, and returned with persisted identifiers.", "PURPOSE": "Persist an initial dataset review session shell." }, @@ -83801,7 +86097,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "returns the same session when versions match; otherwise raises deterministic conflict error.", "PURPOSE": "Enforce optimistic-lock version matching before a session mutation is persisted." }, @@ -83838,7 +86134,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "session version increments monotonically.", "PURPOSE": "Increment optimistic-lock version after a successful session mutation is assembled." }, @@ -83866,7 +86162,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "session mutation is committed with one version increment or a deterministic conflict error is raised.", "PURPOSE": "Commit one prepared session mutation and translate stale writes into deterministic conflicts." }, @@ -83912,7 +86208,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns SessionDetail with all fields populated or None.", "PURPOSE": "Return the full session aggregate for API and frontend resume flows." }, @@ -83949,7 +86245,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "stored profile matches the current session and findings are replaced.", "PURPOSE": "Persist profile state and replace validation findings for an owned session." }, @@ -83995,7 +86291,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist imported filters, template variables, and initial execution mappings." }, "relations": [], @@ -84022,7 +86318,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist a preview snapshot and mark prior session previews stale." }, "relations": [], @@ -84049,7 +86345,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist an immutable launch audit snapshot for an owned session." }, "relations": [], @@ -84076,7 +86372,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "List review sessions owned by a specific user ordered by most recent update." }, "relations": [], @@ -84093,7 +86389,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Manual overrides are never silently replaced by imported, inferred, or AI-generated values.", "LAYER": "Domain", "POST": "candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.", @@ -84156,6 +86452,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84173,6 +86470,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84190,6 +86488,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84207,6 +86506,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84226,17 +86526,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:imports:Block]\nfrom dataclasses import dataclass, field\nfrom difflib import SequenceMatcher\nfrom typing import Any, Dict, Iterable, List, Mapping, Optional\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import (\n CandidateMatchType,\n CandidateStatus,\n FieldProvenance,\n SemanticSource,\n)\n# [/DEF:imports:Block]\n" }, @@ -84249,7 +86539,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Carries field-level dictionary resolution output with explicit review and partial-recovery state." }, "relations": [], @@ -84266,7 +86556,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize uploaded semantic file records into field-level candidates." }, "relations": [], @@ -84283,7 +86573,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]", "POST": "returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.", "PRE": "dictionary source exists and fields contain stable field_name values.", @@ -84326,6 +86616,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84343,6 +86634,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84361,7 +86653,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Reuse semantic metadata from trusted Superset datasets." }, "relations": [], @@ -84378,7 +86670,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Apply confidence ordering and determine best candidate per field." }, "relations": [ @@ -84411,6 +86703,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84429,7 +86722,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Mark competing candidate sets that require explicit user review." }, "relations": [], @@ -84446,7 +86739,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Accept, reject, or manually override a field-level semantic value." }, "relations": [], @@ -84463,7 +86756,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]", "POST": "unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.", "PRE": "source is persisted and fields belong to the same session aggregate.", @@ -84506,6 +86799,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84523,6 +86817,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -84541,7 +86836,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize one dictionary row into a consistent lookup structure." }, "relations": [], @@ -84558,7 +86853,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Produce confidence-scored fuzzy matches while keeping them reviewable." }, "relations": [], @@ -84575,7 +86870,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Project normalized dictionary rows into semantic candidate payloads." }, "relations": [], @@ -84592,7 +86887,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention." }, "relations": [], @@ -84609,7 +86904,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize field identifiers for stable exact/fuzzy comparisons." }, "relations": [], @@ -84626,7 +86921,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Composed GitService via multiple inheritance from domain-specific mixins.", "RATIONALE": "Decomposed from monolithic git_service.py (2101 lines) into", @@ -84694,7 +86989,17 @@ "target_ref": "[GitServiceGitlabMixin]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServiceModule:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, service, decomposition, mixin, composition\n# @PURPOSE: Composed GitService via multiple inheritance from domain-specific mixins.\n# @RELATION: DEPENDS_ON -> [GitServiceBase]\n# @RELATION: DEPENDS_ON -> [GitServiceBranchMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceSyncMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceStatusMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceMergeMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGiteaMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGithubMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGitlabMixin]\n#\n# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into\n# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class\n# preserves the original public API surface — all consumers continue to import\n# `from src.services.git_service import GitService` without changes.\n# @REJECTED: Keeping a single 2101-line file — violates fractal limit INV_7.\n\nfrom ._base import GitServiceBase\nfrom ._branch import GitServiceBranchMixin\nfrom ._sync import GitServiceSyncMixin\nfrom ._status import GitServiceStatusMixin\nfrom ._merge import GitServiceMergeMixin\nfrom ._url import GitServiceUrlMixin\nfrom ._gitea import GitServiceGiteaMixin\nfrom ._remote_providers import GitServiceGithubMixin, GitServiceGitlabMixin\n\n__all__ = [\"GitService\"]\n\n\n# [DEF:GitService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull,\n# merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab).\n# @RELATION: INHERITS -> [GitServiceBase]\n# @RELATION: INHERITS -> [GitServiceBranchMixin]\n# @RELATION: INHERITS -> [GitServiceSyncMixin]\n# @RELATION: INHERITS -> [GitServiceStatusMixin]\n# @RELATION: INHERITS -> [GitServiceMergeMixin]\n# @RELATION: INHERITS -> [GitServiceUrlMixin]\n# @RELATION: INHERITS -> [GitServiceGiteaMixin]\n# @RELATION: INHERITS -> [GitServiceGithubMixin]\n# @RELATION: INHERITS -> [GitServiceGitlabMixin]\nclass GitService(\n GitServiceGiteaMixin,\n GitServiceGithubMixin,\n GitServiceGitlabMixin,\n GitServiceMergeMixin,\n GitServiceSyncMixin,\n GitServiceStatusMixin,\n GitServiceBranchMixin,\n GitServiceUrlMixin,\n GitServiceBase,\n):\n \"\"\"\n Composed GitService — all Git operations via domain-specific mixins.\n\n MRO ensures provider mixins and domain mixins resolve before the base class.\n All consumers continue to use: from src.services.git_service import GitService\n \"\"\"\n pass\n# [/DEF:GitService:Class]\n# [/DEF:GitServiceModule:Module]\n" }, @@ -84707,7 +87012,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull," }, "relations": [], @@ -84734,7 +87039,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration.", "SEMANTICS": [ @@ -84784,6 +87089,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "INHERITED_BY" @@ -84832,6 +87138,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -84869,6 +87184,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -84908,6 +87232,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -84952,6 +87285,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -84994,6 +87336,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -85033,6 +87384,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85084,6 +87444,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85126,6 +87495,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -85165,6 +87543,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85207,6 +87594,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -85221,7 +87617,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes.", "SEMANTICS": [ @@ -85253,6 +87649,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -85294,6 +87691,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -85333,6 +87739,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85352,7 +87767,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation.", "SEMANTICS": [ @@ -85384,6 +87799,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -85427,6 +87843,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85471,6 +87896,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85515,6 +87949,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85559,6 +88002,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85603,6 +88055,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85647,6 +88108,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85666,7 +88136,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote.", "SEMANTICS": [ @@ -85698,6 +88168,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -85719,7 +88190,17 @@ "PURPOSE": "Read text from a Git blob." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_read_blob_text:Function]\n # @PURPOSE: Read text from a Git blob.\n def _read_blob_text(self, blob: Blob) -> str:\n with belief_scope(\"GitService._read_blob_text\"):\n if blob is None:\n return \"\"\n try:\n return blob.data_stream.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n return \"\"\n # [/DEF:_read_blob_text:Function]\n" }, @@ -85735,7 +88216,17 @@ "PURPOSE": "List files with merge conflicts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_get_unmerged_file_paths:Function]\n # @PURPOSE: List files with merge conflicts.\n def _get_unmerged_file_paths(self, repo: Repo) -> List[str]:\n with belief_scope(\"GitService._get_unmerged_file_paths\"):\n try:\n return sorted(list(repo.index.unmerged_blobs().keys()))\n except Exception:\n return []\n # [/DEF:_get_unmerged_file_paths:Function]\n" }, @@ -85751,7 +88242,17 @@ "PURPOSE": "Build payload for unfinished merge state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_build_unfinished_merge_payload:Function]\n # @PURPOSE: Build payload for unfinished merge state.\n def _build_unfinished_merge_payload(self, repo: Repo) -> Dict[str, Any]:\n with belief_scope(\"GitService._build_unfinished_merge_payload\"):\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n merge_head_value = \"\"\n merge_msg_preview = \"\"\n current_branch = \"unknown\"\n try:\n merge_head_value = Path(merge_head_path).read_text(encoding=\"utf-8\").strip()\n except Exception:\n merge_head_value = \"\"\n try:\n merge_msg_path = os.path.join(repo.git_dir, \"MERGE_MSG\")\n if os.path.exists(merge_msg_path):\n merge_msg_preview = (\n Path(merge_msg_path).read_text(encoding=\"utf-8\").strip().splitlines()[:1] or [\"\"]\n )[0]\n except Exception:\n merge_msg_preview = \"\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n conflicts_count = len(self._get_unmerged_file_paths(repo))\n return {\n \"error_code\": \"GIT_UNFINISHED_MERGE\",\n \"message\": (\n \"\\u0412 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0438 \\u0435\\u0441\\u0442\\u044c \\u043d\\u0435\\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0451\\u043d\\u043d\\u043e\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435. \"\n \"\\u0417\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u0435 \\u0438\\u043b\\u0438 \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435 \\u0432\\u0440\\u0443\\u0447\\u043d\\u0443\\u044e.\"\n ),\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": merge_head_value,\n \"merge_message_preview\": merge_msg_preview,\n \"conflicts_count\": conflicts_count,\n \"next_steps\": [\n \"\\u041e\\u0442\\u043a\\u0440\\u043e\\u0439\\u0442\\u0435 \\u043b\\u043e\\u043a\\u0430\\u043b\\u044c\\u043d\\u044b\\u0439 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0439 \\u043f\\u043e \\u043f\\u0443\\u0442\\u0438 repository_path\",\n \"\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u044c\\u0442\\u0435 \\u0441\\u043e\\u0441\\u0442\\u043e\\u044f\\u043d\\u0438\\u0435: git status\",\n \"\\u0420\\u0430\\u0437\\u0440\\u0435\\u0448\\u0438\\u0442\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b \\u0438 \\u0432\\u044b\\u043f\\u043e\\u043b\\u043d\\u0438\\u0442\\u0435 commit, \\u043b\\u0438\\u0431\\u043e \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435: git merge --abort\",\n \"\\u041f\\u043e\\u0441\\u043b\\u0435 \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0435\\u043d\\u0438\\u044f/\\u043e\\u0442\\u043c\\u0435\\u043d\\u044b \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u044f \\u043f\\u043e\\u0432\\u0442\\u043e\\u0440\\u0438\\u0442\\u0435 Pull \\u0438\\u0437 \\u0438\\u043d\\u0442\\u0435\\u0440\\u0444\\u0435\\u0439\\u0441\\u0430\",\n ],\n \"manual_commands\": [\"git status\", \"git add \", 'git commit -m \"resolve merge conflicts\"', \"git merge --abort\"],\n }\n # [/DEF:_build_unfinished_merge_payload:Function]\n" }, @@ -85789,6 +88290,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85833,6 +88343,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85877,6 +88396,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85896,7 +88424,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Mixin providing GitLab API operations for GitService." }, "relations": [], @@ -85948,6 +88476,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -85992,6 +88529,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86011,7 +88557,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues.", "SEMANTICS": [ @@ -86043,6 +88589,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -86061,7 +88608,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns (staged, modified, untracked) tuple of file path lists.", "PRE": "`repo` is an open GitPython Repo instance.", "PURPOSE": "Parse git status --porcelain output into staged, modified, and untracked file lists.", @@ -86086,6 +88633,15 @@ "actual_complexity": 2, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -86125,6 +88681,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86176,6 +88741,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86227,6 +88801,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86246,7 +88829,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Push and pull operations for GitService with origin host auto-alignment.", "SEMANTICS": [ @@ -86284,6 +88867,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -86302,7 +88886,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.", "SEMANTICS": [ @@ -86346,6 +88930,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -86363,6 +88948,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -86380,6 +88966,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -86423,6 +89010,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86467,6 +89063,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86511,6 +89116,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86555,6 +89169,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86599,6 +89222,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86643,6 +89275,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86687,6 +89328,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -86706,7 +89356,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Re-export shim — GitService has been decomposed into services/git/ package." }, "relations": [], @@ -86722,7 +89372,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -86747,7 +89399,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -86773,7 +89427,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain/Service", "PURPOSE": "Business logic for aggregating dashboard health status from validation records.", "SEMANTICS": [ @@ -86809,20 +89463,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain/Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain/Service" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -86835,6 +89475,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -86852,6 +89493,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -86869,6 +89511,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -86886,6 +89529,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -86904,7 +89548,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]", "POST": "Exposes health summary aggregation and validation report deletion operations.", "PRE": "Service is constructed with a live SQLAlchemy session and optional config manager.", @@ -86971,6 +89615,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -86988,6 +89633,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87005,6 +89651,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87022,6 +89669,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87039,6 +89687,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87056,6 +89705,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87074,7 +89724,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]", "POST": "Service is ready to aggregate summaries and delete health reports.", "PRE": "db is a valid SQLAlchemy session.", @@ -87138,6 +89788,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -87156,7 +89807,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[records: List[ValidationRecord]] -> Output[None]", "POST": "Numeric dashboard ids for known environments are cached when discoverable.", "PRE": "records may contain mixed numeric and slug dashboard identifiers.", @@ -87232,6 +89883,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87249,6 +89901,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87266,6 +89919,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87284,7 +89938,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns dict with `slug` and `title` keys, using cache when possible.", "PRE": "dashboard_id may be numeric or slug-like; environment_id may be empty.", "PURPOSE": "Resolve slug/title for a dashboard referenced by persisted validation record.", @@ -87310,6 +89964,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -87332,7 +89995,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]", "POST": "Returns True only when a matching record was deleted.", "PRE": "record_id is a validation record identifier.", @@ -87408,6 +90071,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87425,6 +90089,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87442,6 +90107,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87460,7 +90126,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "All required prompt template keys are always present after normalization.", "LAYER": "Domain", "PURPOSE": "Provide default LLM prompt templates and normalization helpers for runtime usage.", @@ -87511,7 +90177,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Default prompt templates used by documentation, dashboard validation, and git commit generation." }, "relations": [], @@ -87527,7 +90193,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -87552,7 +90220,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -87578,7 +90248,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Default provider binding per task domain." }, "relations": [], @@ -87594,7 +90264,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -87619,7 +90291,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -87645,7 +90319,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Default planner settings for assistant chat intent model/provider resolution." }, "relations": [], @@ -87661,7 +90335,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -87686,7 +90362,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -87712,7 +90390,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returned dict contains prompts with all required template keys.", "PRE": "llm_settings is dictionary-like value or None.", "PURPOSE": "Ensure llm settings contain stable schema with prompts section and default templates." @@ -87757,7 +90435,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns True when model likely supports multimodal input.", "PRE": "model_name may be empty or mixed-case.", "PURPOSE": "Heuristically determine whether model supports image input required for dashboard validation." @@ -87802,7 +90480,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns configured provider id or fallback id/empty string when not defined.", "PRE": "llm_settings is normalized or raw dict from config.", "PURPOSE": "Resolve provider id configured for a task binding with fallback to default provider." @@ -87847,7 +90525,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns rendered prompt text with known placeholders substituted.", "PRE": "template is a string and variables values are already stringifiable.", "PURPOSE": "Render prompt template using deterministic placeholder replacement with graceful fallback." @@ -87892,7 +90570,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Service for managing LLM provider configurations with encrypted API keys.", "SEMANTICS": [ @@ -87935,6 +90613,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87952,6 +90631,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87969,6 +90649,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -87987,7 +90668,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[ENCRYPTION_KEY:str] -> Output[bytes]", "INVARIANT": "Encryption initialization never falls back to a hardcoded secret.", "POST": "Returns validated key bytes ready for Fernet initialization.", @@ -88003,7 +90684,26 @@ "target_ref": "[backend.src.core.logger:Function]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_require_fernet_key:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Load and validate the Fernet key used for secret encryption.\n# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.\n# @POST: Returns validated key bytes ready for Fernet initialization.\n# @RELATION: DEPENDS_ON ->[backend.src.core.logger:Function]\n# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.\n# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]\n# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.\ndef _require_fernet_key() -> bytes:\n with belief_scope(\"_require_fernet_key\"):\n raw_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if not raw_key:\n logger.explore(\n \"Missing ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be set\")\n\n key = raw_key.encode()\n try:\n Fernet(key)\n except Exception as exc:\n logger.explore(\n \"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be a valid Fernet key\") from exc\n\n logger.reflect(\"Validated ENCRYPTION_KEY for EncryptionManager initialization\")\n return key\n\n\n# [/DEF:_require_fernet_key:Function]\n" }, @@ -88016,7 +90716,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[str] -> Output[str]", "INVARIANT": "Uses only a validated secret key from environment.", "POST": "Manager exposes reversible encrypt/decrypt operations for persisted secrets.", @@ -88055,6 +90755,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -88067,6 +90785,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -88108,6 +90827,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -88145,6 +90873,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -88182,6 +90919,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -88196,7 +90942,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Service to manage LLM provider lifecycle." }, "relations": [ @@ -88232,6 +90978,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88249,6 +90996,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88266,6 +91014,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88315,6 +91064,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -88336,6 +91094,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88354,7 +91113,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns list of all LLMProvider records.", "PRE": "Database connection must be active.", "PURPOSE": "Returns all configured LLM providers." @@ -88398,6 +91157,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88416,7 +91176,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns LLMProvider or None if not found.", "PRE": "provider_id must be a valid string.", "PURPOSE": "Returns a single LLM provider by ID." @@ -88460,6 +91220,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88478,7 +91239,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns decrypted API key or None on failure.", "PRE": "provider_id must exist with valid encrypted key.", "PURPOSE": "Returns the decrypted API key for a provider." @@ -88528,6 +91289,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -88545,6 +91307,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -88563,7 +91326,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]", "INVARIANT": "Suggestions are based on database names.", "LAYER": "Service", @@ -88611,20 +91374,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -88665,7 +91414,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[config_manager] -> Output[List[Dict]]", "POST": "Provides client resolution and mapping suggestion methods.", "PRE": "config_manager exposes get_environments() with environment objects containing id.", @@ -88736,7 +91485,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "env_id (str) - The ID of the environment.", "POST": "Returns an initialized SupersetClient.", "PRE": "environment must exist in config.", @@ -88795,7 +91544,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "target_env_id (str) - Target environment ID.", "POST": "Returns fuzzy-matched database suggestions.", "PRE": "Both environments must be accessible.", @@ -88883,7 +91632,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -88917,6 +91668,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -88935,7 +91687,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Unit tests for NotificationService routing and dispatch logic." }, "relations": [ @@ -88947,15 +91699,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -88977,6 +91720,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -88995,7 +91739,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[target, subject, body, context?] -> Output[bool]", "INVARIANT": "Sensitive credentials must be handled via encrypted config.", "LAYER": "Infra", @@ -89045,6 +91789,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -89057,6 +91819,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDED_ON_BY]" @@ -89074,6 +91837,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89091,6 +91855,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89108,6 +91873,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89125,6 +91891,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89143,7 +91910,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Abstract base class for all notification providers." }, "relations": [ @@ -89188,6 +91955,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDED_ON_BY]" @@ -89205,6 +91973,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDED_ON_BY]" @@ -89222,6 +91991,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDED_ON_BY]" @@ -89240,7 +92010,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Delivers notifications via SMTP." }, "relations": [ @@ -89264,6 +92034,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[INHERITS]" @@ -89282,7 +92053,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Delivers notifications via Telegram Bot API." }, "relations": [ @@ -89306,6 +92077,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[INHERITS]" @@ -89324,7 +92096,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Delivers notifications via Slack Webhooks or API." }, "relations": [ @@ -89348,6 +92120,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[INHERITS]" @@ -89366,7 +92139,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]", "POST": "Service can resolve targets and dispatch provider sends without mutating validation records.", "PRE": "Service receives a live DB session and configuration manager with notification payload settings.", @@ -89421,6 +92194,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89438,6 +92212,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89455,6 +92230,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89472,6 +92248,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89490,7 +92267,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Bind DB and configuration collaborators used for provider initialization and routing." }, "relations": [ @@ -89520,6 +92297,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -89537,6 +92315,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89555,7 +92334,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Materialize configured notification channel adapters once per service lifetime." }, "relations": [ @@ -89591,6 +92370,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89608,6 +92388,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89625,6 +92406,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89643,7 +92425,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]", "POST": "Eligible notification sends are scheduled in background or awaited inline.", "PRE": "record is persisted and providers can be initialized from configuration payload.", @@ -89698,6 +92480,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -89715,6 +92498,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -89732,6 +92516,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -89749,6 +92534,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -89767,7 +92553,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Evaluate record status against effective alert policy." }, "relations": [ @@ -89797,6 +92583,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89814,6 +92601,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89832,7 +92620,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Resolve owner and policy-defined delivery targets for one validation record." }, "relations": [ @@ -89868,6 +92656,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -89885,6 +92674,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89902,6 +92692,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89920,7 +92711,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Load candidate dashboard owners from persisted profile preferences." }, "relations": [ @@ -89950,6 +92741,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89967,6 +92759,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -89985,7 +92778,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Format one validation record into provider-ready body text." }, "relations": [ @@ -90018,6 +92811,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90036,7 +92830,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Profile_id -> ProfileInfo; session_id -> valid UUID", "INVARIANT": "Profile ID needs to unique per-user session", "LAYER": "Domain", @@ -90160,10 +92954,22 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -90178,7 +92984,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Domain validation error for profile preference update requests." }, "relations": [ @@ -90212,7 +93018,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Raised when environment_id from lookup request is unknown in app configuration." }, "relations": [ @@ -90246,7 +93052,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Raised when caller attempts cross-user preference mutation." }, "relations": [ @@ -90280,7 +93086,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]", "INVARIANT": "Profile data integrity maintained, cache consistency with database state", "POST": "Preference operations remain user-scoped and return normalized profile/lookup responses.", @@ -90327,6 +93133,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -90339,6 +93163,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90356,6 +93181,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90373,6 +93199,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90390,6 +93217,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90407,6 +93235,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -90424,6 +93253,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -90473,6 +93303,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90526,6 +93365,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90579,6 +93427,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90632,6 +93489,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90685,6 +93551,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90738,6 +93613,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90791,6 +93675,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90844,6 +93737,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90897,6 +93799,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -90950,6 +93861,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91003,6 +93923,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91056,6 +93985,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91109,6 +94047,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91162,6 +94109,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91215,6 +94171,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91268,6 +94233,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91321,6 +94295,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91374,6 +94357,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91427,6 +94419,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91480,6 +94481,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91533,6 +94543,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91586,6 +94605,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -91608,21 +94636,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database." }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:rbac_permission_catalog:Module]\n#\n# @COMPLEXITY: 2\n# @PURPOSE: Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database.\n\n# [SECTION: IMPORTS]\nimport re\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Iterable, Set, Tuple\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import Permission\n# [/SECTION: IMPORTS]\n\n# [DEF:HAS_PERMISSION_PATTERN:Constant]\n# @PURPOSE: Regex pattern for extracting has_permission(\"resource\", \"ACTION\") declarations.\nHAS_PERMISSION_PATTERN = re.compile(\n r\"\"\"has_permission\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([A-Z]+)['\"]\\s*\\)\"\"\"\n)\n# [/DEF:HAS_PERMISSION_PATTERN:Constant]\n\n# [DEF:ROUTES_DIR:Constant]\n# @PURPOSE: Absolute directory path where API route RBAC declarations are defined.\nROUTES_DIR = Path(__file__).resolve().parent.parent / \"api\" / \"routes\"\n# [/DEF:ROUTES_DIR:Constant]\n\n\n# [DEF:_iter_route_files:Function]\n# @PURPOSE: Iterates API route files that may contain RBAC declarations.\n# @PRE: ROUTES_DIR points to backend/src/api/routes.\n# @POST: Yields Python files excluding test and cache directories.\n# @RETURN: Iterable[Path] - Route file paths for permission extraction.\ndef _iter_route_files() -> Iterable[Path]:\n with belief_scope(\"rbac_permission_catalog._iter_route_files\"):\n if not ROUTES_DIR.exists():\n return []\n\n files = []\n for file_path in ROUTES_DIR.rglob(\"*.py\"):\n path_parts = set(file_path.parts)\n if \"__tests__\" in path_parts or \"__pycache__\" in path_parts:\n continue\n files.append(file_path)\n return files\n# [/DEF:_iter_route_files:Function]\n\n\n# [DEF:_discover_route_permissions:Function]\n# @PURPOSE: Extracts explicit has_permission declarations from API route source code.\n# @PRE: Route files are readable UTF-8 text files.\n# @POST: Returns unique set of (resource, action) pairs declared in route guards.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations.\ndef _discover_route_permissions() -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n for route_file in _iter_route_files():\n try:\n source = route_file.read_text(encoding=\"utf-8\")\n except OSError as read_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read route file %s: %s\",\n route_file,\n read_error,\n )\n continue\n\n for resource, action in HAS_PERMISSION_PATTERN.findall(source):\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n discovered.add((normalized_resource, normalized_action))\n return discovered\n# [/DEF:_discover_route_permissions:Function]\n\n\n# [DEF:_discover_route_permissions_cached:Function]\n# @PURPOSE: Cache route permission discovery because route source files are static during normal runtime.\n# @PRE: None.\n# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.\n@lru_cache(maxsize=1)\ndef _discover_route_permissions_cached() -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions_cached\"):\n return tuple(sorted(_discover_route_permissions()))\n# [/DEF:_discover_route_permissions_cached:Function]\n\n\n# [DEF:_discover_plugin_execute_permissions:Function]\n# @PURPOSE: Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.\n# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.\n# @POST: Returns unique plugin EXECUTE permissions if loader is available.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs.\ndef _discover_plugin_execute_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n if plugin_loader is None:\n return discovered\n\n try:\n plugin_configs = plugin_loader.get_all_plugin_configs()\n except Exception as plugin_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read plugin configs for RBAC discovery: %s\",\n plugin_error,\n )\n return discovered\n\n for plugin_config in plugin_configs:\n plugin_id = str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n if plugin_id:\n discovered.add((f\"plugin:{plugin_id}\", \"EXECUTE\"))\n return discovered\n# [/DEF:_discover_plugin_execute_permissions:Function]\n\n\n# [DEF:_discover_plugin_execute_permissions_cached:Function]\n# @PURPOSE: Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.\n# @PRE: plugin_ids is a deterministic tuple of plugin ids.\n# @POST: Returns stable permission tuple without repeated plugin catalog expansion.\n@lru_cache(maxsize=8)\ndef _discover_plugin_execute_permissions_cached(\n plugin_ids: Tuple[str, ...],\n) -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions_cached\"):\n return tuple((f\"plugin:{plugin_id}\", \"EXECUTE\") for plugin_id in plugin_ids)\n# [/DEF:_discover_plugin_execute_permissions_cached:Function]\n\n\n# [DEF:discover_declared_permissions:Function]\n# @PURPOSE: Builds canonical RBAC permission catalog from routes and plugin registry.\n# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.\n# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.\n# @RETURN: Set[Tuple[str, str]] - Complete discovered permission set.\ndef discover_declared_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog.discover_declared_permissions\"):\n permissions = set(_discover_route_permissions_cached())\n plugin_ids = tuple(\n sorted(\n {\n str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])\n if str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n }\n )\n )\n permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))\n return permissions\n# [/DEF:discover_declared_permissions:Function]\n\n\n# [DEF:sync_permission_catalog:Function]\n# @PURPOSE: Persists missing RBAC permission pairs into auth database.\n# @PRE: db is a valid SQLAlchemy session bound to auth database.\n# @PRE: declared_permissions is an iterable of (resource, action) tuples.\n# @POST: Missing permissions are inserted; existing permissions remain untouched.\n# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.\n# @RETURN: int - Number of inserted permission records.\ndef sync_permission_catalog(\n db: Session,\n declared_permissions: Iterable[Tuple[str, str]],\n) -> int:\n with belief_scope(\"rbac_permission_catalog.sync_permission_catalog\"):\n normalized_declared: Set[Tuple[str, str]] = set()\n for resource, action in declared_permissions:\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n normalized_declared.add((normalized_resource, normalized_action))\n\n existing_permissions = db.query(Permission).all()\n existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}\n\n missing_pairs = sorted(normalized_declared - existing_pairs)\n for resource, action in missing_pairs:\n db.add(Permission(resource=resource, action=action))\n\n if missing_pairs:\n db.commit()\n\n return len(missing_pairs)\n# [/DEF:sync_permission_catalog:Function]\n\n# [/DEF:rbac_permission_catalog:Module]\n" }, @@ -91651,7 +94669,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -91693,7 +94713,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -91744,6 +94766,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -91788,6 +94819,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -91830,6 +94870,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -91869,6 +94918,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -91911,6 +94969,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -91950,6 +95017,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -91995,6 +95071,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -92039,7 +95124,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -92065,7 +95152,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Unknown plugin types are mapped to canonical unknown task type.", "LAYER": "Domain (Tests)", "PURPOSE": "Validate unknown task type fallback and partial payload normalization behavior.", @@ -92094,20 +95181,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -92129,6 +95202,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -92246,7 +95320,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain", "PURPOSE": "Unit tests for ReportsService list/detail operations" }, @@ -92280,6 +95354,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -92310,9 +95385,9 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -92461,12 +95536,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -92522,7 +95591,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "ReportRow -> NormalizerInput; session_id -> valid UUID", "INVARIANT": "Normalizer instance maintains consistent field order", "LAYER": "Domain", @@ -92557,7 +95626,26 @@ "target_ref": "[backend.src.services.reports.type_profiles:Function]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:normalizer:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: reports, normalization, tasks, fallback\n# @PURPOSE: Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON ->[backend.src.core.task_manager.models.Task:Function]\n# @RELATION: DEPENDS_ON ->[backend.src.models.report:Function]\n# @RELATION: DEPENDS_ON ->[backend.src.services.reports.type_profiles:Function]\n# @INVARIANT: Normalizer instance maintains consistent field order\n# @DATA_CONTRACT: ReportRow -> NormalizerInput; session_id -> valid UUID\n# @PRE: session is active and valid\n# @POST: Returns Normalizer output with normalized fields\n# @SIDE_EFFECT: Read-only database operations\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager.models import Task, TaskStatus\nfrom ...models.report import ErrorContext, ReportStatus, TaskReport\nfrom .type_profiles import get_type_profile, resolve_task_type\n# [/SECTION]\n\n\n# [DEF:status_to_report_status:Function]\n# @PURPOSE: Normalize internal task status to canonical report status.\n# @PRE: status may be known or unknown string/enum value.\n# @POST: Always returns one of canonical ReportStatus values.\n# @PARAM: status (Any) - Internal task status value.\n# @RETURN: ReportStatus - Canonical report status.\ndef status_to_report_status(status: Any) -> ReportStatus:\n with belief_scope(\"status_to_report_status\"):\n raw = str(status.value if isinstance(status, TaskStatus) else status).upper()\n if raw == TaskStatus.SUCCESS.value:\n return ReportStatus.SUCCESS\n if raw == TaskStatus.FAILED.value:\n return ReportStatus.FAILED\n if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:\n return ReportStatus.IN_PROGRESS\n return ReportStatus.PARTIAL\n# [/DEF:status_to_report_status:Function]\n\n\n# [DEF:build_summary:Function]\n# @PURPOSE: Build deterministic user-facing summary from task payload and status.\n# @PRE: report_status is canonical; plugin_id may be unknown.\n# @POST: Returns non-empty summary text.\n# @PARAM: task (Task) - Source task object.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: str - Normalized summary.\ndef build_summary(task: Task, report_status: ReportStatus) -> str:\n with belief_scope(\"build_summary\"):\n result = task.result\n if isinstance(result, dict):\n for key in (\"summary\", \"message\", \"status_message\", \"description\"):\n value = result.get(key)\n if isinstance(value, str) and value.strip():\n return value.strip()\n if report_status == ReportStatus.SUCCESS:\n return \"Task completed successfully\"\n if report_status == ReportStatus.FAILED:\n return \"Task failed\"\n if report_status == ReportStatus.IN_PROGRESS:\n return \"Task is in progress\"\n return \"Task completed with partial data\"\n# [/DEF:build_summary:Function]\n\n\n# [DEF:extract_error_context:Function]\n# @PURPOSE: Extract normalized error context and next actions for failed/partial reports.\n# @PRE: task is a valid Task object.\n# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.\n# @PARAM: task (Task) - Source task.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: Optional[ErrorContext] - Error context block.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:\n with belief_scope(\"extract_error_context\"):\n if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n return None\n\n result = task.result if isinstance(task.result, dict) else {}\n message = None\n code = None\n next_actions = []\n\n if isinstance(result.get(\"error\"), dict):\n error_obj = result.get(\"error\", {})\n message = error_obj.get(\"message\") or message\n code = error_obj.get(\"code\") or code\n actions = error_obj.get(\"next_actions\")\n if isinstance(actions, list):\n next_actions = [str(action) for action in actions if str(action).strip()]\n\n if not message:\n message = result.get(\"error_message\") if isinstance(result.get(\"error_message\"), str) else None\n\n if not message:\n for log in reversed(task.logs):\n if str(log.level).upper() == \"ERROR\" and log.message:\n message = log.message\n break\n\n if not message:\n message = \"Not provided\"\n\n if not next_actions:\n next_actions = [\"Review task diagnostics\", \"Retry the operation\"]\n\n return ErrorContext(code=code, message=message, next_actions=next_actions)\n# [/DEF:extract_error_context:Function]\n\n\n# [DEF:normalize_task_report:Function]\n# @PURPOSE: Convert one Task to canonical TaskReport envelope.\n# @PRE: task has valid id and plugin_id fields.\n# @POST: Returns TaskReport with required fields and deterministic fallback behavior.\n# @PARAM: task (Task) - Source task.\n# @RETURN: TaskReport - Canonical normalized report.\n#\n# @TEST_CONTRACT: NormalizeTaskReport ->\n# {\n# required_fields: {task: Task},\n# invariants: [\n# \"Returns a valid TaskReport object\",\n# \"Maps TaskStatus to ReportStatus deterministically\",\n# \"Extracts ErrorContext for FAILED/PARTIAL tasks\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task -> {\"task\": \"MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)\"}\n# @TEST_EDGE: task_with_error -> {\"task\": \"MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])\"}\n# @TEST_EDGE: unknown_plugin_type -> {\"task\": \"MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)\"}\n# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]\ndef normalize_task_report(task: Task) -> TaskReport:\n with belief_scope(\"normalize_task_report\"):\n task_type = resolve_task_type(task.plugin_id)\n report_status = status_to_report_status(task.status)\n profile = get_type_profile(task_type)\n\n started_at = task.started_at if isinstance(task.started_at, datetime) else None\n updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None\n if not updated_at:\n updated_at = started_at or datetime.utcnow()\n\n details: Dict[str, Any] = {\n \"profile\": {\n \"display_label\": profile.get(\"display_label\"),\n \"visual_variant\": profile.get(\"visual_variant\"),\n \"icon_token\": profile.get(\"icon_token\"),\n \"emphasis_rules\": profile.get(\"emphasis_rules\", []),\n },\n \"result\": task.result if task.result is not None else {\"note\": \"Not provided\"},\n }\n\n source_ref: Dict[str, Any] = {}\n if isinstance(task.params, dict):\n for key in (\"environment_id\", \"source_env_id\", \"target_env_id\", \"dashboard_id\", \"dataset_id\", \"resource_id\"):\n if key in task.params:\n source_ref[key] = task.params.get(key)\n\n return TaskReport(\n report_id=task.id,\n task_id=task.id,\n task_type=task_type,\n status=report_status,\n started_at=started_at,\n updated_at=updated_at,\n summary=build_summary(task, report_status),\n details=details,\n error_context=extract_error_context(task, report_status),\n source_ref=source_ref or None,\n )\n# [/DEF:normalize_task_report:Function]\n\n# [/DEF:normalizer:Module]\n" }, @@ -92602,6 +95690,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -92653,6 +95750,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -92704,6 +95810,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -92756,6 +95871,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -92775,7 +95899,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "ReportQuery -> ReportRow; session_id -> valid UUID", "INVARIANT": "ReportService maintains consistent report structure", "LAYER": "Domain", @@ -92837,6 +95961,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -92849,6 +95991,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92866,6 +96009,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92883,6 +96027,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92900,6 +96045,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92917,6 +96063,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92934,6 +96081,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92951,6 +96099,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -92969,7 +96118,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]", "INVARIANT": "Service methods are read-only over task history source.", "POST": "Provides deterministic list/detail report responses.", @@ -93020,6 +96169,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -93032,6 +96199,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -93049,6 +96217,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -93066,6 +96235,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -93119,6 +96289,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93180,6 +96359,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93241,6 +96429,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93302,6 +96499,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93363,6 +96569,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93382,21 +96597,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata." }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:type_profiles:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata.\n\n# [SECTION: IMPORTS]\nfrom typing import Any, Dict, Optional\n\nfrom ...core.logger import belief_scope\nfrom ...models.report import TaskType\n# [/SECTION]\n\n# [DEF:PLUGIN_TO_TASK_TYPE:Data]\n# @PURPOSE: Maps plugin identifiers to normalized report task types.\nPLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = {\n \"llm_dashboard_validation\": TaskType.LLM_VERIFICATION,\n \"superset-backup\": TaskType.BACKUP,\n \"superset-migration\": TaskType.MIGRATION,\n \"documentation\": TaskType.DOCUMENTATION,\n \"clean-release-compliance\": TaskType.CLEAN_RELEASE,\n \"clean_release_compliance\": TaskType.CLEAN_RELEASE,\n}\n# [/DEF:PLUGIN_TO_TASK_TYPE:Data]\n\n# [DEF:TASK_TYPE_PROFILES:Data]\n# @PURPOSE: Profile metadata registry for each normalized task type.\nTASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {\n TaskType.LLM_VERIFICATION: {\n \"display_label\": \"LLM Verification\",\n \"visual_variant\": \"llm\",\n \"icon_token\": \"sparkles\",\n \"emphasis_rules\": [\"summary\", \"status\", \"next_actions\"],\n \"fallback\": False,\n },\n TaskType.BACKUP: {\n \"display_label\": \"Backup\",\n \"visual_variant\": \"backup\",\n \"icon_token\": \"archive\",\n \"emphasis_rules\": [\"summary\", \"status\", \"updated_at\"],\n \"fallback\": False,\n },\n TaskType.MIGRATION: {\n \"display_label\": \"Migration\",\n \"visual_variant\": \"migration\",\n \"icon_token\": \"shuffle\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\"],\n \"fallback\": False,\n },\n TaskType.DOCUMENTATION: {\n \"display_label\": \"Documentation\",\n \"visual_variant\": \"documentation\",\n \"icon_token\": \"file-text\",\n \"emphasis_rules\": [\"summary\", \"status\", \"details\"],\n \"fallback\": False,\n },\n TaskType.CLEAN_RELEASE: {\n \"display_label\": \"Clean Release\",\n \"visual_variant\": \"clean-release\",\n \"icon_token\": \"shield-check\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\", \"details\"],\n \"fallback\": False,\n },\n TaskType.UNKNOWN: {\n \"display_label\": \"Other / Unknown\",\n \"visual_variant\": \"unknown\",\n \"icon_token\": \"help-circle\",\n \"emphasis_rules\": [\"summary\", \"status\"],\n \"fallback\": True,\n },\n}\n# [/DEF:TASK_TYPE_PROFILES:Data]\n\n\n# [DEF:resolve_task_type:Function]\n# @PURPOSE: Resolve canonical task type from plugin/task identifier with guaranteed fallback.\n# @PRE: plugin_id may be None or unknown.\n# @POST: Always returns one of TaskType enum values.\n# @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record.\n# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.\n#\n# @TEST_CONTRACT: ResolveTaskType ->\n# {\n# required_fields: {plugin_id: str},\n# invariants: [\"returns TaskType.UNKNOWN for missing/unmapped plugin_id\"]\n# }\n# @TEST_FIXTURE: valid_plugin -> {\"plugin_id\": \"superset-migration\"}\n# @TEST_EDGE: empty_plugin -> {\"plugin_id\": \"\"}\n# @TEST_EDGE: none_plugin -> {\"plugin_id\": None}\n# @TEST_EDGE: unknown_plugin -> {\"plugin_id\": \"invalid-plugin\"}\n# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]\ndef resolve_task_type(plugin_id: Optional[str]) -> TaskType:\n with belief_scope(\"resolve_task_type\"):\n normalized = (plugin_id or \"\").strip()\n if not normalized:\n return TaskType.UNKNOWN\n return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)\n\n\n# [/DEF:resolve_task_type:Function]\n\n\n# [DEF:get_type_profile:Function]\n# @PURPOSE: Return deterministic profile metadata for a task type.\n# @PRE: task_type may be known or unknown.\n# @POST: Returns a profile dict and never raises for unknown types.\n# @PARAM: task_type (TaskType) - Canonical task type.\n# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.\n#\n# @TEST_CONTRACT: GetTypeProfile ->\n# {\n# required_fields: {task_type: TaskType},\n# invariants: [\"returns a valid metadata dictionary even for UNKNOWN\"]\n# }\n# @TEST_FIXTURE: valid_profile -> {\"task_type\": \"migration\"}\n# @TEST_EDGE: missing_profile -> {\"task_type\": \"some_new_type\"}\n# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]\ndef get_type_profile(task_type: TaskType) -> Dict[str, Any]:\n with belief_scope(\"get_type_profile\"):\n return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])\n\n\n# [/DEF:get_type_profile:Function]\n\n# [/DEF:type_profiles:Module]\n" }, @@ -93425,7 +96630,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -93467,7 +96674,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -93526,6 +96735,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93578,6 +96796,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -93597,7 +96824,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All resources include metadata about their current state", "LAYER": "Service", "PURPOSE": "Shared service for fetching resource data with Git status and task status", @@ -93645,20 +96872,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service" - } } ], "anchor_syntax": "def", @@ -93673,7 +96886,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Provides centralized access to resource data with enhanced metadata" }, "relations": [ @@ -93703,7 +96916,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "ResourceService is ready to fetch resources", "PRE": "None", "PURPOSE": "Initialize the resource service with dependencies" @@ -93727,6 +96940,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -93741,7 +96963,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (List[Task]) - List of tasks to check for status", "POST": "Returns list of dashboards with enhanced metadata", "PRE": "env is a valid Environment object", @@ -93812,7 +97034,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "page_size (int) - Page size.", "POST": "Returns page items plus total counters without scanning all pages locally.", "PRE": "env is valid; page >= 1; page_size > 0.", @@ -93883,7 +97105,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (Optional[List[Task]]) - List of tasks to search", "POST": "Returns the newest llm_dashboard_validation task summary or None", "PRE": "dashboard_id is a valid integer identifier", @@ -93954,7 +97176,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "raw_status (Any) - Raw task status object/value", "POST": "Returns uppercase status without enum class prefix", "PRE": "raw_status can be enum or string", @@ -94012,6 +97234,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94030,7 +97253,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "raw_status (Any) - Raw validation status from task result", "POST": "Returns normalized validation status token or None", "PRE": "raw_status can be any scalar type", @@ -94088,6 +97311,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94106,7 +97330,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "value (Any) - Candidate datetime-like value.", "POST": "Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.", "PRE": "value may be datetime or any scalar.", @@ -94170,6 +97394,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94187,6 +97412,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94205,7 +97431,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (List[Task]) - List of tasks to check for status", "POST": "Returns list of datasets with enhanced metadata", "PRE": "env is a valid Environment object", @@ -94270,7 +97496,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (List[Task]) - List of tasks to summarize", "POST": "Returns summary with active_count and recent_tasks", "PRE": "tasks is a list of Task objects", @@ -94335,7 +97561,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "dashboard_id (int) - The dashboard ID", "POST": "Returns git status or None if no repo exists", "PRE": "dashboard_id is a valid integer", @@ -94394,7 +97620,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (Optional[List[Task]]) - List of tasks to search", "POST": "Returns task summary or None if no tasks found", "PRE": "resource_id is a valid string", @@ -94453,7 +97679,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task (Task) - The task to extract from", "POST": "Returns resource name or task ID", "PRE": "task is a valid Task object", @@ -94511,6 +97737,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94529,7 +97756,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task (Task) - The task to extract from", "POST": "Returns resource type or 'unknown'", "PRE": "task is a valid Task object", @@ -94587,6 +97814,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -94605,7 +97833,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationArchiveParser ZIP extraction contract." }, @@ -94644,12 +97872,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -94672,7 +97894,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationDryRunService diff and risk computation contracts." }, @@ -94708,6 +97930,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -94745,18 +97976,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -95019,7 +98238,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "A 404 from primary Gitea URL retries once against remote-url host when different.", "LAYER": "Domain", "PURPOSE": "Validate Gitea PR creation fallback behavior when configured server URL is stale.", @@ -95061,6 +98280,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -95238,7 +98458,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for the IdMappingService matching UUIDs to integer IDs." }, @@ -95703,7 +98923,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationEngine's cross-filter patching algorithms." }, @@ -95728,7 +98948,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Returns mappings only for requested UUID keys present in seeded map.", "INVARIANT_VIOLATION": "resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.", "PURPOSE": "Deterministic mapping service double for native filter ID remapping scenarios." @@ -95790,6 +99010,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -96142,7 +99371,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Smoke tests for the redesigned clean release CLI." }, @@ -96167,6 +99396,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -96327,7 +99557,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.", "LAYER": "Scripts", "PURPOSE": "Unit tests for the interactive curses TUI of the clean release process.", @@ -96356,20 +99586,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Scripts" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -96382,6 +99598,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -96640,7 +99857,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Smoke tests for thin-client TUI action dispatch and blocked transition behavior." }, @@ -96665,6 +99882,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -96694,6 +99912,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -96848,7 +100075,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.", "LAYER": "Tests", "PURPOSE": "Define approval gate contracts for approve/reject operations over immutable compliance evidence.", @@ -96869,20 +100096,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -96910,6 +100123,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -96931,6 +100162,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -96980,6 +100212,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -97267,7 +100508,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Test lifecycle and manifest versioning for release candidates." }, @@ -97280,20 +100521,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -97306,6 +100533,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -97401,6 +100629,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -97621,7 +100858,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Missing manifest prevents run startup; failed execution cannot finalize as PASSED.", "LAYER": "Tests", "PURPOSE": "Validate stage pipeline and run finalization contracts for compliance execution.", @@ -97642,20 +100879,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -97683,6 +100906,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -97704,6 +100945,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -97753,6 +100995,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -97934,7 +101185,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Compliance execution triggered as task produces terminal task status and persists run evidence.", "LAYER": "Tests", "PURPOSE": "Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.", @@ -97955,20 +101206,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -97996,6 +101233,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -98017,6 +101272,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -98066,6 +101322,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -98099,6 +101364,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -98121,7 +101395,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "CONTRACT": "Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.", "INVARIANT": "has_plugin/get_plugin only acknowledge the seeded compliance plugin id.", "PURPOSE": "Provide minimal plugin loader contract used by TaskManager in integration tests." @@ -98193,6 +101467,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -98246,6 +101529,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -98374,7 +101666,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Verify demo and real mode namespace isolation contracts before TUI integration.", "SEMANTICS": [ @@ -98393,22 +101685,7 @@ "target_ref": "backend.src.services.clean_release.demo_data_service" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestDemoModeIsolation:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository\n# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.\n# @LAYER: Tests\n# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.demo_data_service\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.services.clean_release.demo_data_service import (\n build_namespaced_id,\n create_isolated_repository,\n resolve_namespace,\n)\n\n\n# [DEF:test_resolve_namespace_separates_demo_and_real:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.\n# @PRE: Mode names are provided as user/runtime strings.\n# @POST: Demo and real namespaces are different and stable.\ndef test_resolve_namespace_separates_demo_and_real() -> None:\n demo = resolve_namespace(\"demo\")\n real = resolve_namespace(\"real\")\n\n assert demo == \"clean-release:demo\"\n assert real == \"clean-release:real\"\n assert demo != real\n# [/DEF:test_resolve_namespace_separates_demo_and_real:Function]\n\n\n# [DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.\n# @PRE: Same logical candidate id is used in two different namespaces.\n# @POST: Produced physical IDs differ by namespace prefix.\ndef test_build_namespaced_id_prevents_cross_mode_collisions() -> None:\n logical_id = \"2026.03.09-rc1\"\n demo_id = build_namespaced_id(resolve_namespace(\"demo\"), logical_id)\n real_id = build_namespaced_id(resolve_namespace(\"real\"), logical_id)\n\n assert demo_id != real_id\n assert demo_id.startswith(\"clean-release:demo::\")\n assert real_id.startswith(\"clean-release:real::\")\n# [/DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n\n\n# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n\n# [/DEF:TestDemoModeIsolation:Module]\n" }, @@ -98580,7 +101857,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Resolution uses only ConfigManager active IDs and rejects runtime override attempts.", "LAYER": "Tests", "PURPOSE": "Verify trusted policy snapshot resolution contract and error guards.", @@ -98612,20 +101889,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -98653,6 +101916,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -98675,7 +101956,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.", "POST": "Returns object exposing get_config().settings.clean_release active IDs.", "PRE": "policy_id and registry_id may be None or non-empty strings.", @@ -98717,6 +101998,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -98898,7 +102188,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.", "LAYER": "Tests", "PURPOSE": "Define publication gate contracts over approved candidates and immutable publication records.", @@ -98919,20 +102209,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -98960,6 +102236,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -98981,6 +102275,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -99030,6 +102325,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -99211,7 +102515,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Built reports are immutable snapshots; audit hooks produce append-only event traces.", "LAYER": "Tests", "PURPOSE": "Validate report snapshot immutability expectations and append-only audit hook behavior for US2.", @@ -99233,20 +102537,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -99274,6 +102564,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -99295,6 +102603,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -99472,7 +102781,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.", "SEMANTICS": [ @@ -99499,20 +102808,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Tests" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -99525,6 +102820,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99542,6 +102838,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99560,7 +102857,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Build an adapter with a mock Superset client and deterministic environment for compatibility tests." }, "relations": [ @@ -99593,6 +102890,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99611,7 +102909,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Confirms preview compilation uses a supported client method first when the capability exists." }, "relations": [ @@ -99644,6 +102942,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99662,7 +102961,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "FRAGILE": "Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.", "PURPOSE": "Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL." }, @@ -99693,6 +102992,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99711,7 +103011,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "FRAGILE": "Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.", "PURPOSE": "Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction." }, @@ -99742,6 +103042,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -99760,7 +103061,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Test", "PURPOSE": "Covers authentication service/repository behavior and auth bootstrap helpers." }, @@ -99803,6 +103104,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -99820,6 +103122,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -99837,6 +103140,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -99854,6 +103158,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "TESTS" @@ -100072,7 +103377,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain (Tests)", "PURPOSE": "Comprehensive contract-driven tests for Dashboard Hub API", "SEMANTICS": [ @@ -100091,22 +103396,7 @@ "target_ref": "[src.api.routes.dashboards]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestDashboardsApi:Module]\n# @RELATION: VERIFIES ->[src.api.routes.dashboards]\n# @COMPLEXITY: 3\n# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API\n# @LAYER: Domain (Tests)\n# @SEMANTICS: tests, dashboards, api, contract, remediation\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime, timezone\nfrom src.app import app\nfrom src.api.routes.dashboards import (\n DashboardsResponse,\n DashboardDetailResponse,\n DashboardTaskHistoryResponse,\n DatabaseMappingsResponse,\n)\nfrom src.dependencies import (\n get_current_user,\n has_permission,\n get_config_manager,\n get_task_manager,\n get_resource_service,\n get_mapping_service,\n)\n\n# Global mock user\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n config_manager = MagicMock()\n task_manager = MagicMock()\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # Overrides for specific permission checks\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n app.dependency_overrides[has_permission(\"dashboards\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n# --- 1. get_dashboards tests ---\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&page=1&page_size=10\")\n\n assert response.status_code == 200\n data = response.json()\n\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1 # @TEST_FIXTURE: expected_count: 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 10\n assert data[\"total_pages\"] == 1\n\n # schema validation\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\"id\": 1, \"title\": \"Sales Report\", \"slug\": \"sales\"},\n {\"id\": 2, \"title\": \"Marketing\", \"slug\": \"marketing\"},\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n\n\n# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n\n\n# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n\n\n# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # page < 1\n assert client.get(\"/api/dashboards?env_id=prod&page=0\").status_code == 400\n assert client.get(\"/api/dashboards?env_id=prod&page=-1\").status_code == 400\n\n # page_size < 1\n assert client.get(\"/api/dashboards?env_id=prod&page_size=0\").status_code == 400\n\n # page_size > 100\n assert client.get(\"/api/dashboards?env_id=prod&page_size=101\").status_code == 400\n\n\n# --- 2. get_database_mappings tests ---\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n\n\n# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[{\"source_db\": \"src\", \"target_db\": \"dst\", \"confidence\": 0.9}]\n )\n response = client.get(\"/api/dashboards/db-mappings?source_env_id=s&target_env_id=t\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.9\n DatabaseMappingsResponse(**data)\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n\n\n# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# --- 3. get_dashboard_detail tests ---\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n detail_payload = {\n \"id\": 42,\n \"title\": \"Detail\",\n \"charts\": [],\n \"datasets\": [],\n \"chart_count\": 0,\n \"dataset_count\": 0,\n }\n mock_client.get_dashboard_detail.return_value = detail_payload\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == 42\n DashboardDetailResponse(**data)\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n\n\n# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n assert response.status_code == 404\n\n\n# --- 4. get_dashboard_tasks_history tests ---\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(timezone.utc)\n task1 = MagicMock(\n id=\"t1\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=now,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n mock_deps[\"task\"].get_all_tasks.return_value = [task1]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 1\n DashboardTaskHistoryResponse(**data)\n\n\n# [/DEF:test_get_dashboard_tasks_history_success:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_sorting:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_sorting(mock_deps):\n \"\"\"@POST: Response contains sorted task history (newest first).\"\"\"\n from datetime import timedelta\n\n now = datetime.now(timezone.utc)\n older = now - timedelta(hours=2)\n newest = now\n\n task_old = MagicMock(\n id=\"t-old\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=older,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n task_new = MagicMock(\n id=\"t-new\",\n plugin_id=\"superset-backup\",\n status=\"RUNNING\",\n started_at=newest,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n\n # Provide in wrong order to verify the endpoint sorts\n mock_deps[\"task\"].get_all_tasks.return_value = [task_old, task_new]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"items\"]) == 2\n # Newest first\n assert data[\"items\"][0][\"id\"] == \"t-new\"\n assert data[\"items\"][1][\"id\"] == \"t-old\"\n\n\n# --- 5. get_dashboard_thumbnail tests ---\n\n# [/DEF:test_get_dashboard_tasks_history_sorting:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n mock_response = MagicMock(\n status_code=200, content=b\"img\", headers={\"Content-Type\": \"image/png\"}\n )\n mock_client.network.request.side_effect = (\n lambda method, endpoint, **kw: {\"image_url\": \"url\"}\n if method == \"POST\"\n else mock_response\n )\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 200\n assert response.content == b\"img\"\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=missing\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_202:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_202(mock_deps):\n \"\"\"@POST: Returns 202 when thumbnail is being prepared by Superset.\"\"\"\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n\n # POST cache_dashboard_screenshot returns image_url\n mock_client.network.request.side_effect = [\n {\"image_url\": \"/api/v1/dashboard/42/thumbnail/abc123/\"}, # POST\n MagicMock(\n status_code=202,\n json=lambda: {\"message\": \"Thumbnail is being generated\"},\n headers={\"Content-Type\": \"application/json\"},\n ), # GET thumbnail -> 202\n ]\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 202\n assert \"Thumbnail is being generated\" in response.json()[\"message\"]\n\n\n# --- 6. migrate_dashboards tests ---\n\n# [/DEF:test_get_dashboard_thumbnail_202:Function]\n\n\n# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"task-123\"\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n\n\n# [DEF:test_migrate_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_pre_checks(mock_deps):\n # Missing IDs\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": []},\n )\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_pre_checks:Function]\n\n\n# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# --- 7. backup_dashboards tests ---\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"backup-123\"))\n\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"backup-123\"\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n\n\n# [DEF:test_backup_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_pre_checks(mock_deps):\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": []}\n )\n assert response.status_code == 400\n\n\n# [/DEF:test_backup_dashboards_pre_checks:Function]\n\n\n# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_with_schedule:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_with_schedule(mock_deps):\n \"\"\"@POST: If schedule is provided, a scheduled task is created.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"sched-456\"))\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1], \"schedule\": \"0 0 * * *\"},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"sched-456\"\n\n # Verify schedule was propagated to create_task\n call_kwargs = mock_deps[\"task\"].create_task.call_args\n task_params = call_kwargs.kwargs.get(\"params\") or call_kwargs[1].get(\"params\", {})\n assert task_params[\"schedule\"] == \"0 0 * * *\"\n\n\n# --- 8. Internal logic: _task_matches_dashboard ---\n# [/DEF:test_backup_dashboards_with_schedule:Function]\n\nfrom src.api.routes.dashboards import _task_matches_dashboard\n\n\n# [DEF:test_task_matches_dashboard_logic:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_task_matches_dashboard_logic():\n task = MagicMock(\n plugin_id=\"superset-backup\", params={\"dashboards\": [42], \"env\": \"prod\"}\n )\n assert _task_matches_dashboard(task, 42, \"prod\") is True\n assert _task_matches_dashboard(task, 43, \"prod\") is False\n assert _task_matches_dashboard(task, 42, \"dev\") is False\n\n llm_task = MagicMock(\n plugin_id=\"llm_dashboard_validation\",\n params={\"dashboard_id\": 42, \"environment_id\": \"prod\"},\n )\n assert _task_matches_dashboard(llm_task, 42, \"prod\") is True\n assert _task_matches_dashboard(llm_task, 42, None) is True\n\n\n# [/DEF:test_task_matches_dashboard_logic:Function]\n# [/DEF:TestDashboardsApi:Module]\n" }, @@ -100439,7 +103729,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "LAYER": "Test", "PURPOSE": "Unit tests for TaskLogPersistenceService.", "SEMANTICS": [ @@ -100494,6 +103784,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -100515,6 +103823,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -100533,7 +103842,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskLogPersistenceService.", "TEST_DATA": "log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}" }, @@ -100588,6 +103897,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -101054,7 +104381,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All required log statements must correctly check the threshold.", "LAYER": "Logging (Tests)", "PURPOSE": "Unit tests for the custom logger CoT JSON formatters and configuration context manager.", @@ -101083,20 +104410,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Logging (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Logging (Tests)" - } } ], "anchor_syntax": "def", @@ -101111,7 +104424,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain (Tests)", "PURPOSE": "Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.", "SEMANTICS": [ @@ -101137,22 +104450,7 @@ "target_ref": "[DatasetsApi]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestResourceHubs:Module]\n# @RELATION: DEPENDS_ON -> [DashboardsApi]\n# @RELATION: DEPENDS_ON -> [DatasetsApi]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api\n# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.\n# @LAYER: Domain (Tests)\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom unittest.mock import MagicMock, AsyncMock\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n get_resource_service,\n has_permission,\n)\n\nclient = TestClient(app)\n\n# [DEF:test_dashboards_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/dashboards contract compliance\n# @TEST_CONTRACT: dashboards_query -> dashboards payload or not_found response\n# @TEST_SCENARIO: dashboards_env_found_returns_payload -> HTTP 200 returns normalized dashboards list.\n# @TEST_SCENARIO: dashboards_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: dashboards_search_filters_results -> Search narrows payload to matching dashboard title.\n# @TEST_INVARIANT: dashboards_route_contract_stays_observable -> VERIFIED_BY: [dashboards_env_found_returns_payload, dashboards_unknown_env_returns_not_found, dashboards_search_filters_results]\n\n\n# [DEF:mock_deps:Function]\n# @PURPOSE: Provide dependency override fixture for resource hub route tests.\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @TEST_FIXTURE: resource_hub_overrides -> INLINE_JSON\n@pytest.fixture\ndef mock_deps():\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n config_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n task_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n resource_service = MagicMock()\n\n # Mock environment\n env = MagicMock()\n env.id = \"env1\"\n config_manager.get_environments.return_value = [env]\n\n # Mock tasks\n task_manager.get_all_tasks.return_value = []\n\n # Mock dashboards\n resource_service.get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Sales\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing\",\n \"slug\": \"mkt\",\n \"git_status\": None,\n \"last_task\": {\"task_id\": \"t1\", \"status\": \"SUCCESS\"},\n },\n ]\n )\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n\n # Bypass permission check\n mock_user = MagicMock()\n mock_user.username = \"testadmin\"\n mock_user.roles = []\n admin_role = MagicMock()\n admin_role.name = \"Admin\"\n mock_user.roles.append(admin_role)\n\n # Override both get_current_user and has_permission\n from src.dependencies import get_current_user\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # We need to override the specific instance returned by has_permission\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n\n yield {\"config\": config_manager, \"task\": task_manager, \"resource\": resource_service}\n\n app.dependency_overrides.clear()\n\n\n# [/DEF:mock_deps:Function]\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 200 with expected dashboard payload fields.\ndef test_get_dashboards_success(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 2\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n assert data[\"dashboards\"][0][\"git_status\"][\"sync_status\"] == \"OK\"\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_not_found:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 404 for unknown environment identifier.\ndef test_get_dashboards_not_found(mock_deps):\n response = client.get(\"/api/dashboards?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboards_not_found:Function]\n\n\n# [DEF:test_get_dashboards_search:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint search filter returns matching subset.\ndef test_get_dashboards_search(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1&search=Sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n\n\n# [/DEF:test_get_dashboards_search:Function]\n# [/DEF:test_dashboards_api:Block]\n\n\n# [DEF:test_datasets_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/datasets contract compliance\n# @TEST_CONTRACT: datasets_query -> datasets payload or error response\n# @TEST_SCENARIO: datasets_env_found_returns_payload -> HTTP 200 returns normalized datasets list.\n# @TEST_SCENARIO: datasets_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: datasets_search_filters_results -> Search narrows payload to matching dataset table.\n# @TEST_SCENARIO: datasets_service_failure_returns_503 -> Backend fetch failure surfaces as HTTP 503.\n# @TEST_INVARIANT: datasets_route_contract_stays_observable -> VERIFIED_BY: [datasets_env_found_returns_payload, datasets_unknown_env_returns_not_found, datasets_search_filters_results, datasets_service_failure_returns_503]\n\n\n# [DEF:test_get_datasets_success:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 200 with mapped fields payload.\ndef test_get_datasets_success(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n }\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n assert data[\"datasets\"][0][\"mapped_fields\"][\"mapped\"] == 5\n\n\n# [/DEF:test_get_datasets_success:Function]\n\n\n# [DEF:test_get_datasets_not_found:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 404 for unknown environment identifier.\ndef test_get_datasets_not_found(mock_deps):\n response = client.get(\"/api/datasets?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_datasets_not_found:Function]\n\n\n# [DEF:test_get_datasets_search:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint search filter returns matching dataset subset.\ndef test_get_datasets_search(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"table_name\": \"users\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 5, \"mapped\": 5},\n \"last_task\": None,\n },\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1&search=orders\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n\n\n# [/DEF:test_get_datasets_search:Function]\n\n\n# [DEF:test_get_datasets_service_failure:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint surfaces backend fetch failure as HTTP 503.\ndef test_get_datasets_service_failure(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Superset down\")\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_service_failure:Function]\n# [/DEF:test_datasets_api:Block]\n\n\n# [DEF:test_pagination_boundaries:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify pagination validation for GET endpoints\n# @TEST_CONTRACT: pagination_query -> validation error response\n# @TEST_SCENARIO: dashboards_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: dashboards_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_SCENARIO: datasets_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: datasets_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_EDGE: missing_field -> Missing env_id prevents route contract completion.\n# @TEST_EDGE: invalid_type -> Invalid pagination values are rejected at route validation layer.\n# @TEST_EDGE: external_fail -> Validation failure returns HTTP 400 without partial payload.\n# @TEST_INVARIANT: pagination_limits_apply_to_both_routes -> VERIFIED_BY: [dashboards_zero_page_rejected, dashboards_oversize_page_rejected, datasets_zero_page_rejected, datasets_oversize_page_rejected]\n\n\n# [DEF:test_get_dashboards_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects page=0 with HTTP 400 validation error.\ndef test_get_dashboards_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page -> {page: 0, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_zero_page:Function]\n\n\n# [DEF:test_get_dashboards_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects oversized page_size with HTTP 400.\ndef test_get_dashboards_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize -> {page_size: 101, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_oversize:Function]\n\n\n# [DEF:test_get_datasets_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects page=0 with HTTP 400.\ndef test_get_datasets_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page_datasets -> {page: 0, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page=0\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_zero_page:Function]\n\n\n# [DEF:test_get_datasets_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects oversized page_size with HTTP 400.\ndef test_get_datasets_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize_datasets -> {page_size: 101, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page_size=101\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_oversize:Function]\n# [/DEF:test_pagination_boundaries:Block]\n# [/DEF:TestResourceHubs:Module]\n" }, @@ -101180,16 +104478,25 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_INVARIANT is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Module", + "Function" + ] + } }, { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "TEST_INVARIANT", + "message": "@TEST_INVARIANT is forbidden for contract type 'Block' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Block" + } }, { "code": "tag_forbidden_by_complexity", @@ -101235,6 +104542,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_FIXTURE", @@ -101358,16 +104674,25 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_INVARIANT is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Module", + "Function" + ] + } }, { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "TEST_INVARIANT", + "message": "@TEST_INVARIANT is forbidden for contract type 'Block' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Block" + } }, { "code": "tag_forbidden_by_complexity", @@ -101506,16 +104831,25 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_INVARIANT is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Module", + "Function" + ] + } }, { - "code": "unknown_tag", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "TEST_INVARIANT", + "message": "@TEST_INVARIANT is forbidden for contract type 'Block' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Block" + } }, { "code": "tag_forbidden_by_complexity", @@ -101675,7 +105009,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "TaskManager state changes are deterministic and testable with mocked dependencies.", "LAYER": "Core", "PURPOSE": "Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.", @@ -101697,20 +105031,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -101738,6 +105058,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -101759,6 +105097,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -101786,15 +105125,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -101826,15 +105156,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -101857,7 +105178,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "LAYER": "Test", "PURPOSE": "Unit tests for TaskPersistenceService.", "SEMANTICS": [ @@ -101919,6 +105240,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -101940,6 +105279,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -101958,7 +105298,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskPersistenceService static helper methods." }, "relations": [ @@ -102006,6 +105346,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -102220,7 +105578,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskPersistenceService CRUD operations.", "TEST_DATA": "valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}" }, @@ -102275,6 +105633,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -102300,7 +105676,17 @@ "PURPOSE": "Setup in-memory test database." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:setup_class:Function]\n # @PURPOSE: Setup in-memory test database.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory SQLite database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskPersistenceService()\n # [/DEF:setup_class:Function]\n" }, @@ -102316,7 +105702,17 @@ "PURPOSE": "Dispose of test database." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:teardown_class:Function]\n # @PURPOSE: Dispose of test database.\n @classmethod\n def teardown_class(cls):\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n" }, @@ -102332,7 +105728,17 @@ "PURPOSE": "Clean task_records table before each test." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:setup_method:Function]\n # @PURPOSE: Clean task_records table before each test.\n def setup_method(self):\n session = self.TestSessionLocal()\n session.query(TaskRecord).delete()\n session.query(Environment).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n" }, @@ -102789,7 +106195,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for term correction API endpoints and DictionaryManager correction methods.", "SEMANTICS": [ @@ -102972,7 +106378,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for run history list/detail endpoints and metrics aggregation.", "SEMANTICS": [ @@ -103203,7 +106609,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for translation job CRUD endpoints and service layer with column validation.", "SEMANTICS": [ @@ -103307,7 +106713,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -103336,7 +106744,17 @@ "PURPOSE": "Mock ConfigManager for service tests that returns a test environment." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MockConfigManager:Class]\n# @PURPOSE: Mock ConfigManager for service tests that returns a test environment.\nclass MockConfigManager:\n def get_environments(self):\n return [\n Environment(\n id=\"test_env\",\n name=\"Test Environment\",\n url=\"http://superset:8088\",\n username=\"admin\",\n password=\"admin\",\n )\n ]\n\n def get_config(self):\n return MagicMock()\n# [/DEF:MockConfigManager:Class]\n" }, @@ -103354,7 +106772,35 @@ "REJECTED": "Mocking get_db with MagicMock — the route handlers need a real session for ORM queries." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:mock_api_deps:Function]\n# @PURPOSE: Override FastAPI dependencies (including get_db with in-memory SQLite) for API route tests.\n# @RATIONALE: Uses the same in-memory SQLite engine as service tests to avoid real PostgreSQL dependency.\n# @REJECTED: Mocking get_db with MagicMock — the route handlers need a real session for ORM queries.\n@pytest.fixture\ndef mock_api_deps():\n from src.core.database import get_db\n\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n # Create in-memory SQLite session for API tests (same engine as service tests)\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass # Session lifecycle managed by fixture teardown\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n\n # Apply all overrides\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:mock_api_deps:Function]\n" }, @@ -103370,7 +106816,17 @@ "PURPOSE": "FastAPI TestClient for API route tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:client:Function]\n# @PURPOSE: FastAPI TestClient for API route tests.\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n# [/DEF:client:Function]\n" }, @@ -103751,7 +107207,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for TranslationScheduler CRUD and APScheduler integration.", "SEMANTICS": [ @@ -103959,15 +107415,15 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Utility script for build" }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -104016,6 +107472,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -104061,6 +107532,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -104112,23 +107592,7 @@ "target_ref": "[ADR-0006:ADR]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "STATUS", - "message": "@STATUS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'ADR' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "ADR" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ADR-0001:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Define the canonical project directory layout, module boundaries, and naming conventions for the ss-tools repository. This ADR is the root structural authority — all other ADRs and feature plans derive their file placement from it.\n# @RELATION CALLS -> [ADR-0002:ADR]\n# @RELATION CALLS -> [ADR-0003:ADR]\n# @RELATION CALLS -> [ADR-0004:ADR]\n# @RELATION CALLS -> [ADR-0005:ADR]\n# @RELATION CALLS -> [ADR-0006:ADR]\n# @RATIONALE A single authoritative layout prevents module sprawl, cyclic dependencies, and agent confusion during long‑horizon speckit workflows. Without this, every feature spec must re‑debate where to place files, leading to inconsistent structures and broken tooling assumptions.\n# @REJECTED Monorepo with per‑feature packages (e.g. `packages/llm‑plugin/`) — rejected because the repository has two top‑level runtime targets (Python backend, Svelte frontend) with shared Docker/configuration infrastructure. Package‑style nesting would double the directory depth without adding isolation value.\n# @REJECTED Flat `src/` root — rejected because Python and JavaScript toolchains have incompatible project roots (`pyproject.toml` vs `package.json`), and mixing them in one top‑level `src/` would force cross‑toolchain contamination.\n\n## Decision\n\nThe repository uses a **two‑platform, top‑level separation**:\n\n```\nss-tools/ # Repository root\n├── backend/ # Python 3.13+ / FastAPI application\n│ ├── src/\n│ │ ├── api/ # FastAPI route modules (one file per route group)\n│ │ ├── core/ # Core services: task_manager, auth, migration, plugin_loader\n│ │ ├── models/ # SQLAlchemy ORM models (one file per entity group)\n│ │ ├── services/ # Business logic (orchestration, validators, transformers)\n│ │ ├── schemas/ # Pydantic request/response schemas\n│ │ └── app.py # FastAPI application factory\n│ ├── tests/ # pytest test suite (mirrors src/ structure)\n│ ├── pyproject.toml # Build & tool configuration\n│ └── requirements.txt # Production dependencies\n├── frontend/ # SvelteKit application\n│ ├── src/\n│ │ ├── routes/ # SvelteKit page routes (file‑based routing)\n│ │ ├── lib/\n│ │ │ ├── components/ # Reusable Svelte 5 components\n│ │ │ ├── stores/ # Svelte 5 rune stores ($state)\n│ │ │ └── api/ # API client modules (typed fetch wrappers)\n│ │ └── i18n/ # Internationalization dictionaries\n│ ├── tests/ # vitest test suite\n│ ├── package.json # Runtime + dev dependencies\n│ ├── svelte.config.js # SvelteKit configuration\n│ └── vite.config.ts # Vite build configuration\n├── docker/ # Dockerfiles for backend, frontend, nginx\n├── docker-compose.yml # Development Docker Compose\n├── docs/\n│ ├── adr/ # Architecture Decision Records (this file)\n│ ├── design/ # Detailed design documents\n│ └── ... # Operational docs (installation, settings, etc.)\n├── specs/ # Feature specifications (speckit artifacts)\n│ └── NNN‑feature‑name/\n│ ├── spec.md\n│ ├── plan.md\n│ ├── research.md\n│ ├── data-model.md\n│ ├── quickstart.md\n│ ├── contracts/\n│ │ └── modules.md\n│ └── tasks.md\n├── scripts/ # Shell utility scripts\n├── .specify/ # Speckit workflow framework\n│ ├── memory/constitution.md # Repository constitution\n│ ├── templates/ # Speckit artifact templates\n│ └── scripts/bash/ # Speckit helper scripts\n├── .opencode/ # OpenCode AI agent configuration\n│ ├── command/ # Speckit command definitions\n│ ├── skills/ # GRACE semantic protocol skills\n│ └── agents/ # Specialized agent definitions\n└── .github/ # GitHub Actions workflows\n```\n\n## Module Boundary Rules\n\n1. **`backend/src/api/`** — route handlers only. Must not contain business logic, ORM queries, or schema definitions. Each file = one FastAPI `APIRouter` group.\n2. **`backend/src/core/`** — singleton services with application‑scoped lifetime (auth manager, task scheduler, plugin loader, migration engine). Must not import from `api/`.\n3. **`backend/src/services/`** — stateless or request‑scoped business logic. May depend on `models/`, `schemas/`, and `core/`. Must not depend on `api/`.\n4. **`backend/src/models/`** — SQLAlchemy ORM declarations only. No business logic, no API dependencies.\n5. **`backend/src/schemas/`** — Pydantic models for validation/serialization. No ORM dependencies, no business logic.\n6. **`frontend/src/routes/`** — SvelteKit pages. Import components from `lib/components/`, state from `lib/stores/`, API clients from `lib/api/`.\n7. **`frontend/src/lib/components/`** — reusable components. Must not import from `routes/` (prevents circular dependencies).\n8. **`frontend/src/lib/stores/`** — Svelte 5 `$state` rune stores. Must not import from `routes/` or `components/`.\n\n## File Naming Conventions\n\n- **Python modules**: `snake_case.py` (e.g., `task_manager.py`, `auth_provider.py`)\n- **Svelte components**: `PascalCase.svelte` (e.g., `DashboardGrid.svelte`, `MappingTable.svelte`)\n- **Test files**: `test_.py` or `.test.ts`\n- **ADR files**: `ADR-NNNN-short-description.md` (zero‑padded, 4 digits, kebab‑case)\n\n## Enforcement\n\n- Speckit `/speckit.plan` command reads this ADR to validate proposed module placement.\n- Speckit `/speckit.implement` rejects files written outside canonical boundaries unless justified in feature‑local `research.md`.\n- CI static verification (`python3 scripts/static_verify.py`) may optionally enforce module import direction rules.\n\n# [/DEF:ADR-0001:ADR]\n" }, @@ -104154,23 +107618,7 @@ "target_ref": "[ADR-0001:ADR]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "STATUS", - "message": "@STATUS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'ADR' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "ADR" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ADR-0002:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Mandate the GRACE skill set (`.opencode/skills/`) as the single semantic governance authority for all source files and AI‑agent workflows in this repository. This ADR is the adoption decision — the protocol mechanics live exclusively in the skills; this ADR records *why* the skills are non‑optional for this project.\n# @RELATION DEPENDS_ON -> [ADR-0001:ADR]\n# @RATIONALE A multi‑platform repository (Python/FastAPI + SvelteKit) served by multiple AI agents (backend‑coder, frontend‑coder, qa‑tester, semantic‑curator, …) requires a single, machine‑parseable contract language that works identically across both platforms. Without it, agents produce inconsistent annotations, the semantic index (`semantic_map.json`) degrades, and long‑horizon agent sessions (50+ commits) accumulate invisible architectural drift.\n# @RATIONALE GRACE was chosen over lightweight alternatives because this project has specific stressors that only a full protocol addresses: (a) two platforms with different comment syntax — GRACE provides platform‑specific anchor forms under one unified graph, (b) long‑horizon agent sessions — GRACE Decision Memory (`@RATIONALE`/`@REJECTED`) prevents agents from re‑exploring already‑rejected paths, (c) complex orchestration flows (plugin execution, backup pipelines) — GRACE belief‑state markers make side‑effect‑heavy code auditable and debuggable, (d) enterprise deployment requirements — fractal limits (module <400 lines, `[DEF]` <150 lines) enforce structural hygiene critical for audit‑ready code.\n# @RATIONALE Skills are the canonical source rather than this ADR because protocol details (complexity scale, tag inventory, syntax variants) evolve. Duplicating them here creates a fork that will inevitably desynchronise — agents would face two competing versions and inevitably read the stale one. Skills are version‑controlled in the same repo and loaded dynamically via `skill({name=\"...\"})`, ensuring agents always receive the current protocol.\n# @REJECTED Plain docstring conventions (Google/NumPy style) — rejected because free‑text docstrings are invisible to the semantic index and cannot express relations, pre/post conditions, or rejected paths in a machine‑queryable form.\n# @REJECTED Decorator‑based contracts (`@contract`, `@pre`, `@post`) — rejected because they are Python‑only, cannot annotate Svelte components or TypeScript, and break the unified semantic graph spanning both platforms.\n# @REJECTED JSDoc/TSDoc for the frontend — rejected because it would create a second annotation language, fragmenting the semantic graph into two incompatible halves and forcing agents to master two different contract systems.\n# @REJECTED Embedding protocol rules directly in ADRs (the previous version of this document) — rejected because it duplicates the skill content and inevitably diverges. Agents receiving both the skill and the ADR would face conflicting versions; the skill is the single source of truth.\n\n## Decision\n\nThis repository adopts the **GRACE-Poly v2.4 protocol** as implemented by five skills:\n\n| Skill | File | Role in this project |\n|-------|------|---------------------|\n| `semantics-core` | `.opencode/skills/semantics-core/SKILL.md` | Anchor syntax, complexity scale (C1‑C5), global invariants, tag inventory |\n| `semantics-contracts` | `.opencode/skills/semantics-contracts/SKILL.md` | Design by Contract, Decision Memory (ADR chain, `@RATIONALE`/`@REJECTED`), anti‑erosion rules |\n| `semantics-belief` | `.opencode/skills/semantics-belief/SKILL.md` | Belief‑state runtime markers (`belief_scope`, `reason`, `reflect`, `explore`) for C4/C5 Python flows |\n| `semantics-testing` | `.opencode/skills/semantics-testing/SKILL.md` | Test constraints, invariant traceability, anti‑tautology rules |\n| `semantics-frontend` | `.opencode/skills/semantics-frontend/SKILL.md` | Svelte 5 UX contracts (`@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY`, `@UX_REACTIVITY`) |\n\n**Key principle:** Skills are the protocol. This ADR is the adoption record. When an agent needs to know *what tags are required at C4*, it reads `semantics-core`. When it needs to know *why this project chose C4 annotations at all*, it reads this ADR.\n\n## Enforcement\n\nAll speckit commands (`.opencode/command/speckit.*.md`) load the skill set as part of their execution flow:\n\n- **`/speckit.plan`** — reads skills to validate contract designs against the complexity scale.\n- **`/speckit.implement`** — requires `[DEF]` anchors and complexity‑appropriate metadata; rejects naked code.\n- **`/speckit.test`** — audits semantic compliance and decision‑memory continuity per skill rules.\n- **`/speckit.semantics`** — reindexes and audits the entire workspace against the skill‑defined protocol.\n- **`/speckit.analyze`** — checks for decision‑memory drift and rejected‑path scheduling.\n\nStatic verification (`python3 scripts/static_verify.py`) performs offline checks for broken anchors, missing complexity metadata, and orphan relations.\n\n# [/DEF:ADR-0002:ADR]\n" }, @@ -104208,23 +107656,7 @@ "target_ref": "[ADR-0005:ADR]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "STATUS", - "message": "@STATUS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'ADR' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "ADR" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ADR-0004:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Define the plugin architecture for ss-tools — the loading mechanism, lifecycle contract, isolation guarantees, and the boundary between core services and pluggable extensions.\n# @RELATION DEPENDS_ON -> [ADR-0001:ADR]\n# @RELATION DEPENDS_ON -> [ADR-0003:ADR]\n# @RELATION CALLS -> [ADR-0005:ADR]\n# @RATIONALE Extensibility is a core architectural value: the system must support LLM‑driven analysis, custom data transformations, and environment‑specific logic without modifying core code. A plugin system prevents the monolith from accumulating every domain‑specific feature and enables third‑party (or future‑self) contributions without forking.\n# @RATIONALE Process isolation was chosen over in‑process imports because: (a) plugins may use incompatible library versions, (b) a crashing plugin must not take down the orchestrator, (c) security boundary — plugins should not access the orchestrator's database connection directly.\n# @REJECTED In‑process Python `importlib` plugin loading — rejected because a misbehaving plugin can corrupt global state, exhaust memory, or crash the server. Process isolation provides a hard boundary.\n# @REJECTED Docker‑container per plugin — rejected because it adds excessive orchestration complexity and startup latency for plugins that are mostly lightweight LLM prompt chains. Subprocess isolation is sufficient.\n# @REJECTED WebAssembly (WASI) sandbox — rejected because the Python AI/LLM ecosystem (langchain, transformers) does not yet reliably compile to WASM. Premature optimization.\n\n## Decision\n\n### Plugin Architecture\n\n```\nss-tools Core\n├── core/plugin_loader.py # Plugin discovery, loading, lifecycle\n├── core/plugin_executor.py # Subprocess execution, timeout, error boundary\n├── core/plugin_registry.py # Registered plugins, metadata, health\n└── plugins/ # Plugin packages (each = one directory)\n ├── llm_analysis/ # LLM‑driven Superset data analysis\n ├── dataset_orchestration/ # LLM dataset operations\n └── git_integration/ # Git‑based version control for dashboards\n```\n\n### Plugin Contract\n\nEvery plugin MUST provide:\n\n1. **`plugin.toml`** — metadata manifest at the plugin root\n ```toml\n [plugin]\n id = \"llm_analysis\"\n name = \"LLM Data Analysis\"\n version = \"1.0.0\"\n entrypoint = \"plugin.py\"\n timeout_sec = 300\n max_memory_mb = 512\n requires = [\"superset-api>=1.0\", \"openai>=1.0\"]\n ```\n\n2. **`plugin.py`** — entrypoint with two required functions:\n - `def register(registry: PluginRegistry) -> PluginInfo` — declare capabilities\n - `def execute(task: TaskContext) -> TaskResult` — run the plugin\n\n3. **Task context contract** (Pydantic `TaskContext`):\n - `task_id: str`, `plugin_id: str`, `action: str`, `params: dict`, `superset_env: SupersetConnection`, `auth_token: str` (scoped, short‑lived)\n\n4. **Result envelope** (Pydantic `TaskResult`):\n - `status: Literal[\"success\", \"warning\", \"error\"]`, `data: dict | None`, `error_message: str | None`, `execution_time_ms: int`, `artifacts: list[str]` (file paths to saved artifacts)\n\n### Plugin Lifecycle\n\n```\nDiscover → Validate → Register → [Execute] → Report\n │ │ │ │\n │ Check TOML Store in Spawn subprocess\n │ schema, registry with timeout +\n │ dependencies in DB memory limit\n```\n\n### Isolation Guarantees\n\n- **Subprocess**: `subprocess.run(..., timeout=timeout_sec)`, killed on timeout.\n- **Memory**: `resource.setrlimit(RLIMIT_AS, max_memory_mb * 1024 * 1024)` before exec.\n- **No DB access**: Plugins receive only a scoped REST API token, never a database connection.\n- **No filesystem writes outside allowed dirs**: Configurable artifact directory per plugin.\n\n### RBAC Integration\n\nPlugin access is governed by ADR-0005 (RBAC):\n- Each plugin declares `required_roles: [\"admin\", \"analyst\"]` in `plugin.toml`.\n- Plugin executor checks the user's role set before allowing execution.\n- Forbidden access returns `403` with audit log entry.\n\n# [/DEF:ADR-0004:ADR]\n" }, @@ -104262,23 +107694,7 @@ "target_ref": "[ADR-0004:ADR]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "STATUS", - "message": "@STATUS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'ADR' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "ADR" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ADR-0005:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Define the authentication and authorization architecture for ss-tools: local auth, optional ADFS SSO federation, RBAC model, session management, and the security boundary between ss-tools (DevOps privileges) and Superset (BI privileges).\n# @RELATION DEPENDS_ON -> [ADR-0001:ADR]\n# @RELATION DEPENDS_ON -> [ADR-0003:ADR]\n# @RELATION CALLS -> [ADR-0004:ADR]\n# @RATIONALE ss-tools manages operations that can modify production Superset instances (dashboard migration, backup, deployment). Unauthenticated or under‑authorized access to these operations is a critical security risk. A dedicated RBAC system ensures that DevOps privileges (manage deployments) are cleanly separated from BI privileges (view dashboards) and that actions are auditable.\n# @RATIONALE Local auth (bcrypt + JWT) is the primary path because ss-tools must work in air‑gapped enterprise deployments where external identity providers are unavailable. ADFS SSO is an optional federation layer for organizations that already have Active Directory.\n# @RATIONALE Role‑Based Access Control (RBAC) was chosen over Attribute‑Based Access Control (ABAC) because: (a) the system has a small, well‑defined set of operations (deploy, backup, migrate, view), (b) RBAC maps naturally to organizational roles (admin, analyst, operator), (c) ABAC adds complexity without proportional benefit for this scope.\n# @REJECTED Delegating all auth to Superset — rejected because ss-tools must operate when Superset is down, and Superset's auth model is designed for BI users, not DevOps operators with cross‑environment privileges.\n# @REJECTED OAuth2 social login (Google, GitHub) as primary path — rejected because enterprise deployments require air‑gapped operation. External OAuth providers are unavailable in offline mode.\n# @REJECTED Simple API key (no RBAC) — rejected because it cannot express granular permissions (admin vs analyst vs viewer), making auditability and least‑privilege impossible.\n\n## Decision\n\n### Authentication Flow\n\n```\n┌──────────┐ POST /api/auth/login ┌──────────────┐\n│ User │ ──────────────────────────────│ FastAPI │\n│ (Browser)│ {username, password} │ Backend │\n│ │ ◄──────────────────────────── │ │\n│ │ {access_token, refresh} │ bcrypt + │\n│ │ │ JWT (HS256) │\n└──────────┘ └──────┬───────┘\n │\n ┌───────▼───────┐\n │ PostgreSQL │\n │ users table │\n │ roles table │\n └───────────────┘\n```\n\n### ADFS Federation (Optional)\n\n- Enabled via `config.json: auth.adfs_enabled = true`\n- SAML 2.0 flow through `python3-saml`\n- ADFS users mapped to local roles via `adfs_group → ss_tools_role` mapping table\n- Federated sessions still receive JWTs (stateless after initial SAML handshake)\n\n### RBAC Model\n\n| Role | Permissions |\n|------|-------------|\n| `admin` | All: manage users, manage roles, manage plugins, deploy, backup, migrate, view dashboards, view logs |\n| `analyst` | View dashboards, run LLM analysis plugins, view reports, view logs (own) |\n| `operator` | Deploy dashboards, run migrations, manage backups, view logs |\n| `viewer` | View dashboards, view reports |\n\n### Token Design\n\n- **Access token**: JWT (HS256), 15‑minute expiry, contains `user_id`, `roles: list[str]`\n- **Refresh token**: opaque random string (SHA‑256), 7‑day expiry, stored hashed in DB\n- **Superset API token per environment**: AES‑256‑GCM encrypted, stored in `connection_configs` table\n- **Plugin execution token**: JWT, scoped to a single `task_id`, 15‑minute expiry\n\n### Security Constraints\n\n1. Passwords: bcrypt with cost factor 12 (minimum).\n2. Rate limiting: 5 failed login attempts per IP per 15 minutes → temporary IP block.\n3. Token revocation: admin can revoke all sessions for a user (delete refresh tokens).\n4. Audit log: all auth events (login success/failure, role change, token revoke) written to `audit_log` table.\n5. Enterprise clean mode: local auth only, ADFS disabled, no external network calls for identity.\n\n### RBAC Enforcement Pattern\n\n```python\n# backend/src/api/dependencies.py\nfrom fastapi import Depends, HTTPException\n\ndef require_role(required_role: str):\n def dependency(current_user: User = Depends(get_current_user)):\n if required_role not in current_user.roles:\n raise HTTPException(status_code=403, detail=\"Insufficient permissions\")\n return current_user\n return dependency\n\n# Usage in route:\n@router.post(\"/api/dashboards/deploy\")\nasync def deploy(..., user: User = Depends(require_role(\"operator\"))):\n ...\n```\n\n# [/DEF:ADR-0005:ADR]\n" }, @@ -104310,23 +107726,7 @@ "target_ref": "[ADR-0002:ADR]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "STATUS", - "message": "@STATUS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'ADR' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "ADR" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ADR-0006:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Define the frontend architecture for ss-tools: Svelte 5 runes reactivity model, SvelteKit routing, component composition rules, state management patterns, API client conventions, and UX contract expectations for C4/C5 components.\n# @RELATION DEPENDS_ON -> [ADR-0001:ADR]\n# @RELATION DEPENDS_ON -> [ADR-0002:ADR]\n# @RATIONALE Svelte 5 introduces a fundamentally different reactivity model (runes: `$state`, `$derived`, `$effect`, `$props`) compared to Svelte 4's `$:` reactive statements. This ADR locks in the Svelte 5 runes approach and prevents backsliding into legacy patterns when new developers or AI agents contribute code.\n# @RATIONALE SvelteKit with static adapter (`@sveltejs/adapter-static`) was chosen over SvelteKit SSR because: (a) ss-tools is a Docker‑deployed SPA behind nginx — server‑side rendering provides no latency benefit, (b) static SPA simplifies Docker deployment (no Node.js server needed, just nginx serving static files), (c) all data is fetched via REST API, not server‑side load functions.\n# @RATIONALE Svelte 5 runes (`$state`, `$derived`, `$effect`) are mandated over Svelte 4 `$:` reactivity and `writable` stores because: (a) runes are the forward path — Svelte 4 patterns are deprecated, (b) runes provide fine‑grained reactivity without subscription boilerplate, (c) `$state` can be used outside `.svelte` files in `.svelte.js` modules, enabling reusable reactive logic.\n# @REJECTED React/Next.js — rejected by ADR-0003 (technology stack independence from Superset). Svelte 5 was chosen for its compile‑time approach, smaller bundle size, and superior DX for this project's scale.\n# @REJECTED Svelte 4 legacy patterns (`$:`, `writable`/`readable` stores) — rejected because Svelte 5 runes are the actively maintained reactivity model. Mixing two models creates confusion and potential reactivity bugs.\n# @REJECTED Server‑Side Rendering (SSR) with SvelteKit — rejected because the data is entirely API‑driven (no server‑side page data loading), and SSR adds deployment complexity (Node.js process) without latency benefit for authenticated SPA users.\n\n## Decision\n\n### Technology Stack\n\n| Layer | Choice | Version |\n|-------|--------|---------|\n| Framework | SvelteKit | 2.x |\n| UI Library | Svelte | 5.x (runes mode) |\n| Build | Vite | 7.x |\n| Styling | Tailwind CSS | 3.x |\n| Adapter | @sveltejs/adapter-static | 3.x (SPA mode) |\n| Testing | vitest + @testing-library/svelte | 4.x / 5.x |\n\n### Reactivity Model (Svelte 5 Runes)\n\n```\nLegacy (FORBIDDEN) Svelte 5 Runes (REQUIRED)\n──────────────────────── ──────────────────────────\nlet count = 0; let count = $state(0);\n$: doubled = count * 2; let doubled = $derived(count * 2);\n$: { /* side effect */ } $effect(() => { /* side effect */ });\nexport let name; let { name } = $props();\nimport { writable } from ... ❌ stores are .svelte.js modules with $state\n```\n\n### State Management Pattern\n\nThree tiers of state, strictly separated:\n\n1. **Component‑local state**: `$state()` inside `.svelte` files. Never exported.\n2. **Shared reactive state**: `.svelte.js` modules exporting `$state` objects. Imported by components.\n ```js\n // frontend/src/lib/stores/dashboard.svelte.js\n export const dashboardState = $state({\n dashboards: [],\n selectedId: null,\n isLoading: false\n });\n ```\n3. **Server state**: Fetched via API client, held in `$state` variables locally. No global cache — each route fetches what it needs.\n\n### API Client Convention\n\nAPI client modules live under `frontend/src/lib/api/`. Each module wraps `fetch` calls to the FastAPI backend, attaches the JWT access token from the auth store, and normalises errors into a typed `ApiError`. Contract annotations follow the rules defined in the `semantics-core` and `semantics-contracts` skills.\n\n### Component Complexity & UX Contracts\n\nSvelte components with side effects (API calls, WebSocket subscriptions, file operations) MUST carry UX contract tags as defined by the `semantics-frontend` skill (`.opencode/skills/semantics-frontend/SKILL.md`). The skill is the canonical source for tag inventory, syntax, and per‑complexity requirements.\n\n### Component File Structure\n\n```\nfrontend/src/lib/components/\n├── DashboardGrid.svelte # C4: complex data grid with WebSocket updates\n├── MappingTable.svelte # C3: migration mapping table\n├── PluginExecutionCard.svelte # C4: plugin runner with progress feedback\n├── RoleBadge.svelte # C1: simple role display\n└── ...\n```\n\n### Routing\n\n- SvelteKit file‑based routing under `frontend/src/routes/`\n- Protected routes check auth in `+layout.svelte` (redirect to `/login` if no token)\n- Route structure: `/` (dashboard list), `/envs/[id]` (environment detail), `/migration` (migration wizard), `/plugins` (plugin management), `/admin` (user/role management)\n\n# [/DEF:ADR-0006:ADR]\n" }, @@ -104342,7 +107742,17 @@ "PURPOSE": "Triggers dashboard validation task." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleValidate:Function]\n /**\n * @purpose Triggers dashboard validation task.\n */\n async function handleValidate(dashboard: DashboardMetadata) {\n if (validatingIds.has(dashboard.id)) return;\n\n validatingIds.add(dashboard.id);\n validatingIds = new Set(validatingIds);\n\n try {\n await api.postApi(\"/tasks\", {\n plugin_id: \"llm_dashboard_validation\",\n params: {\n dashboard_id: dashboard.id.toString(),\n environment_id: environmentId,\n },\n });\n\n toast(\"Validation task started\", \"success\");\n } catch (e: any) {\n toast(e.message || \"Validation failed to start\", \"error\");\n } finally {\n validatingIds.delete(dashboard.id);\n validatingIds = new Set(validatingIds);\n }\n }\n // [/DEF:handleValidate:Function]\n" }, @@ -104358,7 +107768,17 @@ "PURPOSE": "Opens the Git management modal for a dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:openGit:Function]\n /**\n * @purpose Opens the Git management modal for a dashboard.\n */\n function openGit(dashboard: DashboardMetadata) {\n gitDashboardId = String(dashboard.slug || dashboard.id);\n gitDashboardTitle = dashboard.title;\n showGitManager = true;\n }\n // [/DEF:openGit:Function]\n" }, @@ -104397,7 +107817,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -104416,6 +107838,15 @@ "message": "@PROPS is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -104423,7 +107854,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -104481,6 +107915,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -104530,7 +107973,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -104544,17 +107989,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -104564,7 +108004,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -104599,7 +108042,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "UI", "PURPOSE": "Displays the application footer with copyright information.", "SEMANTICS": [ @@ -104617,7 +108060,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -104630,6 +108075,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -104637,7 +108091,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -104698,7 +108155,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -104712,17 +108171,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -104732,7 +108186,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -104790,6 +108247,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -104827,6 +108293,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -104882,7 +108357,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -104896,17 +108373,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -104916,7 +108388,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -104974,6 +108449,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105011,6 +108495,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105025,7 +108518,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Main navigation bar for the application.", "SEMANTICS": [ @@ -105064,7 +108557,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -105084,7 +108579,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -105096,12 +108594,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -105154,7 +108646,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -105167,6 +108661,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -105174,7 +108677,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -105208,6 +108714,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -105249,6 +108756,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105286,6 +108802,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105300,7 +108825,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Selected IDs must be a subset of available dashboards.", "LAYER": "Component", "PURPOSE": "Displays a grid of dashboards with selection and pagination.", @@ -105336,7 +108861,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -105349,20 +108876,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -105370,7 +108883,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -105383,6 +108899,15 @@ "contract_type": "Component" } }, + { + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -105395,6 +108920,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -105436,6 +108962,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105473,6 +109008,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105510,6 +109054,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105547,6 +109100,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105584,6 +109146,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105601,7 +109172,17 @@ "PURPOSE": "Determines whether git actions can run for a dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:isRepositoryReady:Function]\n // @PURPOSE: Determines whether git actions can run for a dashboard.\n function isRepositoryReady(dashboardId: number): boolean {\n const token = getRepositoryStatusToken(dashboardId);\n return token !== \"loading\" && token !== \"no_repo\" && token !== \"error\";\n }\n // [/DEF:isRepositoryReady:Function]\n" }, @@ -105617,7 +109198,17 @@ "PURPOSE": "Marks dashboard statuses as loading so they are refetched." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:invalidateRepositoryStatuses:Function]\n // @PURPOSE: Marks dashboard statuses as loading so they are refetched.\n function invalidateRepositoryStatuses(dashboardIds: number[]): void {\n if (dashboardIds.length === 0) return;\n const nextStatuses = { ...repositoryStatusByDashboardId };\n dashboardIds.forEach((dashboardId) => {\n nextStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = nextStatuses;\n }\n // [/DEF:invalidateRepositoryStatuses:Function]\n" }, @@ -105633,7 +109224,17 @@ "PURPOSE": "Converts git status payload into a stable UI status token." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:resolveRepositoryStatusToken:Function]\n /**\n * @purpose Converts git status payload into a stable UI status token.\n */\n function resolveRepositoryStatusToken(status: any): string {\n const syncState = String(status?.sync_state || \"\").toUpperCase();\n if (syncState === \"DIVERGED\") return \"diverged\";\n if (syncState === \"BEHIND_REMOTE\") return \"behind_remote\";\n if (syncState === \"AHEAD_REMOTE\") return \"ahead_remote\";\n if (syncState === \"CHANGES\") return \"changes\";\n if (syncState === \"SYNCED\") return \"synced\";\n\n const syncStatus = String(status?.sync_status || \"\").toUpperCase();\n if (syncStatus === \"NO_REPO\") return \"no_repo\";\n if (syncStatus === \"ERROR\") return \"error\";\n if (syncStatus === \"DIFF\") return \"changes\";\n if (syncStatus === \"OK\") return \"synced\";\n\n const aheadCount = Number(status?.ahead_count || 0);\n const behindCount = Number(status?.behind_count || 0);\n if (aheadCount > 0 && behindCount > 0) return \"diverged\";\n if (behindCount > 0) return \"behind_remote\";\n if (aheadCount > 0) return \"ahead_remote\";\n\n const hasChanges =\n Boolean(status?.is_dirty) ||\n (status?.untracked_files?.length || 0) > 0 ||\n (status?.modified_files?.length || 0) > 0 ||\n (status?.staged_files?.length || 0) > 0;\n\n return hasChanges ? \"changes\" : \"synced\";\n }\n // [/DEF:resolveRepositoryStatusToken:Function]\n" }, @@ -105649,7 +109250,17 @@ "PURPOSE": "Hydrates repository status map for dashboards in repository mode." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:loadRepositoryStatuses:Function]\n /**\n * @purpose Hydrates repository status map for dashboards in repository mode.\n */\n async function loadRepositoryStatuses() {\n if (statusMode !== \"repository\") {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const requestId = repositoryStatusRequestId + 1;\n repositoryStatusRequestId = requestId;\n\n const visibleDashboards = paginatedDashboards;\n const visibleIds = visibleDashboards.map((dashboard) => dashboard.id);\n\n if (visibleDashboards.length === 0) {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const missingIds = visibleIds.filter((dashboardId) => {\n const token = repositoryStatusByDashboardId[dashboardId];\n return token === undefined || token === \"loading\";\n });\n\n if (missingIds.length === 0) {\n return;\n }\n\n const loadingStatuses = { ...repositoryStatusByDashboardId };\n missingIds.forEach((dashboardId) => {\n loadingStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = loadingStatuses;\n\n let entries: Array = [];\n try {\n const batchResult = await gitService.getStatusesBatch(missingIds);\n const statusesByDashboardId = batchResult?.statuses || {};\n entries = missingIds.map((dashboardId) => {\n const status =\n statusesByDashboardId[dashboardId] ??\n statusesByDashboardId[String(dashboardId)];\n if (!status) return [dashboardId, \"error\"] as const;\n return [dashboardId, resolveRepositoryStatusToken(status)] as const;\n });\n } catch (error) {\n entries = missingIds.map((dashboardId) => [dashboardId, \"error\"] as const);\n }\n\n if (requestId !== repositoryStatusRequestId) return;\n repositoryStatusByDashboardId = {\n ...repositoryStatusByDashboardId,\n ...Object.fromEntries(entries),\n };\n }\n // [/DEF:loadRepositoryStatuses:Function]\n" }, @@ -105665,7 +109276,17 @@ "PURPOSE": "Executes git action for selected dashboards with limited parallelism." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:runBulkGitAction:Function]\n // @PURPOSE: Executes git action for selected dashboards with limited parallelism.\n async function runBulkGitAction(\n actionToken: string,\n action: (dashboardId: number) => Promise,\n ): Promise {\n if (bulkActionRunning) return;\n const selectedDashboardIds = selectedIds.filter((dashboardId) =>\n isRepositoryReady(dashboardId),\n );\n\n if (selectedDashboardIds.length === 0) {\n addToast($t.git?.no_repositories_selected, \"error\");\n return;\n }\n\n bulkActionRunning = true;\n const concurrency = 3;\n const idsQueue = [...selectedDashboardIds];\n let successCount = 0;\n let failedCount = 0;\n\n const worker = async () => {\n while (idsQueue.length > 0) {\n const dashboardId = idsQueue.shift();\n if (dashboardId === undefined) break;\n try {\n await action(dashboardId);\n successCount += 1;\n } catch (_error) {\n failedCount += 1;\n }\n }\n };\n\n try {\n await Promise.all(\n Array.from({ length: Math.min(concurrency, selectedDashboardIds.length) }, () => worker()),\n );\n invalidateRepositoryStatuses(selectedDashboardIds);\n const actionLabel = $t.git?.[`bulk_action_${actionToken}`] || actionToken;\n addToast(\n $t.git?.bulk_result\n .replace(\"{action}\", actionLabel)\n .replace(\"{success}\", String(successCount))\n .replace(\"{failed}\", String(failedCount)),\n failedCount > 0 ? \"warning\" : \"success\",\n );\n } finally {\n bulkActionRunning = false;\n }\n }\n // [/DEF:runBulkGitAction:Function]\n" }, @@ -105679,17 +109300,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " // [DEF:handleBulkSync:Function]\n async function handleBulkSync(): Promise {\n await runBulkGitAction(\"sync\", (dashboardId) => gitService.sync(dashboardId, null, envId));\n }\n // [/DEF:handleBulkSync:Function]\n" }, @@ -105703,17 +109314,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " // [DEF:handleBulkCommit:Function]\n async function handleBulkCommit(): Promise {\n const message = prompt($t.git?.commit_message);\n if (!message?.trim()) return;\n await runBulkGitAction(\"commit\", (dashboardId) =>\n gitService.commit(dashboardId, message.trim(), [], envId),\n );\n }\n // [/DEF:handleBulkCommit:Function]\n" }, @@ -105727,17 +109328,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " // [DEF:handleBulkPull:Function]\n async function handleBulkPull(): Promise {\n await runBulkGitAction(\"pull\", (dashboardId) => gitService.pull(dashboardId, envId));\n }\n // [/DEF:handleBulkPull:Function]\n" }, @@ -105751,17 +109342,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " // [DEF:handleBulkPush:Function]\n async function handleBulkPush(): Promise {\n await runBulkGitAction(\"push\", (dashboardId) => gitService.push(dashboardId, envId));\n }\n // [/DEF:handleBulkPush:Function]\n" }, @@ -105777,7 +109358,17 @@ "PURPOSE": "Removes selected repositories from storage and binding table." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleBulkDelete:Function]\n // @PURPOSE: Removes selected repositories from storage and binding table.\n async function handleBulkDelete(): Promise {\n if (!confirm($t.git?.confirm_delete_repo || \"Удалить выбранные репозитории?\")) return;\n const idsToDelete = [...selectedIds];\n await runBulkGitAction(\"delete\", (dashboardId) =>\n gitService.deleteRepository(dashboardId, envId),\n );\n dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));\n selectedIds = [];\n }\n // [/DEF:handleBulkDelete:Function]\n" }, @@ -105793,7 +109384,17 @@ "PURPOSE": "Opens Git manager for exactly one selected dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleManageSelected:Function]\n // @PURPOSE: Opens Git manager for exactly one selected dashboard.\n async function handleManageSelected(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find(\n (dashboard) => dashboard.id === selectedDashboardId,\n );\n openGitManagerForDashboard(selectedDashboard || null);\n }\n // [/DEF:handleManageSelected:Function]\n" }, @@ -105829,6 +109430,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -105846,7 +109456,17 @@ "PURPOSE": "Opens Git manager for provided dashboard metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:openGitManagerForDashboard:Function]\n // @PURPOSE: Opens Git manager for provided dashboard metadata.\n function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {\n if (!dashboard) return;\n const dashboardRef = resolveDashboardRef(dashboard);\n if (!dashboardRef) {\n addToast($t.git?.select_dashboard_with_slug || \"Dashboard slug is required to open GitManager\", \"error\");\n return;\n }\n gitDashboardId = dashboardRef;\n gitDashboardTitle = dashboard.title || \"\";\n showGitManager = true;\n }\n // [/DEF:openGitManagerForDashboard:Function]\n" }, @@ -105862,7 +109482,17 @@ "PURPOSE": "Opens Git manager from bulk actions to initialize selected repository." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleInitializeRepositories:Function]\n // @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.\n async function handleInitializeRepositories(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;\n openGitManagerForDashboard(selectedDashboard);\n }\n // [/DEF:handleInitializeRepositories:Function]\n" }, @@ -105878,7 +109508,17 @@ "PURPOSE": "Returns sort value for status column based on mode." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:getSortStatusValue:Function]\n /**\n * @purpose Returns sort value for status column based on mode.\n */\n function getSortStatusValue(dashboard: DashboardMetadata): string {\n if (statusMode === \"repository\") {\n return getRepositoryStatusToken(dashboard.id);\n }\n return String(dashboard.status || \"\").toLowerCase();\n }\n // [/DEF:getSortStatusValue:Function]\n" }, @@ -105894,7 +109534,17 @@ "PURPOSE": "Returns localized label for status column." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:getStatusLabel:Function]\n /**\n * @purpose Returns localized label for status column.\n */\n function getStatusLabel(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return String(dashboard.status || \"\");\n }\n const token = getRepositoryStatusToken(dashboard.id);\n return $t.git?.repo_status?.[token] || token;\n }\n // [/DEF:getStatusLabel:Function]\n" }, @@ -105910,7 +109560,17 @@ "PURPOSE": "Returns badge style for status column." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:getStatusBadgeClass:Function]\n /**\n * @purpose Returns badge style for status column.\n */\n function getStatusBadgeClass(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return dashboard.status === \"published\"\n ? \"bg-green-100 text-green-800\"\n : \"bg-gray-100 text-gray-800\";\n }\n\n const token = getRepositoryStatusToken(dashboard.id);\n if (token === \"loading\") return \"bg-slate-100 text-slate-500\";\n if (token === \"no_repo\") return \"bg-gray-100 text-gray-800\";\n if (token === \"synced\") return \"bg-green-100 text-green-800\";\n if (token === \"changes\") return \"bg-amber-100 text-amber-800\";\n if (token === \"behind_remote\") return \"bg-blue-100 text-blue-800\";\n if (token === \"ahead_remote\") return \"bg-indigo-100 text-indigo-800\";\n if (token === \"diverged\") return \"bg-purple-100 text-purple-800\";\n return \"bg-rose-100 text-rose-800\";\n }\n // [/DEF:getStatusBadgeClass:Function]\n" }, @@ -105923,7 +109583,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "When open, wizard keeps user on an actionable setup path until the first environment exists.", "LAYER": "UI", "PURPOSE": "Blocking startup wizard for creating the first Superset environment from zero-state screens.", @@ -105962,7 +109622,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -105974,24 +109636,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -106038,7 +109682,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106051,6 +109697,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -106058,7 +109713,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106092,6 +109750,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106109,6 +109768,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106150,6 +109810,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106187,6 +109856,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106226,7 +109904,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106240,17 +109920,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -106260,7 +109935,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106294,6 +109972,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106335,6 +110014,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106349,7 +110037,7 @@ "tier": "CRITICAL", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Real-time logs are always appended without duplicates.", "LAYER": "UI", "POST": "task logs are displayed and updated in real time.", @@ -106399,7 +110087,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106437,7 +110127,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106473,20 +110166,8 @@ }, { "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -106501,6 +110182,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106518,6 +110200,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106552,7 +110235,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -106575,7 +110259,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -106600,7 +110285,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -106649,6 +110336,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106686,6 +110382,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106723,6 +110428,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -106737,7 +110451,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Shows inline logs", "SEMANTICS": [ @@ -106762,7 +110476,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106782,7 +110498,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106795,12 +110514,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -106813,6 +110526,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106831,7 +110545,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Shows modal logs", "SEMANTICS": [ @@ -106856,7 +110570,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106876,7 +110592,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106889,12 +110608,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -106907,6 +110620,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -106925,7 +110639,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Connects to a WebSocket to display real-time logs for a running task with filtering support.", "SEMANTICS": [ @@ -106952,7 +110666,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -106972,7 +110688,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -106984,12 +110703,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -107027,6 +110740,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107064,6 +110786,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107101,6 +110832,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107138,6 +110878,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107175,6 +110924,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107212,6 +110970,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -107226,7 +110993,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Displays transient notifications (toasts) in the bottom-right corner.", "SEMANTICS": [ @@ -107253,7 +111020,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -107273,7 +111042,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -107286,12 +111058,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -107314,7 +111080,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Duplicate logs are never appended. Polling only active for in-progress tasks.", "LAYER": "UI (Tests)", "PURPOSE": "Unit tests for TaskLogViewer component by mounting it and observing the DOM.", @@ -107349,20 +111115,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -107426,16 +111178,25 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_not_for_contract_type", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -107450,7 +111211,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Unauthenticated users are redirected to /login, unauthorized users are redirected to fallbackPath, and protected slot renders only when access is verified.", "LAYER": "UI", "PURPOSE": "Enforces authenticated and authorized access before protected route content is rendered.", @@ -107570,40 +111331,105 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -107617,6 +111443,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -107634,6 +111461,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -107651,6 +111479,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -107668,6 +111497,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -107721,6 +111551,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -107778,6 +111617,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -107824,7 +111672,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -107838,17 +111688,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -107858,7 +111703,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -107892,6 +111740,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -107950,7 +111799,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -107964,17 +111815,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -107984,7 +111830,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108018,6 +111867,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -108035,6 +111885,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -108079,6 +111930,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -108130,6 +111990,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108187,7 +112056,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -108201,17 +112072,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -108221,7 +112087,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108279,6 +112148,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108316,6 +112194,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108350,6 +112237,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108387,6 +112283,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108426,7 +112331,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -108440,17 +112347,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -108460,7 +112362,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108518,6 +112423,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108532,7 +112446,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Component", "PURPOSE": "Модальное окно для создания коммита с просмотром изменений (diff).", "SEMANTICS": [ @@ -108560,7 +112474,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -108573,20 +112489,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -108594,7 +112496,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108606,12 +112511,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -108629,7 +112528,17 @@ "PURPOSE": "Generates a commit message using LLM." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleGenerateMessage:Function]\n /**\n * @purpose Generates a commit message using LLM.\n */\n async function handleGenerateMessage() {\n generatingMessage = true;\n try {\n console.log(\n `[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,\n );\n // postApi returns the JSON data directly or throws an error\n const data = await api.postApi(\n `/git/repositories/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : \"\"}`,\n );\n message = data.message;\n toast($t.git?.commit_message_generated, \"success\");\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message || $t.git?.commit_message_failed, \"error\");\n } finally {\n generatingMessage = false;\n }\n }\n // [/DEF:handleGenerateMessage:Function]\n" }, @@ -108665,6 +112574,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108714,7 +112632,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -108728,17 +112648,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -108748,7 +112663,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108824,7 +112742,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -108838,17 +112758,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -108858,7 +112773,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -108920,6 +112838,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108947,6 +112874,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -108976,6 +112912,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -109023,6 +112968,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -109084,7 +113038,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -109098,17 +113054,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Component' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Component" + "actual_complexity": 1, + "contract_type": "Component" } }, { @@ -109118,7 +113069,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -109152,6 +113106,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -109169,6 +113124,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -109200,6 +113156,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109227,6 +113192,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109254,6 +113228,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109281,6 +113264,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109308,6 +113300,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109335,6 +113336,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109362,6 +113372,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109389,6 +113408,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109416,6 +113444,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109443,6 +113480,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109470,6 +113516,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109497,6 +113552,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109524,6 +113588,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109551,6 +113624,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109578,6 +113660,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109605,6 +113696,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109632,6 +113732,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109659,6 +113768,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109686,6 +113804,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109713,6 +113840,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109740,6 +113876,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -109754,7 +113899,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI Tests", "PURPOSE": "Protect unresolved-merge dialog contract in GitManager pull flow.", "SEMANTICS": [ @@ -109772,22 +113917,7 @@ "target_ref": "[GitManager]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:GitManagerUnfinishedMergeIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst COMPONENT_PATH = path.resolve(\n process.cwd(),\n 'src/components/git/GitManager.svelte',\n);\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('Number(error?.status) !== 409');\n expect(source).toContain(\"payload.error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(source).toContain('function openUnfinishedMergeDialogFromError(error)');\n expect(source).toContain('showUnfinishedMergeDialog = true;');\n expect(source).toContain('const handledByDialog = openUnfinishedMergeDialogFromError(e);');\n expect(source).toContain('await loadMergeRecoveryState();');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('{#if showUnfinishedMergeDialog && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('handleCopyUnfinishedMergeCommands');\n expect(source).toContain('handleOpenConflictResolver');\n expect(source).toContain('handleAbortUnfinishedMerge');\n expect(source).toContain('handleContinueUnfinishedMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// [/DEF:GitManagerUnfinishedMergeIntegrationTest:Module]\n" }, @@ -109800,7 +113930,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "UI component for previewing generated dataset documentation before saving.", "TYPE": "{Object}", @@ -109822,7 +113952,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -109840,12 +113972,6 @@ "tag": "TYPE", "message": "@TYPE is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -109860,7 +113986,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "UI form for managing LLM provider configurations.", "UX_STATE": "Loading -> Default" @@ -109881,7 +114007,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -109893,12 +114021,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -109913,7 +114035,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Edit action keeps explicit click handler and opens normalized edit form.", "LAYER": "UI Tests", "PURPOSE": "Protect edit and delete interaction contracts in LLM provider settings UI.", @@ -109942,20 +114064,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } } ], "anchor_syntax": "def", @@ -109970,7 +114078,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Contract checks ensure edit click cannot degrade into no-op flow.", "PRE": "ProviderConfig component source exists in expected path.", "PURPOSE": "Validate edit and delete handler wiring plus normalized edit form state mapping." @@ -110015,7 +114123,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "EVENTS": "ondelete/onnavigate callback props - Raised when a file is deleted or navigation is requested.", "LAYER": "UI", "PROPS": "files (Array) - List of StoredFile objects.", @@ -110050,7 +114158,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110076,7 +114186,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110088,12 +114201,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -110140,6 +114247,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -110191,6 +114307,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -110242,6 +114367,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -110284,6 +114418,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -110298,7 +114441,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "EVENTS": "onuploaded callback prop - Invoked when a file is successfully uploaded.", "LAYER": "UI", "PROPS": "None", @@ -110332,7 +114475,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110358,7 +114503,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110370,12 +114518,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -110413,6 +114555,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -110437,6 +114588,15 @@ "tag": "PARAM", "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -110451,7 +114611,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Renders a single log entry with stacked layout optimized for narrow drawer panels.", "SEMANTICS": [ @@ -110479,7 +114639,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110499,7 +114661,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110518,12 +114683,6 @@ "message": "@TYPE is not defined in axiom_config.yaml tags", "detail": null }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -110536,6 +114695,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -110557,7 +114717,17 @@ "PURPOSE": "Format ISO timestamp to HH:MM:SS" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:formatTime:Function]\n /** @PURPOSE Format ISO timestamp to HH:MM:SS */\n function formatTime(timestamp) {\n if (!timestamp) return \"\";\n const date = new Date(timestamp);\n return date.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n }\n // [/DEF:formatTime:Function]\n" }, @@ -110570,7 +114740,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Compact filter toolbar for logs — level, source, and text search in a single dense row.", "SEMANTICS": [ @@ -110596,7 +114766,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110616,7 +114788,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110629,12 +114804,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -110647,6 +114816,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -110665,7 +114835,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Must always display logs in chronological order and respect auto-scroll preference.", "LAYER": "UI", "PURPOSE": "Combines log filtering and display into a single cohesive dark-themed panel.", @@ -110709,7 +114879,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110729,7 +114901,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110742,12 +114917,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -110760,6 +114929,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -110777,6 +114947,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -110819,7 +114990,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110832,6 +115005,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -110839,7 +115021,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110873,6 +115058,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -110915,7 +115101,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -110928,6 +115116,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -110935,7 +115132,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -110969,6 +115169,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -111010,6 +115211,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111049,7 +115259,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -111062,6 +115274,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -111069,7 +115290,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -111103,6 +115327,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -111146,6 +115371,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -111197,6 +115431,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -111248,7 +115491,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -111261,6 +115506,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -111268,7 +115522,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -111302,6 +115559,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -111319,6 +115577,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -111360,6 +115619,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111397,6 +115665,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111434,6 +115711,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111448,7 +115734,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Simple counter demo component", "UX_STATE": "Incremented -> Count increases immediately after button activation." @@ -111462,7 +115748,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -111474,12 +115762,6 @@ "actual_complexity": 2, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -111494,7 +115776,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra-API", "PURPOSE": "Handles all communication with the backend API.", "SEMANTICS": [ @@ -111513,20 +115795,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra-API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infra-API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -111539,6 +115807,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -111582,6 +115851,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "TYPE", @@ -111624,6 +115902,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111661,6 +115948,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111707,6 +116003,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -111729,7 +116034,17 @@ "PURPOSE": "Returns headers with Authorization if token exists." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:getAuthHeaders:Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// [/DEF:getAuthHeaders:Function]\n" }, @@ -111774,6 +116089,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -111818,6 +116142,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "TYPE", @@ -111869,6 +116202,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -111920,6 +116262,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -111962,6 +116313,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -111992,7 +116352,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -112018,7 +116380,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Pure functions produce deterministic output. Async wrappers propagate structured errors.", "LAYER": "Infra (Tests)", "PURPOSE": "Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.", @@ -112047,20 +116409,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infra (Tests)" - } } ], "anchor_syntax": "def", @@ -112098,6 +116446,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -112135,6 +116492,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -112172,6 +116538,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -112186,7 +116561,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All assistant requests must use requestApi wrapper (no native fetch).", "LAYER": "Infra-API", "PURPOSE": "API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval.", @@ -112215,20 +116590,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra-API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infra-API" - } } ], "anchor_syntax": "def", @@ -112266,6 +116627,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112303,6 +116673,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112340,6 +116719,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112377,6 +116765,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112414,6 +116811,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112451,6 +116857,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112488,6 +116903,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112502,7 +116926,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra-API", "PURPOSE": "Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.", "SEMANTICS": [ @@ -112521,22 +116945,7 @@ "target_ref": "api_module" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra-API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infra-API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:DatasetReviewApi:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, api, session-version, headers, conflict-handling\n// @PURPOSE: Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { requestApi } from '$lib/api.js';\n\n// [DEF:buildDatasetReviewRequestOptions:Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// [/DEF:buildDatasetReviewRequestOptions:Function]\n\n// [DEF:requestDatasetReviewApi:Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// [/DEF:requestDatasetReviewApi:Function]\n\n// [DEF:isDatasetReviewConflictError:Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// [/DEF:isDatasetReviewConflictError:Function]\n\n// [DEF:getDatasetReviewConflictMessage:Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// [/DEF:getDatasetReviewConflictMessage:Function]\n\n// [DEF:extractDatasetReviewVersion:Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// [/DEF:extractDatasetReviewVersion:Function]\n\n// [DEF:normalizeDatasetReviewDetail:Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// [/DEF:normalizeDatasetReviewDetail:Function]\n\n// [/DEF:DatasetReviewApi:Module]\n" }, @@ -112572,6 +116981,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112609,6 +117027,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112646,6 +117073,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112683,6 +117119,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112720,6 +117165,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112757,6 +117211,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112771,7 +117234,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Uses existing api wrapper methods and returns structured errors for UI-state mapping.", "LAYER": "Infra", "POST": "Report list and detail helpers return backend payloads or normalized UI-safe errors.", @@ -112839,6 +117302,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112876,6 +117348,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112914,6 +117395,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112951,6 +117441,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -112988,6 +117487,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113025,6 +117533,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113062,6 +117579,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113099,6 +117625,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113136,6 +117671,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113173,6 +117717,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113188,17 +117741,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:fetchDatasourceColumns:Function]\n\nexport async function fetchDatasources(envId, search = '') {\n try {\n return await api.fetchApi(\n `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}`\n );\n } catch (error) {\n throw normalizeTranslateError(error, 'Failed to fetch datasources');\n }\n}\n\nexport async function fetchDatasourceColumns(datasourceId, envId) {\n\ttry {\n\t\treturn await api.fetchApi(\n\t\t\t`/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}`\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch datasource columns');\n\t}\n}\n\n// [DEF:fetchPreview:Function]\n// @PURPOSE: Trigger a translation preview for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns preview session with rows and cost estimate.\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) {\n\t\t\tbody.env_id = envId;\n\t\t}\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n// [/DEF:fetchPreview:Function]\n\n// [DEF:approveRow:Function]\n// @PURPOSE: Approve a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'approve'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// [/DEF:approveRow:Function]\n\n// [DEF:editRow:Function]\n// @PURPOSE: Edit a preview row's translation.\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'edit',\n\t\t\ttranslation: translation\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// [/DEF:editRow:Function]\n\n// [DEF:rejectRow:Function]\n// @PURPOSE: Reject a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'reject'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// [/DEF:rejectRow:Function]\n\n// [DEF:acceptPreview:Function]\n// @PURPOSE: Accept a preview session, enabling full translation execution.\n// @PRE: jobId is non-empty string.\n// @POST: Returns accepted preview session.\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n// [/DEF:acceptPreview:Function]\n\n// [DEF:fetchPreviewRecords:Function]\n// @PURPOSE: Fetch records for a preview session.\n// @PRE: sessionId is non-empty string.\n// @POST: Returns list of preview records.\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// [/DEF:fetchPreviewRecords:Function]\n// [/DEF:fetchDatasourceColumns:Function]\n" }, @@ -113234,6 +117777,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113271,6 +117823,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113308,6 +117869,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113345,6 +117915,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113382,6 +117961,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113419,6 +118007,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113456,6 +118053,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113493,6 +118099,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113530,6 +118145,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113567,6 +118191,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113604,6 +118237,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113641,6 +118283,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113678,6 +118329,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113715,6 +118375,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113745,7 +118414,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -113794,6 +118465,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113831,6 +118511,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -113848,7 +118537,17 @@ "PURPOSE": "Fetch schedule for a translation job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchSchedule:Function]\n// @PURPOSE: Fetch schedule for a translation job.\nexport async function fetchSchedule(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch schedule');\n\t}\n}\n// [/DEF:fetchSchedule:Function]\n" }, @@ -113864,7 +118563,17 @@ "PURPOSE": "Create or update schedule for a translation job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:setSchedule:Function]\n// @PURPOSE: Create or update schedule for a translation job.\nexport async function setSchedule(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to set schedule');\n\t}\n}\n// [/DEF:setSchedule:Function]\n" }, @@ -113880,7 +118589,17 @@ "PURPOSE": "Delete schedule for a translation job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:deleteSchedule:Function]\n// @PURPOSE: Delete schedule for a translation job.\nexport async function deleteSchedule(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete schedule');\n\t}\n}\n// [/DEF:deleteSchedule:Function]\n" }, @@ -113896,7 +118615,17 @@ "PURPOSE": "Enable a schedule." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:enableSchedule:Function]\n// @PURPOSE: Enable a schedule.\nexport async function enableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to enable schedule');\n\t}\n}\n// [/DEF:enableSchedule:Function]\n" }, @@ -113912,7 +118641,17 @@ "PURPOSE": "Disable a schedule." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:disableSchedule:Function]\n// @PURPOSE: Disable a schedule.\nexport async function disableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to disable schedule');\n\t}\n}\n// [/DEF:disableSchedule:Function]\n" }, @@ -113928,7 +118667,17 @@ "PURPOSE": "Preview next N schedule executions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchNextExecutions:Function]\n// @PURPOSE: Preview next N schedule executions.\nexport async function fetchNextExecutions(jobId, n = 3) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch next executions');\n\t}\n}\n// [/DEF:fetchNextExecutions:Function]\n" }, @@ -113944,7 +118693,17 @@ "PURPOSE": "Fetch all runs with cross-job filtering and pagination." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchAllRuns:Function]\n// @PURPOSE: Fetch all runs with cross-job filtering and pagination.\nexport async function fetchAllRuns(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.job_id) params.append('job_id', options.job_id);\n\t\tif (options.status) params.append('status', options.status);\n\t\tif (options.trigger_type) params.append('trigger_type', options.trigger_type);\n\t\tif (options.created_by) params.append('created_by', options.created_by);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch runs');\n\t}\n}\n// [/DEF:fetchAllRuns:Function]\n" }, @@ -113960,7 +118719,17 @@ "PURPOSE": "Fetch detailed run info with config_snapshot, records, events." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchRunDetail:Function]\n// @PURPOSE: Fetch detailed run info with config_snapshot, records, events.\nexport async function fetchRunDetail(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/detail`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run detail');\n\t}\n}\n// [/DEF:fetchRunDetail:Function]\n" }, @@ -113976,7 +118745,17 @@ "PURPOSE": "Fetch aggregated metrics for a specific job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchJobMetrics:Function]\n// @PURPOSE: Fetch aggregated metrics for a specific job.\nexport async function fetchJobMetrics(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/metrics`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch job metrics');\n\t}\n}\n// [/DEF:fetchJobMetrics:Function]\n" }, @@ -113992,7 +118771,17 @@ "PURPOSE": "Fetch aggregated metrics for all jobs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:fetchAllMetrics:Function]\n// @PURPOSE: Fetch aggregated metrics for all jobs.\nexport async function fetchAllMetrics() {\n\ttry {\n\t\treturn await api.fetchApi('/translate/metrics');\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch metrics');\n\t}\n}\n// [/DEF:fetchAllMetrics:Function]\n" }, @@ -114008,7 +118797,17 @@ "PURPOSE": "Download skipped records CSV for a run." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:downloadSkippedCsv:Function]\n// @PURPOSE: Download skipped records CSV for a run.\nexport async function downloadSkippedCsv(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/skipped.csv`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to download skipped CSV');\n\t}\n}\n// [/DEF:downloadSkippedCsv:Function]\n" }, @@ -114021,7 +118820,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Verifies frontend RBAC permission parsing and access checks.", "SEMANTICS": [ @@ -114039,22 +118838,7 @@ "target_ref": "[Permissions]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:PermissionsTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, auth, permissions, rbac\n// @PURPOSE: Verifies frontend RBAC permission parsing and access checks.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [Permissions]\n\nimport { describe, it, expect } from \"vitest\";\nimport {\n normalizePermissionRequirement,\n isAdminUser,\n hasPermission,\n} from \"../permissions.js\";\n\ndescribe(\"auth.permissions\", () => {\n it(\"normalizes resource-only requirement with default READ action\", () => {\n expect(normalizePermissionRequirement(\"admin:settings\")).toEqual({\n resource: \"admin:settings\",\n action: \"READ\",\n });\n });\n\n it(\"normalizes explicit resource:action requirement\", () => {\n expect(normalizePermissionRequirement(\"admin:settings:write\")).toEqual({\n resource: \"admin:settings\",\n action: \"WRITE\",\n });\n });\n\n it(\"detects admin role case-insensitively\", () => {\n const user = {\n roles: [{ name: \"ADMIN\" }],\n };\n expect(isAdminUser(user)).toBe(true);\n });\n\n it(\"denies when user is absent and permission is required\", () => {\n expect(hasPermission(null, \"tasks\", \"READ\")).toBe(false);\n });\n\n it(\"grants when permission object matches resource and action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"READ\")).toBe(true);\n });\n\n it(\"grants when requirement is provided as resource:action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"admin:settings\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"admin:settings:READ\")).toBe(true);\n });\n\n it(\"grants when string permission entry matches\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [\"plugin:migration:READ\"],\n },\n ],\n };\n\n expect(hasPermission(user, \"plugin:migration\", \"READ\")).toBe(true);\n });\n\n it(\"denies when action does not match\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"WRITE\")).toBe(false);\n });\n\n it(\"always grants for admin role regardless of explicit permissions\", () => {\n const adminUser = {\n roles: [{ name: \"Admin\", permissions: [] }],\n };\n\n expect(hasPermission(adminUser, \"admin:users\", \"READ\")).toBe(true);\n expect(hasPermission(adminUser, \"plugin:migration\", \"EXECUTE\")).toBe(true);\n });\n});\n\n// [/DEF:PermissionsTest:Module]\n" }, @@ -114067,7 +118851,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Admin role always bypasses explicit permission checks.", "LAYER": "Domain", "PURPOSE": "Shared frontend RBAC utilities for route guards and menu visibility.", @@ -114132,6 +118916,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114169,6 +118962,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114206,6 +119008,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114220,7 +119031,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Feature", "PURPOSE": "Manages the global authentication state on the frontend.", "SEMANTICS": [ @@ -114257,7 +119068,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -114277,7 +119090,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -114290,20 +119105,6 @@ "contract_type": "Store" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -114316,7 +119117,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -114336,7 +119139,10 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -114370,6 +119176,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "MODIFIED_BY" @@ -114404,7 +119211,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -114469,6 +119278,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -114535,6 +119353,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114579,6 +119406,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114616,6 +119452,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114660,6 +119505,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -114674,7 +119528,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[{variant,className,assistantChatStore,datasetReviewSessionId?}] -> Output[{messages,conversations,conversationId,llmReady,panelVisible}]", "INVARIANT": "Risky operations are executed only through explicit confirm action.", "LAYER": "UI", @@ -114726,7 +119580,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -114746,7 +119602,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -114758,24 +119617,6 @@ "actual_complexity": 5, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -114790,7 +119631,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[{sessionId, disabled}] -> Output[{clarification_state|clarification_session|current_question|session}] via onupdated callback.", "LAYER": "UI", "POST": "Clarification queue state is readable in-chat and answer mutations refresh the active question state without requiring a dedicated dialog.", @@ -114846,7 +119687,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -114866,7 +119709,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -114879,30 +119725,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -114915,6 +119737,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -114932,6 +119755,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -114949,6 +119773,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -114967,7 +119792,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Critical assistant UX states and action hooks remain present in component source.", "LAYER": "UI Tests", "PURPOSE": "Contract-level integration checks for assistant chat panel implementation and localization wiring.", @@ -115001,20 +119826,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -115078,16 +119889,25 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_not_for_contract_type", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -115125,6 +119945,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -115162,6 +119991,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -115176,7 +120014,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI Tests", "PURPOSE": "Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.", "SEMANTICS": [ @@ -115195,22 +120033,7 @@ "target_ref": "[AssistantChatPanel]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:AssistantClarificationIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: assistant, clarification, integration-test, dataset-review, mixed-initiative\n// @PURPOSE: Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@testing-library/svelte\";\nimport AssistantChatPanel from \"../AssistantChatPanel.svelte\";\nimport * as assistantApi from \"$lib/api/assistant\";\nimport { api, requestApi } from \"$lib/api.js\";\nimport { setSession as setDatasetReviewSession } from \"$lib/stores/datasetReviewSession.js\";\n\nvi.mock(\"$lib/api/assistant\", () => ({\n getAssistantHistory: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false })),\n getAssistantConversations: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false, active_total: 0, archived_total: 0 })),\n deleteAssistantConversation: vi.fn(),\n confirmAssistantOperation: vi.fn(),\n cancelAssistantOperation: vi.fn(),\n sendAssistantMessage: vi.fn(),\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n getLlmStatus: vi.fn(() => Promise.resolve({ configured: true, reason: \"ok\" })),\n fetchApi: vi.fn(),\n },\n requestApi: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/datasetReviewSession.js\", () => ({\n setSession: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/taskDrawer.js\", () => ({\n openDrawerForTask: vi.fn(),\n}));\n\nconst { assistantState } = vi.hoisted(() => ({\n assistantState: (() => {\n let value = {\n isOpen: true,\n conversationId: \"conv-1\",\n datasetReviewSessionId: \"session-1\",\n seedMessage: \"\",\n focusTarget: null,\n };\n const subscribers = new Set();\n return {\n subscribe(fn) {\n subscribers.add(fn);\n fn(value);\n return () => subscribers.delete(fn);\n },\n update(updater) {\n value = updater(value);\n subscribers.forEach((fn) => fn(value));\n },\n };\n })(),\n}));\n\nvi.mock(\"$lib/stores/assistantChat\", () => ({\n assistantChatStore: { subscribe: assistantState.subscribe },\n closeAssistantChat: vi.fn(),\n setAssistantConversationId: vi.fn(),\n setAssistantSeedMessage: vi.fn(),\n setAssistantFocusTarget: vi.fn(),\n}));\n\nvi.mock(\"$app/navigation\", () => ({ goto: vi.fn() }));\nvi.mock(\"../../../../services/gitService.js\", () => ({ gitService: { getDiff: vi.fn() } }));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: { error: \"Common error\" },\n assistant: {\n title: \"Assistant\",\n close: \"Close\",\n conversations: \"Conversations\",\n new: \"New\",\n active: \"Active\",\n archived: \"Archived\",\n more: \"More\",\n loading_history: \"Loading history\",\n loading_older: \"Loading older\",\n try_commands: \"Try commands\",\n sample_command_branch: \"branch\",\n sample_command_migration: \"migration\",\n sample_command_status: \"status\",\n you: \"You\",\n assistant: \"Assistant\",\n thinking: \"Thinking\",\n input_placeholder: \"Type command\",\n send: \"Send\",\n conversation: \"Conversation\",\n session_scope_title: \"Active review context\",\n session_scope_label: \"Session\",\n focus_target_label: \"Focus\",\n clear_focus_action: \"Clear focus\",\n states: { success: \"Success\" },\n open_task_drawer: \"Open task drawer\",\n },\n dataset_review: {\n clarification: {\n title: \"Clarification queue\",\n progress_label: \"Progress\",\n active_question_label: \"Active question\",\n why_it_matters_label: \"Why it matters\",\n current_guess_label: \"Current guess\",\n topic_label: \"Topic\",\n custom_answer_label: \"Custom answer\",\n answer_action: \"Answer\",\n custom_answer_action: \"Use custom answer\",\n skip_action: \"Skip\",\n expert_review_action: \"Expert review\",\n completed: \"No active clarification question remains.\",\n resume_action: \"Resume clarification\",\n feedback_label: \"Feedback\",\n feedback_prompt: \"Record whether the clarification result was useful.\",\n feedback_up_action: \"Thumbs up\",\n feedback_down_action: \"Thumbs down\",\n messages: {\n saving: \"Saving clarification answer...\",\n saved: \"Clarification answer saved.\",\n resumed: \"Clarification queue resumed.\",\n },\n },\n },\n dashboard: {},\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"AssistantChatPanel clarification integration\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockImplementation((endpoint) => {\n if (endpoint === \"/dataset-orchestration/sessions/session-1/clarification\") {\n return Promise.resolve({\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: \"question-1\",\n resolved_count: 1,\n remaining_count: 2,\n },\n current_question: {\n question_id: \"question-1\",\n topic_ref: \"profile.summary\",\n question_text: \"Which customer label should be used?\",\n why_it_matters: \"This label is shown to reviewers.\",\n current_guess: \"Customer name\",\n options: [{ value: \"Customer name\", label: \"Customer name\", is_recommended: true }],\n },\n });\n }\n if (endpoint === \"/dataset-orchestration/sessions/session-1\") {\n return Promise.resolve({ session_id: \"session-1\", session_version: 4, version: 4, profile: { business_summary: \"Updated summary\" } });\n }\n return Promise.resolve(null);\n });\n requestApi.mockResolvedValue({\n clarification_state: {\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: null,\n resolved_count: 2,\n remaining_count: 1,\n },\n current_question: null,\n },\n session: { session_id: \"session-1\", session_version: 4, version: 4 },\n });\n });\n\n it(\"renders clarification queue inside assistant and refreshes session after answering\", async () => {\n render(AssistantChatPanel);\n\n expect(await screen.findByText(\"Clarification queue\")).toBeTruthy();\n expect(screen.getByText(\"Which customer label should be used?\")).toBeTruthy();\n expect(screen.getByText(\"This label is shown to reviewers.\")).toBeTruthy();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Answer\" }));\n\n await waitFor(() => {\n expect(requestApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/clarification/answers\",\n \"POST\",\n expect.objectContaining({ question_id: \"question-1\", answer_kind: \"selected\", answer_value: \"Customer name\" }),\n );\n });\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions/session-1\");\n expect(setDatasetReviewSession).toHaveBeenCalled();\n });\n });\n});\n// [/DEF:AssistantClarificationIntegrationTest:Module]\n" }, @@ -115223,7 +120046,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Starting a new conversation must not trigger history reload that overwrites the first local user message.", "LAYER": "UI Tests", "PURPOSE": "Verify first optimistic user message stays visible while a new conversation request is pending.", @@ -115251,20 +120074,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } } ], "anchor_syntax": "def", @@ -115279,7 +120088,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "First user message remains visible before pending send request resolves.", "PRE": "Assistant panel renders with open state and mocked network dependencies.", "PURPOSE": "Guard optimistic first-message UX against history reload race in new conversation flow." @@ -115324,7 +120133,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input -> Dataset review preview payload { sessionId, preview, previewState }; Output -> onupdated({ preview, preview_state }) route-shell refresh payload and onjump({ target }) recovery navigation signal.", "LAYER": "UI", "POST": "Users can distinguish missing, pending, ready, stale, and error preview states and can trigger only Superset-backed preview generation.", @@ -115374,7 +120183,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -115394,7 +120205,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -115407,30 +120221,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -115443,6 +120233,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115460,6 +120251,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115478,7 +120270,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input -> Dataset review execution payload { sessionId, mappings[], importedFilters[], templateVariables[] }; Output -> onupdated({ mapping | mappings, preview_state }) route-shell refresh payload.", "LAYER": "UI", "POST": "Users can review effective mapping values, approve warning-sensitive transformations, or manually override them while unresolved required-value blockers remain visible.", @@ -115528,7 +120320,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -115548,7 +120342,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -115561,30 +120358,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -115597,6 +120370,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115614,6 +120388,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115632,7 +120407,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input -> Dataset review launch payload { sessionId, session, findings[], mappings[], preview, previewState, latestRunContext }; Output -> onupdated({ launch_result, preview_state }) workspace refresh payload and onjump({ target }) remediation navigation signal.", "LAYER": "UI", "POST": "Users can see why launch is blocked or confirm a run-ready launch with explicit SQL Lab handoff evidence.", @@ -115682,7 +120457,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -115702,7 +120479,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -115715,30 +120495,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -115751,6 +120507,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115768,6 +120525,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115786,7 +120544,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input -> Dataset review session detail { sessionId, fields[], semanticSources[] }; Output -> onupdated(updatedField | { fields: updatedFields }) refresh payload.", "LAYER": "UI", "POST": "Users can review the current semantic value, accept a candidate, apply manual override, and lock or unlock field state without violating backend provenance rules.", @@ -115836,7 +120594,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -115856,7 +120616,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -115869,30 +120632,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -115905,6 +120644,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115922,6 +120662,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -115940,7 +120681,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Collect initial dataset source input through Superset link paste or dataset selection entry paths.", "SEMANTICS": [ @@ -115971,7 +120712,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -115991,7 +120734,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -116004,30 +120750,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -116040,6 +120762,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -116058,7 +120781,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Present validation findings grouped by severity with explicit resolution and actionability signals.", "SEMANTICS": [ @@ -116089,7 +120812,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116109,7 +120834,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -116122,30 +120850,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -116158,6 +120862,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -116176,7 +120881,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify source intake entry paths, validation feedback, and submit payload behavior for US1.", "SEMANTICS": [ @@ -116244,22 +120949,45 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -116274,7 +121002,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify US2 dataset review surfaces keep semantic review actionable, route clarification through AssistantChatPanel, and preserve explicit preview/mapping gates.", "SEMANTICS": [ @@ -116325,10 +121053,25 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -116343,7 +121086,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify US3 mapping review, Superset preview, and launch confirmation UX contracts.", "SEMANTICS": [ @@ -116424,22 +121167,45 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -116454,7 +121220,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify grouped findings visibility, empty state, and remediation jump behavior for US1.", "SEMANTICS": [ @@ -116522,22 +121288,45 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -116552,7 +121341,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Visual grid summary representing dashboard health status counts.", "TYPE": "{{", @@ -116575,7 +121364,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116593,18 +121384,6 @@ "tag": "TYPE", "message": "@TYPE is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -116619,7 +121398,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "POST": "Form submission forwards the current draft to onSave and cancel delegates dismissal to the parent callback without mutating external state directly.", "PRE": "Parent provides callable onSave/onCancel handlers and an environments collection that may be empty but remains array-like.", @@ -116645,7 +121424,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116681,24 +121462,6 @@ "tag": "TYPE", "message": "@TYPE is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -116713,7 +121476,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Always shows current page path.", "LAYER": "UI", "PURPOSE": "Display page hierarchy navigation.", @@ -116752,7 +121515,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116764,24 +121529,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -116796,7 +121543,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "SidebarCategory[] -> RenderedNavigationTree", "INVARIANT": "Always shows active category and item", "LAYER": "UI", @@ -116853,7 +121600,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116873,7 +121622,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -116906,30 +121658,6 @@ "actual_complexity": 5, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -116944,7 +121672,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "TaskDrawerState -> RecentTaskList | ActiveTaskDetail", "INVARIANT": "Drawer shows logs for active task or remains closed", "LAYER": "UI", @@ -116978,7 +121706,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -116998,7 +121728,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -117037,30 +121770,6 @@ "tag": "TEST_DATA", "message": "@TEST_DATA is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -117075,7 +121784,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "ws is closed and set to null", "PRE": "ws may or may not be initialized", "PURPOSE": "Disconnects the active WebSocket connection", @@ -117109,10 +121818,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } }, { "code": "invalid_relation_predicate", @@ -117126,6 +121849,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -117167,6 +121891,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -117204,6 +121937,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -117218,7 +121960,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Drawer switches to list view and reloads tasks", "PRE": "Drawer is open and activeTaskId is set", "PURPOSE": "Return to task list view from task details", @@ -117252,10 +121994,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } }, { "code": "invalid_relation_predicate", @@ -117269,6 +122025,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -117287,7 +122044,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "SearchQuery -> SearchSection[] | ProfilePreferences -> TaskDrawerPreference", "INVARIANT": "Always visible on non-login pages", "LAYER": "UI", @@ -117358,7 +122115,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -117378,7 +122137,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -117412,30 +122174,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "unknown_tag", "tag": "UX_TEST", @@ -117455,7 +122193,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Verifies RBAC-based sidebar category and subitem visibility.", "SEMANTICS": [ @@ -117474,22 +122212,7 @@ "target_ref": "[SidebarNavigation]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:SidebarNavigationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, sidebar, navigation, rbac, permissions\n// @PURPOSE: Verifies RBAC-based sidebar category and subitem visibility.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [SidebarNavigation]\n\nimport { describe, it, expect } from \"vitest\";\nimport { buildSidebarCategories } from \"../sidebarNavigation.js\";\n\nconst i18nState = {\n nav: {\n dashboards: \"Dashboards\",\n overview: \"Overview\",\n datasets: \"Datasets\",\n all_datasets: \"All datasets\",\n storage: \"Storage\",\n backups: \"Backups\",\n repositories: \"Repositories\",\n reports: \"Reports\",\n profile: \"Profile\",\n admin: \"Admin\",\n admin_users: \"User management\",\n admin_roles: \"Role management\",\n settings: \"Settings\",\n },\n};\n\nfunction makeUser(roles) {\n return { roles };\n}\n\ndescribe(\"sidebarNavigation\", () => {\n it(\"shows only categories available to a non-admin user\", () => {\n const user = makeUser([\n {\n name: \"Operator\",\n permissions: [\n { resource: \"plugin:migration\", action: \"READ\" },\n { resource: \"tasks\", action: \"READ\" },\n ],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n expect(categoryIds).toEqual([\"dashboards\", \"datasets\", \"reports\", \"profile\"]);\n });\n\n it(\"hides admin category when user has no admin permissions\", () => {\n const user = makeUser([\n {\n name: \"Viewer\",\n permissions: [{ resource: \"plugin:migration\", action: \"READ\" }],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeUndefined();\n });\n\n it(\"shows full admin category for admin role\", () => {\n const user = makeUser([\n {\n name: \"Admin\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeDefined();\n expect(adminCategory.subItems.map((item) => item.path)).toEqual([\n \"/admin/users\",\n \"/admin/roles\",\n \"/settings\",\n ]);\n });\n\n it(\"keeps profile visible even without explicit plugin permissions\", () => {\n const user = makeUser([\n {\n name: \"Basic\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n expect(categoryIds).toEqual([\"profile\"]);\n });\n});\n\n// [/DEF:SidebarNavigationTest:Module]\n" }, @@ -117502,7 +122225,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Contract-focused unit tests for Breadcrumbs.svelte logic and UX annotations", "UX_STATE": "Loading -> Default" @@ -117517,10 +122240,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -117535,7 +122272,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Unit tests for Sidebar.svelte component", "UX_STATE": "Loading -> Default" @@ -117550,10 +122287,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -117568,7 +122319,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Unit tests for TaskDrawer.svelte component", "UX_STATE": "Loading -> Default" @@ -117583,10 +122334,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -117601,7 +122366,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Unit tests for TopNavbar.svelte component", "UX_STATE": "Loading -> Default" @@ -117616,10 +122381,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -117634,7 +122413,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Admin role can access all categories and subitems through permission utility.", "LAYER": "UI", "PURPOSE": "Build sidebar navigation categories filtered by current user permissions.", @@ -117706,6 +122485,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -117743,6 +122531,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -117757,7 +122554,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Unknown task type always uses fallback profile.", "LAYER": "UI", "PURPOSE": "Render one report with explicit textual type label and profile-driven visual variant.", @@ -117803,7 +122600,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -117823,7 +122622,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -117856,18 +122658,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -117882,7 +122672,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Failed/partial reports surface actionable hints when available.", "LAYER": "UI", "POST": "Detail output renders stable placeholders and exposes next-step guidance when available.", @@ -117925,7 +122715,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -117945,7 +122737,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -117978,18 +122773,6 @@ "actual_complexity": 4, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -118004,7 +122787,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Every rendered row shows task_type label, status, summary, and updated_at.", "LAYER": "UI", "PURPOSE": "Render unified list of normalized reports with canonical minimum fields.", @@ -118051,7 +122834,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -118071,7 +122856,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -118104,24 +122892,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -118136,7 +122906,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Each test asserts at least one observable UX contract outcome.", "LAYER": "UI", "PURPOSE": "Test UX states and transitions for ReportCard component", @@ -118234,16 +123004,25 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_not_for_contract_type", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -118258,7 +123037,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Failed report detail exposes actionable next actions when available.", "LAYER": "UI (Tests)", "PURPOSE": "Validate detail-panel behavior for failed reports and recovery guidance visibility.", @@ -118293,20 +123072,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } } ], "anchor_syntax": "def", @@ -118321,7 +123086,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Detail UX tests keep placeholder-safe rendering and recovery visibility verifiable.", "LAYER": "UI", "PURPOSE": "Test UX states and recovery for ReportDetailPanel component", @@ -118364,7 +123129,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Unknown task_type always resolves to the fallback profile.", "LAYER": "UI (Tests)", "PURPOSE": "Validate report type profile mapping and unknown fallback behavior.", @@ -118392,20 +123157,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } } ], "anchor_syntax": "def", @@ -118420,7 +123171,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Guard test for report filter responsiveness on moderate in-memory dataset.", "SEMANTICS": [ @@ -118438,22 +123189,7 @@ "target_ref": "[UnifiedReportsPage]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:ReportsFilterPerformanceTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, reports, performance, filtering\n// @PURPOSE: Guard test for report filter responsiveness on moderate in-memory dataset.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [UnifiedReportsPage]\n\nimport { describe, it, expect } from 'vitest';\n\nfunction applyFilters(items, { taskType = 'all', status = 'all' } = {}) {\n return items.filter((item) => {\n const typeMatch = taskType === 'all' || item.task_type === taskType;\n const statusMatch = status === 'all' || item.status === status;\n return typeMatch && statusMatch;\n });\n}\n\nfunction makeDataset(size = 2000) {\n const taskTypes = ['llm_verification', 'backup', 'migration', 'documentation'];\n const statuses = ['success', 'failed', 'in_progress', 'partial'];\n const out = [];\n for (let i = 0; i < size; i += 1) {\n out.push({\n report_id: `r-${i}`,\n task_id: `t-${i}`,\n task_type: taskTypes[i % taskTypes.length],\n status: statuses[i % statuses.length],\n summary: `Report ${i}`,\n updated_at: '2026-02-22T10:00:00Z'\n });\n }\n return out;\n}\n\ndescribe('reports filter performance guard', () => {\n it('applies task_type+status filter quickly on 2000 records', () => {\n const dataset = makeDataset(2000);\n\n const start = Date.now();\n const result = applyFilters(dataset, { taskType: 'migration', status: 'failed' });\n const duration = Date.now() - start;\n\n expect(Array.isArray(result)).toBe(true);\n expect(duration).toBeLessThan(100);\n });\n});\n\n// [/DEF:ReportsFilterPerformanceTest:Module]\n" }, @@ -118466,7 +123202,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Test ReportsList component iteration and event forwarding.", "SEMANTICS": [ @@ -118532,16 +123268,25 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_not_for_contract_type", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -118556,7 +123301,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Mixed fixture includes all supported report types in one list.", "LAYER": "UI (Tests)", "PURPOSE": "Integration-style checks for unified mixed-type reports rendering expectations.", @@ -118591,20 +123336,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } } ], "anchor_syntax": "def", @@ -118619,21 +123350,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Deterministic mapping from report task_type to visual profile with one fallback." }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:ReportTypeProfiles:Module]\n// @COMPLEXITY: 2\n// @PURPOSE: Deterministic mapping from report task_type to visual profile with one fallback.\n\nimport { _ } from '$lib/i18n';\n\nexport const REPORT_TYPE_PROFILES = {\n llm_verification: {\n key: 'llm_verification',\n label: 'LLM',\n variant: 'bg-violet-100 text-violet-700',\n icon: 'sparkles',\n fallback: false\n },\n backup: {\n key: 'backup',\n label: () => _('nav.backups'),\n variant: 'bg-emerald-100 text-emerald-700',\n icon: 'archive',\n fallback: false\n },\n migration: {\n key: 'migration',\n label: () => _('nav.migration'),\n variant: 'bg-amber-100 text-amber-700',\n icon: 'shuffle',\n fallback: false\n },\n documentation: {\n key: 'documentation',\n label: 'Documentation',\n variant: 'bg-sky-100 text-sky-700',\n icon: 'file-text',\n fallback: false\n },\n unknown: {\n key: 'unknown',\n label: () => _('reports.unknown_type'),\n variant: 'bg-slate-100 text-slate-700',\n icon: 'help-circle',\n fallback: true\n }\n};\n\n// [DEF:getReportTypeProfile:Function]\n// @PURPOSE: Resolve visual profile by task type with guaranteed fallback.\n// @PRE: taskType may be known/unknown/empty.\n// @POST: Returns one profile object.\n//\n// @TEST_CONTRACT: GetReportTypeProfileModel ->\n// {\n// required_fields: {taskType: string},\n// invariants: [\n// \"Returns correct profile for known taskType\",\n// \"Returns 'unknown' profile for invalid or missing taskType\"\n// ]\n// }\n// @TEST_FIXTURE: valid_type -> {\"taskType\": \"migration\"}\n// @TEST_EDGE: invalid_type -> {\"taskType\": \"invalid\"}\n// @TEST_INVARIANT: fallbacks_to_unknown -> verifies: [invalid_type]\nexport function getReportTypeProfile(taskType) {\n const key = typeof taskType === 'string' ? taskType : 'unknown';\n console.log(\"[reports][ui][getReportTypeProfile][STATE:START]\");\n console.log(\"[reports][ui][getReportTypeProfile] Resolved type '\" + taskType + \"' to profile '\" + key + \"'\");\n return REPORT_TYPE_PROFILES[key] || REPORT_TYPE_PROFILES.unknown;\n}\n// [/DEF:getReportTypeProfile:Function]\n\n// [/DEF:ReportTypeProfiles:Module]\n" }, @@ -118670,6 +123391,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -118684,7 +123414,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Sidebar for collecting multiple incorrect terms across different rows," }, "relations": [], @@ -118724,6 +123454,15 @@ "actual_complexity": 4, "contract_type": "Component" } + }, + { + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } } ], "anchor_syntax": "def", @@ -118738,7 +123477,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "PURPOSE": "Schedule configuration for a translation job — cron/interval/once triggers, timezone, enable/disable.", "UX_FEEDBACK": "Next-3-executions preview; toast on save/error", @@ -118761,7 +123500,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -118774,24 +123515,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "POST", @@ -118832,7 +123555,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "PURPOSE": "Popup for submitting a term correction from a run result row.", "UX_FEEDBACK": "Toast on success/error; conflict dialog with action buttons", @@ -118861,7 +123584,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -118874,24 +123599,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "POST", @@ -118932,7 +123639,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Admin metrics dashboard for translation — per-job run counts, success/failure ratio," }, "relations": [], @@ -118972,6 +123679,15 @@ "actual_complexity": 4, "contract_type": "Component" } + }, + { + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } } ], "anchor_syntax": "def", @@ -118986,7 +123702,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "User can preview, approve/edit/reject rows, and accept the session to gate full execution.", "PRE": "jobId is a valid translation job ID.", @@ -119017,7 +123733,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -119030,24 +123748,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -119070,7 +123770,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "Real-time progress display with two-phase (translation + insert) and state transitions.", "PRE": "runId is a valid translation run ID.", @@ -119101,7 +123801,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -119114,24 +123816,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -119154,7 +123838,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "Results displayed with actions for retry, SQL audit, and Superset reference.", "PRE": "runId is a valid completed/failed/pending run.", @@ -119185,7 +123869,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -119198,24 +123884,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -119257,7 +123925,17 @@ "PURPOSE": "Unit tests for translate API preview functions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:TranslateApiTests:Class]\n// @PURPOSE: Unit tests for translate API preview functions.\ndescribe('Translate API Preview Functions', () => {\n // Hoisted mock functions so vi.mock() factories (also hoisted) can reference them.\n const mockPostApi = vi.hoisted(() => vi.fn());\n const mockRequestApi = vi.hoisted(() => vi.fn());\n const mockFetchApi = vi.hoisted(() => vi.fn());\n\n vi.mock('$lib/api.js', () => ({\n api: {\n postApi: mockPostApi,\n requestApi: mockRequestApi,\n fetchApi: mockFetchApi,\n }\n }));\n\n vi.mock('$lib/toasts.js', () => ({ addToast: vi.fn() }));\n\n beforeEach(() => {\n vi.resetModules();\n vi.clearAllMocks();\n });\n\n // [DEF:test_fetchPreview:Function]\n // @PURPOSE: Verify fetchPreview calls postApi with correct endpoint and payload.\n it('fetchPreview calls postApi with correct args', async () => {\n mockPostApi.mockResolvedValue({\n id: 'session-1',\n job_id: 'job-123',\n status: 'ACTIVE',\n records: [\n { id: 'r1', source_sql: 'Hello', target_sql: 'Привет', status: 'PENDING' }\n ],\n cost_estimate: {\n sample_size: 1,\n sample_total_tokens: 100,\n sample_cost: 0.0002\n }\n });\n\n const { fetchPreview } = await import('$lib/api/translate.js');\n const result = await fetchPreview('job-123', 5);\n\n expect(mockPostApi).toHaveBeenCalledWith('/translate/jobs/job-123/preview', { sample_size: 5 });\n expect(result.records).toHaveLength(1);\n expect(result.records[0].source_sql).toBe('Hello');\n expect(result.cost_estimate.sample_cost).toBe(0.0002);\n });\n // [/DEF:test_fetchPreview:Function]\n\n // [DEF:test_approveRow:Function]\n // @PURPOSE: Verify approveRow calls requestApi with correct action.\n it('approveRow calls requestApi with approve action', async () => {\n mockRequestApi.mockResolvedValue({\n id: 'r1',\n status: 'APPROVED'\n });\n\n const { approveRow } = await import('$lib/api/translate.js');\n const result = await approveRow('job-123', 'r1');\n\n expect(mockRequestApi).toHaveBeenCalledWith(\n '/translate/jobs/job-123/preview/rows/r1',\n 'PUT',\n { action: 'approve' }\n );\n expect(result.status).toBe('APPROVED');\n });\n // [/DEF:test_approveRow:Function]\n\n // [DEF:test_editRow:Function]\n // @PURPOSE: Verify editRow calls requestApi with edit action and translation.\n it('editRow calls requestApi with edit action', async () => {\n mockRequestApi.mockResolvedValue({\n id: 'r1',\n target_sql: 'Edited translation',\n status: 'APPROVED'\n });\n\n const { editRow } = await import('$lib/api/translate.js');\n const result = await editRow('job-123', 'r1', 'Edited translation');\n\n expect(mockRequestApi).toHaveBeenCalledWith(\n '/translate/jobs/job-123/preview/rows/r1',\n 'PUT',\n { action: 'edit', translation: 'Edited translation' }\n );\n expect(result.target_sql).toBe('Edited translation');\n });\n // [/DEF:test_editRow:Function]\n\n // [DEF:test_rejectRow:Function]\n // @PURPOSE: Verify rejectRow calls requestApi with reject action.\n it('rejectRow calls requestApi with reject action', async () => {\n mockRequestApi.mockResolvedValue({\n id: 'r1',\n status: 'REJECTED'\n });\n\n const { rejectRow } = await import('$lib/api/translate.js');\n const result = await rejectRow('job-123', 'r1');\n\n expect(mockRequestApi).toHaveBeenCalledWith(\n '/translate/jobs/job-123/preview/rows/r1',\n 'PUT',\n { action: 'reject' }\n );\n expect(result.status).toBe('REJECTED');\n });\n // [/DEF:test_rejectRow:Function]\n\n // [DEF:test_acceptPreview:Function]\n // @PURPOSE: Verify acceptPreview calls postApi with correct endpoint.\n it('acceptPreview calls postApi with correct endpoint', async () => {\n mockPostApi.mockResolvedValue({\n id: 'session-1',\n job_id: 'job-123',\n status: 'APPLIED',\n records: [{ id: 'r1', status: 'APPROVED' }]\n });\n\n const { acceptPreview } = await import('$lib/api/translate.js');\n const result = await acceptPreview('job-123');\n\n expect(mockPostApi).toHaveBeenCalledWith('/translate/jobs/job-123/preview/accept', {});\n expect(result.status).toBe('APPLIED');\n });\n // [/DEF:test_acceptPreview:Function]\n\n // [DEF:test_fetchPreviewRecords:Function]\n // @PURPOSE: Verify fetchPreviewRecords calls fetchApi with correct endpoint.\n it('fetchPreviewRecords calls fetchApi with correct endpoint', async () => {\n mockFetchApi.mockResolvedValue([\n { id: 'r1', source_sql: 'Hello', target_sql: 'Привет', status: 'APPROVED' }\n ]);\n\n const { fetchPreviewRecords } = await import('$lib/api/translate.js');\n const result = await fetchPreviewRecords('session-1');\n\n expect(mockFetchApi).toHaveBeenCalledWith('/translate/preview/session-1/records');\n expect(result).toHaveLength(1);\n expect(result[0].status).toBe('APPROVED');\n });\n // [/DEF:test_fetchPreviewRecords:Function]\n\n // [DEF:test_normalizeTranslateError:Function]\n // @PURPOSE: Verify API errors are normalized consistently.\n it('normalizes API errors for preview functions', async () => {\n const error = new Error('API unavailable');\n mockPostApi.mockRejectedValue(error);\n\n const { fetchPreview } = await import('$lib/api/translate.js');\n\n await expect(fetchPreview('job-123')).rejects.toThrow();\n });\n // [/DEF:test_normalizeTranslateError:Function]\n});\n// [/DEF:TranslateApiTests:Class]\n" }, @@ -119382,7 +124060,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Persistence is handled via LocalStorage.", "LAYER": "Infra", "PURPOSE": "Centralized internationalization management using Svelte stores.", @@ -119447,7 +124125,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119470,7 +124150,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -119519,7 +124200,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119566,6 +124249,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -119599,7 +124291,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI-State", "PURPOSE": "Global state management using Svelte stores.", "SEMANTICS": [ @@ -119619,20 +124311,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI-State' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI-State" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -119645,6 +124323,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -119679,7 +124358,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119721,7 +124402,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119763,7 +124446,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119805,7 +124490,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119847,7 +124534,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119889,7 +124578,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -119938,6 +124629,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -119975,6 +124675,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -119989,7 +124698,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Each test starts from default closed state.", "LAYER": "UI Tests", "PURPOSE": "Validate assistant chat store visibility and conversation binding transitions.", @@ -120018,20 +124727,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } } ], "anchor_syntax": "def", @@ -120046,7 +124741,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Open/close/toggle/conversation transitions are validated.", "PRE": "Store can be reset to baseline state in beforeEach hook.", "PURPOSE": "Group store unit scenarios for assistant panel behavior." @@ -120091,7 +124786,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Mock for $env/static/public SvelteKit module in vitest" }, @@ -120103,22 +124798,7 @@ "target_ref": "[$env/static/public]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:mock_env_public:Module]\n// @RELATION: DEPENDS_ON -> [$env/static/public]\n// @COMPLEXITY: 3\n// @PURPOSE: Mock for $env/static/public SvelteKit module in vitest\n// @LAYER: UI (Tests)\nexport const PUBLIC_WS_URL = 'ws://localhost:8000';\n// [/DEF:mock_env_public:Module]\n" }, @@ -120131,7 +124811,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI (Tests)", "PURPOSE": "Mock for $app/environment in tests" }, @@ -120144,20 +124824,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -120180,7 +124846,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Includes SvelteKit v1-only APIs (push, prefetchRoutes) as compatibility surface for legacy test consumers; keep until all imports migrate to SvelteKit v2-compatible mocks.", "PURPOSE": "Mock for $app/navigation in tests", "SEMANTICS": [ @@ -120214,9 +124880,9 @@ } }, { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -120244,27 +124910,12 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI Tests", "PURPOSE": "Mock for AppState in vitest route/component tests." }, "relations": [], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:state_mock:Module]\n// @COMPLEXITY: 2\n// @PURPOSE: Mock for AppState in vitest route/component tests.\n// @LAYER: UI Tests\n\nexport const page = {\n params: {},\n route: { id: \"test\" },\n url: new URL(\"http://localhost\"),\n status: 200,\n error: null,\n data: {},\n form: null,\n};\n\n// [/DEF:state_mock:Module]\n" }, @@ -120277,7 +124928,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "Mocks $app/stores which is a SvelteKit v1 module; SvelteKit v2 uses $app/state. Verify no active test files import this mock before removal.", "PURPOSE": "Mock for $app/stores in tests", "SEMANTICS": [ @@ -120311,9 +124962,9 @@ } }, { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -120341,7 +124992,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Global test setup with mocks for SvelteKit modules", "UX_STATE": "Idle -> Global test environment exposes stable mocked browser services." @@ -120368,10 +125019,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -120386,7 +125051,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Sidebar store transitions must be deterministic across desktop/mobile toggles.", "LAYER": "Domain (Tests)", "PURPOSE": "Unit tests for sidebar store", @@ -120415,20 +125080,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } } ], "anchor_syntax": "def", @@ -120619,7 +125270,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Unit tests for activity store" }, "relations": [ @@ -120636,17 +125287,7 @@ "target_ref": "[taskDrawer]" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:ActivityTest:Module]\n// @COMPLEXITY: 3\n// @PURPOSE: Unit tests for activity store\n// @RELATION: DEPENDS_ON -> [activity]\n// @RELATION: DEPENDS_ON -> [taskDrawer]\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\n\ndescribe('activity store', () => {\n beforeEach(async () => {\n vi.resetModules();\n });\n\n it('should have zero active count initially', async () => {\n const { activityStore } = await import('../activity.js');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(0);\n expect(state.recentTasks).toEqual([]);\n });\n\n it('should count RUNNING tasks as active', async () => {\n const { taskDrawerStore, updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add a running task\n updateResourceTask('dashboard-1', 'task-1', 'RUNNING');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(1);\n });\n\n it('should not count SUCCESS tasks as active', async () => {\n const { updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add a success task\n updateResourceTask('dashboard-1', 'task-1', 'SUCCESS');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(0);\n });\n\n it('should not count ERROR tasks as active', async () => {\n const { updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add an error task\n updateResourceTask('dashboard-1', 'task-1', 'ERROR');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(0);\n });\n\n it('should not count WAITING_INPUT as active', async () => {\n const { updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add a waiting input task - should NOT be counted as active per contract\n // Only RUNNING tasks count as active\n updateResourceTask('dashboard-1', 'task-1', 'WAITING_INPUT');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(0);\n });\n\n it('should track multiple running tasks', async () => {\n const { updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add multiple running tasks\n updateResourceTask('dashboard-1', 'task-1', 'RUNNING');\n updateResourceTask('dashboard-2', 'task-2', 'RUNNING');\n updateResourceTask('dataset-1', 'task-3', 'RUNNING');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.activeCount).toBe(3);\n });\n\n it('should return recent tasks', async () => {\n const { updateResourceTask } = await import('../taskDrawer.js');\n const { activityStore } = await import('../activity.js');\n \n // Add multiple tasks\n updateResourceTask('dashboard-1', 'task-1', 'RUNNING');\n updateResourceTask('dataset-1', 'task-2', 'SUCCESS');\n updateResourceTask('storage-1', 'task-3', 'ERROR');\n \n let state = null;\n const unsubscribe = activityStore.subscribe(s => { state = s; });\n unsubscribe();\n \n expect(state.recentTasks.length).toBeGreaterThan(0);\n expect(state.recentTasks[0]).toHaveProperty('taskId');\n expect(state.recentTasks[0]).toHaveProperty('resourceId');\n expect(state.recentTasks[0]).toHaveProperty('status');\n });\n});\n\n// [/DEF:ActivityTest:Module]\n" }, @@ -120659,7 +125300,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Unit tests for dataset review session store.", "SEMANTICS": [ @@ -120680,10 +125321,24 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -120698,7 +125353,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Unit tests for sidebar store" }, @@ -120723,7 +125378,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Store state transitions remain deterministic for open/close and task-status mapping.", "LAYER": "UI", "PURPOSE": "Unit tests for task drawer store", @@ -120765,7 +125420,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Track active task count for navbar indicator" }, @@ -120789,7 +125444,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -120809,7 +125466,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -120834,7 +125493,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -120869,7 +125530,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Control assistant chat panel visibility and active conversation binding." }, "relations": [ @@ -120892,7 +125553,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -120917,7 +125580,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -120975,6 +125640,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121012,6 +125686,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121049,6 +125732,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121086,6 +125778,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121123,6 +125824,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121160,6 +125870,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121197,6 +125916,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121234,6 +125962,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -121248,7 +125985,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SessionDetail | Partial | boolean | string | null] -> Output[DatasetReviewSessionStoreState]", "LAYER": "UI", "POST": "Store transitions keep session, loading, error, and dirty flags internally consistent after each helper call.", @@ -121278,7 +126015,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -121301,7 +126040,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -121321,7 +126061,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -121344,7 +126086,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -121367,7 +126110,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -121392,7 +126136,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -121415,7 +126161,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -121429,16 +126176,44 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Store'", + "detail": { + "actual_type": "Store", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Store' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Store" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Store'", + "detail": { + "actual_type": "Store", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Store" + } }, { "code": "tag_forbidden_by_complexity", @@ -121560,7 +126335,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI-State", "PURPOSE": "Global selected environment context for navigation and safety cues." }, @@ -121584,7 +126359,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -121604,7 +126381,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -121617,20 +126396,6 @@ "contract_type": "Store" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI-State' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI-State" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -121643,7 +126408,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -121677,6 +126444,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -121695,7 +126463,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PROPERTY": "{Date|null} lastUpdated - Last successful fetch timestamp", "PURPOSE": "Manage dashboard health summary state and failing counts for UI badges.", @@ -121722,7 +126490,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -121742,7 +126512,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -121773,7 +126545,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -121793,10 +126567,24 @@ "detail": null }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Store'", + "detail": { + "actual_type": "Store", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Store" + } }, { "code": "tag_forbidden_by_complexity", @@ -121848,7 +126636,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "isExpanded state is always synced with localStorage", "LAYER": "UI", "PURPOSE": "Manage sidebar visibility and navigation state", @@ -121874,7 +126662,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -121897,7 +126687,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -121917,7 +126710,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -121942,7 +126737,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -121956,10 +126753,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Store'", + "detail": { + "actual_type": "Store", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Store" + } }, { "code": "tag_forbidden_by_complexity", @@ -121982,6 +126793,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -122070,7 +126882,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Manage Task Drawer visibility and resource-to-task mapping" }, "relations": [ @@ -122093,7 +126905,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -122118,7 +126932,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -122152,6 +126968,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -122294,17 +127111,12 @@ "relations": [], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI-State' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI-State" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -122336,7 +127148,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -122392,6 +127206,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -122436,6 +127259,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -122450,7 +127282,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Supports accessible labels and keyboard navigation.", "LAYER": "Atom", "PURPOSE": "Standardized button component with variants and loading states.", @@ -122486,7 +127318,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -122499,20 +127333,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -122520,7 +127340,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -122533,12 +127356,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -122561,7 +127378,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Atom", "PURPOSE": "Standardized container with padding and elevation.", "SEMANTICS": [ @@ -122587,7 +127404,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -122600,20 +127419,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -122621,7 +127426,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -122634,12 +127442,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -122662,7 +127464,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Icon output remains aria-hidden because labels belong to the interactive parent.", "LAYER": "Atom", "PURPOSE": "Render the shared inline SVG icon set with consistent sizing and stroke props.", @@ -122691,7 +127493,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -122704,20 +127508,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -122725,7 +127515,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -122737,12 +127530,6 @@ "actual_complexity": 2, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -122757,7 +127544,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Consistent spacing and focus states.", "LAYER": "Atom", "PURPOSE": "Standardized text input component with label and error handling.", @@ -122793,7 +127580,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -122806,20 +127595,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -122827,7 +127602,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -122840,12 +127618,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -122868,7 +127640,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Atom", "PURPOSE": "Dropdown component to switch between supported languages.", "SEMANTICS": [ @@ -122900,7 +127672,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -122913,20 +127687,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -122934,7 +127694,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -122947,12 +127710,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -122975,7 +127732,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Atom", "PURPOSE": "Standardized page header with title and action area.", "SEMANTICS": [ @@ -123000,7 +127757,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -123013,20 +127772,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -123034,7 +127779,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -123047,12 +127795,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -123075,7 +127817,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Atom", "PURPOSE": "Standardized dropdown selection component.", "SEMANTICS": [ @@ -123102,7 +127844,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -123115,20 +127859,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -123136,7 +127866,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -123149,12 +127882,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -123177,7 +127904,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "All components exported here must follow Semantic Protocol.", "LAYER": "Atom", "PURPOSE": "Central export point for standardized UI components.", @@ -123200,17 +127927,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Atom' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Atom" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -123226,12 +127948,22 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "PURPOSE": "General utility functions (class merging)" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:Utils:Module]\n/**\n * @COMPLEXITY: 1\n * @PURPOSE: General utility functions (class merging)\n * @LAYER: Infra\n * \n * Merges class names into a single string.\n * @param {...(string | undefined | null | false)} inputs\n * @returns {string}\n */\nexport function cn(...inputs) {\n return inputs.filter(Boolean).join(\" \");\n}\n// [/DEF:Utils:Module]\n" }, @@ -123260,12 +127992,22 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "PURPOSE": "Debounce utility for limiting function execution rate" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:Debounce:Module]\n/**\n * @COMPLEXITY: 1\n * @PURPOSE: Debounce utility for limiting function execution rate\n * @LAYER: Infra\n * \n * Debounce utility function\n * Delays the execution of a function until a specified time has passed since the last call\n * \n * @param {Function} func - The function to debounce\n * @param {number} wait - The delay in milliseconds\n * @returns {Function} - The debounced function\n */\nexport function debounce(func, wait) {\n let timeout;\n return function executedFunction(...args) {\n const later = () => {\n clearTimeout(timeout);\n func(...args);\n };\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n}\n// [/DEF:Debounce:Module]\n" }, @@ -123294,7 +128036,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "EVENTS": "None", "LAYER": "UI", "PROPS": "None", @@ -123335,7 +128077,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -123361,7 +128105,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -123373,12 +128120,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -123416,6 +128157,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123460,6 +128210,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123474,7 +128233,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "EVENTS": "None", "INVARIANT": "Settings changes must be saved to the backend.", "LAYER": "UI", @@ -123530,7 +128289,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -123556,7 +128317,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -123569,12 +128333,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -123587,6 +128345,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -123628,6 +128387,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123665,6 +128433,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123702,6 +128479,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123746,6 +128532,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123790,6 +128585,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123834,6 +128638,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123871,6 +128684,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -123885,7 +128707,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Global error page displaying HTTP status and messages", "UX_STATE": "Error -> Displays error code and message with home link" @@ -123934,7 +128756,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -123954,7 +128778,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -123979,7 +128805,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -123993,10 +128821,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -124019,6 +128861,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -124037,7 +128880,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All pages except /login are wrapped in ProtectedRoute.", "LAYER": "UI (Layout)", "PURPOSE": "Root layout component that provides global UI structure (Sidebar, Navbar, Footer, TaskDrawer, Toasts).", @@ -124102,20 +128945,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Layout)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Layout)" - } } ], "anchor_syntax": "def", @@ -124130,7 +128959,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Login route bypasses shell; all other routes are wrapped by ProtectedRoute.", "LAYER": "UI", "PURPOSE": "Bind global layout shell and conditional login/full-app rendering.", @@ -124160,10 +128989,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -124178,12 +129021,22 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "PURPOSE": "Root layout configuration (SPA mode)" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:RootLayoutConfig:Module]\n/**\n * @COMPLEXITY: 1\n * @PURPOSE: Root layout configuration (SPA mode)\n * @LAYER: Infra\n */\nexport const ssr = false;\nexport const prerender = false;\n// [/DEF:RootLayoutConfig:Module]\n" }, @@ -124196,7 +129049,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Redirect target resolves to one of /dashboards, /datasets, /reports.", "LAYER": "UI", "PURPOSE": "Redirect to preferred start page from profile settings with safe dashboards fallback.", @@ -124242,7 +129095,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -124265,7 +129120,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -124285,7 +129143,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -124310,7 +129170,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -124345,16 +129207,44 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -124401,6 +129291,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -124429,7 +129328,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Shared semantic route-page registry for SvelteKit route surfaces in the frontend route layer." }, "relations": [ @@ -124458,7 +129357,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -124483,7 +129384,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -124518,7 +129421,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Route and page surfaces expose purpose, relation, and UX-state metadata for semantic auditing.", "PRE": "Route and page files keep matched anchors and preserve existing runtime behavior.", "PURPOSE": "Define the minimum page-level semantic metadata required for route and page surfaces without changing UI behavior.", @@ -124544,7 +129447,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -124567,7 +129472,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -124590,7 +129496,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -124615,7 +129522,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -124638,7 +129547,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -124673,7 +129583,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Track navigation-shell and redirect semantics that connect page routes to the shared application layout." }, "relations": [ @@ -124702,7 +129612,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -124727,7 +129639,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -124762,7 +129676,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Only accessible by users with Admin role.", "LAYER": "Domain", "PURPOSE": "UI for managing system roles and their permissions.", @@ -124804,7 +129718,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -124824,7 +129740,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -124836,12 +129755,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -124879,6 +129792,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -124916,6 +129838,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -124953,6 +129884,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -124990,6 +129930,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -125004,7 +129953,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Only accessible by users with \"admin:settings\" permission.", "LAYER": "Feature", "PURPOSE": "UI for configuring Active Directory Group to local Role mappings for ADFS SSO and logging settings.", @@ -125048,7 +129997,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -125061,20 +130012,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -125082,7 +130019,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -125094,12 +130034,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -125147,6 +130081,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -125216,6 +130159,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -125276,6 +130228,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -125304,7 +130265,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Admin settings page for LLM provider configuration.", "SEMANTICS": [ @@ -125332,7 +130293,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -125352,7 +130315,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -125364,12 +130330,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -125384,7 +130344,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Only accessible by users with \"admin:users\" permission.", "LAYER": "Feature", "PURPOSE": "UI for managing system users and their roles.", @@ -125438,7 +130398,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -125451,20 +130413,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Feature' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Feature" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -125472,7 +130420,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -125484,12 +130435,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -125542,6 +130487,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -125612,6 +130566,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -125643,7 +130606,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Always shows dashboards for the active environment from context store.", "LAYER": "UI", "POST": "The page renders dashboards for the active environment together with deterministic selection and Git status state.", @@ -125694,7 +130657,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -125717,7 +130682,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -125737,7 +130705,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -125760,7 +130730,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -125783,7 +130754,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -125808,7 +130780,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -125831,7 +130805,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -125866,28 +130841,84 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -125934,6 +130965,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -125971,6 +131011,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126008,6 +131057,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126045,6 +131103,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126082,6 +131149,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126122,10 +131198,33 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126163,6 +131262,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126200,6 +131308,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126237,6 +131354,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126274,6 +131400,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126311,6 +131446,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126348,6 +131492,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126385,6 +131538,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126422,6 +131584,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126459,6 +131630,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126496,6 +131676,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126533,6 +131722,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126570,6 +131768,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126607,6 +131814,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126644,6 +131860,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126681,6 +131906,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126718,6 +131952,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126755,6 +131998,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126792,6 +132044,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126832,10 +132093,33 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -126850,7 +132134,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Shows dashboard metadata, charts, and datasets for selected environment.", "LAYER": "UI", "POST": "Dashboard metadata, related assets, and task/Git panels remain synchronized for the selected dashboard context.", @@ -126899,7 +132183,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -126922,7 +132208,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -126942,7 +132231,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -126965,7 +132256,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -126988,7 +132280,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127013,7 +132306,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -127036,7 +132331,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127077,10 +132373,24 @@ "detail": null }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -127104,7 +132414,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Git Repository block (status, ahead/behind, changes count, push/pull/sync buttons, and diff preview)." }, @@ -127117,7 +132427,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127143,7 +132455,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Top title area, breadcrumb, git branch selector, and action buttons for dashboard detail." }, @@ -127156,7 +132468,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127182,7 +132496,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Block for overview, charts table, and datasets list." }, @@ -127195,7 +132509,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127221,7 +132537,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "UI", "PURPOSE": "Block for recent tasks table." }, @@ -127234,7 +132550,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127260,7 +132578,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Verifies temporary show-all override and restore-on-return behavior for profile-default dashboard filtering.", "SEMANTICS": [ @@ -127281,20 +132599,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - }, { "code": "unknown_tag", "tag": "TYPE", @@ -127314,7 +132618,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI/Page", "PURPOSE": "Main page for the Dashboard Health Center.", "UX_REACTIVITY": "State: $state, Derived: $derived.", @@ -127342,7 +132646,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127354,32 +132660,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI/Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI/Page" - } - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -127419,6 +132699,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -127464,6 +132753,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -127503,6 +132801,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -127525,7 +132832,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI Tests", "PURPOSE": "Lock dashboard health page contract for slug navigation and report deletion.", "SEMANTICS": [ @@ -127543,22 +132850,7 @@ "target_ref": "[HealthCenterPage]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:HealthPageIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: health-page, integration-test, slug-link, delete-flow\n// @PURPOSE: Lock dashboard health page contract for slug navigation and report deletion.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [HealthCenterPage]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst PAGE_PATH = path.resolve(\n process.cwd(),\n 'src/routes/dashboards/health/+page.svelte',\n);\n\ndescribe('Dashboard health page contract', () => {\n it('renders slug-first dashboard link bound to environment route context', () => {\n const source = fs.readFileSync(PAGE_PATH, 'utf-8');\n\n expect(source).toContain(\"{$t.health?.table_dashboard}\");\n expect(source).toContain(\"item.dashboard_slug || item.dashboard_id\");\n expect(source).toContain(\"href={`/dashboards/${encodeURIComponent(String(item.dashboard_slug || item.dashboard_id))}?env_id=${encodeURIComponent(item.environment_id)}`}\");\n });\n\n it('keeps explicit delete report flow with confirm and DELETE request', () => {\n const source = fs.readFileSync(PAGE_PATH, 'utf-8');\n\n expect(source).toContain('async function handleDeleteReport(item)');\n expect(source).toContain(\"requestApi(`/health/summary/${item.record_id}`, 'DELETE')\");\n expect(source).toContain(\"$t.health?.delete_confirm.replace('{slug}', item.dashboard_slug || item.dashboard_id)\");\n expect(source).toContain(\"$t.common?.delete\");\n });\n\n it('reuses global health store refresh for all-environment summary loads', () => {\n const source = fs.readFileSync(PAGE_PATH, 'utf-8');\n\n expect(source).toContain(\"import { healthStore } from '$lib/stores/health.js';\");\n expect(source).toContain('? getHealthSummary(selectedEnvId)');\n expect(source).toContain(': healthStore.refresh();');\n });\n});\n// [/DEF:HealthPageIntegrationTest:Module]\n" }, @@ -127571,7 +132863,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Always shows datasets for the active environment from context store.", "LAYER": "UI", "POST": "Dataset list, selection state, and bulk-action affordances remain aligned with the active environment.", @@ -127615,7 +132907,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -127638,7 +132932,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -127658,7 +132955,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127681,7 +132980,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127704,7 +133004,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127729,7 +133030,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -127752,7 +133055,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127787,22 +133091,64 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -127826,7 +133172,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Expects dataset detail payload with dataset metadata, columns, SQL context, and linked dashboards for the selected dataset id.", "INVARIANT": "Always shows dataset details when loaded", "LAYER": "UI", @@ -127859,7 +133205,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -127882,7 +133230,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127905,7 +133254,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -127925,7 +133277,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -127948,7 +133302,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127971,7 +133326,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -127996,7 +133352,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -128019,7 +133377,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -128054,22 +133413,64 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -128093,7 +133494,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Entry route for Dataset Review Workspace that allows starting a new resumable review session before navigating to a specific session id route.", "SEMANTICS": [ @@ -128143,7 +133544,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -128163,7 +133566,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -128188,7 +133593,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -128208,7 +133615,10 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -128222,16 +133632,44 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -128254,6 +133692,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -128271,6 +133710,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -128288,6 +133728,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128305,6 +133746,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128323,7 +133765,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[route:id|StartSessionRequest] -> Output[SessionDetail|null]", "INVARIANT": "Navigation away from dirty session state must require explicit confirmation.", "LAYER": "UI", @@ -128378,7 +133820,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -128398,7 +133842,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -128411,30 +133858,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -128447,6 +133870,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128464,6 +133888,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128481,6 +133906,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128498,6 +133924,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -128516,7 +133943,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.", "SEMANTICS": [ @@ -128539,16 +133966,45 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -128563,7 +134019,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Page", "PURPOSE": "Dashboard management page for Git integration.", "SEMANTICS": [ @@ -128614,7 +134070,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -128627,20 +134085,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -128648,7 +134092,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -128660,12 +134107,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -128680,7 +134121,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Shows both local login form and ADFS SSO button.", "LAYER": "UI", "PURPOSE": "Provides the user interface for local and ADFS authentication.", @@ -128723,7 +134164,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -128743,7 +134186,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -128755,12 +134201,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -128798,6 +134238,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -128815,7 +134264,17 @@ "PURPOSE": "Redirects the user to the ADFS login endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:handleADFSLogin:Function]\n /**\n * @purpose Redirects the user to the ADFS login endpoint.\n */\n function handleADFSLogin() {\n window.location.href = '/api/auth/login/adfs';\n }\n // [/DEF:handleADFSLogin:Function]\n" }, @@ -128828,7 +134287,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Migration start is blocked unless source and target environments are selected, distinct, and at least one dashboard is selected.", "LAYER": "UI", "PURPOSE": "Main migration dashboard page for environment selection, dry-run validation, and migration execution.", @@ -128986,40 +134445,105 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -129033,6 +134557,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -129050,6 +134575,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -129067,6 +134593,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -129084,6 +134611,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129101,6 +134629,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129118,6 +134647,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129135,6 +134665,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129152,6 +134683,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129169,6 +134701,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129186,6 +134719,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -129239,6 +134773,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -129278,7 +134821,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -129301,7 +134845,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -129315,10 +134860,33 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } } ], "anchor_syntax": "def", @@ -129356,6 +134924,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129393,6 +134970,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129430,6 +135016,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129461,7 +135056,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -129484,7 +135080,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -129498,10 +135095,33 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } } ], "anchor_syntax": "def", @@ -129539,6 +135159,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129579,6 +135208,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -129589,10 +135227,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129635,22 +135287,73 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -129673,22 +135376,73 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } } ], "anchor_syntax": "def", @@ -129704,17 +135458,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n \n \n" }, @@ -129728,17 +135472,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n \n \n" }, @@ -129752,17 +135486,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n {#if $selectedTask}\n
\n \n
\n \n
\n
\n {:else}\n \n" }, @@ -129776,17 +135500,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n
\n \n \n
\n \n" }, @@ -129800,17 +135514,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n
\n

\n {$t.migration?.select_dashboards_title }\n

\n\n {#if sourceEnvId}\n \n {:else}\n

\n {$t.dashboard?.select_source ||\n \"Select a source environment to view dashboards.\"}\n

\n {/if}\n
\n \n" }, @@ -129824,17 +135528,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n
\n
\n \n \n
\n\n
\n {\n console.info(\"[MigrationOptionsSection][REASON] Database replacement toggled\", { replaceDb });\n if (replaceDb && sourceDatabases.length === 0) fetchDatabases();\n }}\n class=\"h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded\"\n />\n \n
\n
\n \n" }, @@ -129848,17 +135542,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " \n {#if dryRunResult}\n
\n

Pre-flight Diff

\n
\n
\n
Dashboards
\n
create: {dryRunResult.summary.dashboards.create}
\n
update: {dryRunResult.summary.dashboards.update}
\n
delete: {dryRunResult.summary.dashboards.delete}
\n
\n
\n
Charts
\n
create: {dryRunResult.summary.charts.create}
\n
update: {dryRunResult.summary.charts.update}
\n
delete: {dryRunResult.summary.charts.delete}
\n
\n
\n
Datasets
\n
create: {dryRunResult.summary.datasets.create}
\n
update: {dryRunResult.summary.datasets.update}
\n
delete: {dryRunResult.summary.datasets.delete}
\n
\n
\n\n
\n
Risk
\n
\n score: {dryRunResult.risk.score}, level: {dryRunResult.risk.level}\n
\n
\n issues: {dryRunResult.risk.items.length}\n
\n
\n\n
\n Diff JSON\n
{JSON.stringify(dryRunResult, null, 2)}
\n
\n
\n {/if}\n \n" }, @@ -129877,10 +135561,33 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } } ], "anchor_syntax": "def", @@ -129895,7 +135602,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "CRITICAL_TRACE": "Frontend scope; Python belief_scope/logger are not applicable in Svelte runtime. Reflective tracing, when added, must use console prefix [ID][REFLECT].", "DATA_CONTRACT": "Input(Event: update{sourceUuid,targetUuid}) -> Model(MappingPayload); Output(UIState{environments,databases,mappings,suggestions,status})", "INVARIANT": "Persisted mapping state in backend remains the source of truth for rendered mapping pairs.", @@ -130023,40 +135730,123 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -130070,6 +135860,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -130087,6 +135878,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -130104,6 +135896,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -130121,6 +135914,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -130138,6 +135932,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -130179,6 +135974,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -130200,6 +136004,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -130217,6 +136022,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -130234,6 +136040,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -130253,17 +136060,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " // [DEF:Imports:Block]\n import { onMount } from 'svelte';\n import { api } from '../../../lib/api.js';\n import EnvSelector from '../../../components/EnvSelector.svelte';\n import MappingTable from '../../../components/MappingTable.svelte';\n import { t } from '$lib/i18n';\n import { Button, PageHeader } from '$lib/ui';\n // [/DEF:Imports:Block]\n" }, @@ -130292,7 +136089,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -130353,6 +136152,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -130410,6 +136218,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -130467,6 +136284,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -130490,17 +136316,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "\n
\n \n\n {#if loading}\n

{$t.migration?.loading_envs }

\n {:else}\n
\n { sourceDatabases = []; mappings = []; suggestions = []; }}\n />\n { targetDatabases = []; mappings = []; suggestions = []; }}\n />\n
\n\n
\n \n {$t.migration?.fetch_dbs }\n \n
\n\n {#if error}\n
\n {error}\n
\n {/if}\n\n {#if success}\n
\n {success}\n
\n {/if}\n\n {#if sourceDatabases.length > 0}\n \n {:else if !fetchingDbs && sourceEnvId && targetEnvId}\n

{$t.migration?.mapping_hint }

\n {/if}\n {/if}\n
\n\n" }, @@ -130513,7 +136329,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[{preference, security}, {superset_username, show_only_my_dashboards, show_only_slug_dashboards, git_username, git_email, git_personal_access_token, start_page, auto_open_task_drawer, dashboards_table_density, telegram_id, email_address, notify_on_fail}, {items, status, warning}] -> UIState[{selectedEnvironmentId, supersetUsername, validationErrors, lookupItems, securitySummary}]", "INVARIANT": "Save operations mutate only current user's preference payload via profile API.", "LAYER": "UI", @@ -130560,7 +136376,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -130572,30 +136390,6 @@ "actual_complexity": 5, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -130610,7 +136404,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "lookupFailedManualFallback.warning is a hardcoded English string; drift risk if i18n key profile.lookup_error changes without updating this fixture.", "PURPOSE": "Shared deterministic fixture inputs for profile page integration tests.", "SEMANTICS": [ @@ -130650,9 +136444,9 @@ } }, { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -130680,7 +136474,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Verifies profile binding happy path and degraded lookup manual fallback save flow.", "SEMANTICS": [ @@ -130702,20 +136496,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - }, { "code": "unknown_tag", "tag": "TYPE", @@ -130735,7 +136515,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Verifies profile settings preload, cancel without persistence, and saved-state reload behavior.", "SEMANTICS": [ @@ -130757,20 +136537,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - }, { "code": "unknown_tag", "tag": "TYPE", @@ -130790,7 +136556,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "List state remains deterministic for active filter set.", "LAYER": "UI", "POST": "List filters, selected report, and report detail remain synchronized for the active query.", @@ -130847,7 +136613,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -130867,7 +136635,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -130900,24 +136671,6 @@ "actual_complexity": 4, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -130932,7 +136685,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "LAYER": "UI", "PURPOSE": "Full report page for LLM dashboard validation task execution.", "TEST_CONTRACT": "Page_LLMReport ->", @@ -130963,7 +136716,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131003,24 +136758,6 @@ "message": "@TEST_DATA is not defined in axiom_config.yaml tags", "detail": null }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -131079,7 +136816,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI Tests", "PURPOSE": "Protect the LLM report page from self-triggering screenshot load effects.", "SEMANTICS": [ @@ -131097,22 +136834,7 @@ "target_ref": "frontend/src/routes/reports/llm/[taskId]/+page.svelte" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI Tests" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:ReportPageContractTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: llm-report, svelte-effect, screenshot-loading, regression-test\n// @PURPOSE: Protect the LLM report page from self-triggering screenshot load effects.\n// @LAYER: UI Tests\n// @RELATION: VERIFIES -> frontend/src/routes/reports/llm/[taskId]/+page.svelte\n\nimport { describe, it, expect } from \"vitest\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst PAGE_PATH = path.resolve(\n process.cwd(),\n \"src/routes/reports/llm/[taskId]/+page.svelte\",\n);\n\n// [DEF:llm_report_screenshot_effect_contract:Function]\n// @RELATION: USES -> App\n// @COMPLEXITY: 3\n// @PURPOSE: Ensure screenshot loading stays untracked from blob-url mutation state.\n// @PRE: Report page source exists.\n// @POST: Contract fails if screenshot loading effect can subscribe to screenshotBlobUrls updates.\ndescribe(\"LLM report screenshot effect contract\", () => {\n it(\"uses untrack around screenshot blob loading side effect\", () => {\n const source = fs.readFileSync(PAGE_PATH, \"utf-8\");\n\n expect(source).toContain('import { onDestroy, onMount, untrack } from \"svelte\";');\n expect(source).toContain(\"untrack(() => {\");\n expect(source).toContain(\"void loadScreenshotBlobUrls(paths);\");\n });\n});\n// [/DEF:llm_report_screenshot_effect_contract:Function]\n// [/DEF:ReportPageContractTest:Module]\n" }, @@ -131125,7 +136847,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Contract fails if screenshot loading effect can subscribe to screenshotBlobUrls updates.", "PRE": "Report page source exists.", "PURPOSE": "Ensure screenshot loading stays untracked from blob-url mutation state." @@ -131169,6 +136891,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -131187,7 +136910,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Expects consolidated settings payload including environments, llm provider configuration, and tab-specific settings collections.", "INVARIANT": "Always shows tabbed interface with all settings categories", "LAYER": "UI", @@ -131222,7 +136945,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -131245,7 +136970,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -131268,7 +136994,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -131288,7 +137017,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131311,7 +137042,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -131334,7 +137066,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -131359,7 +137092,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -131382,7 +137117,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -131444,22 +137180,64 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -131497,7 +137275,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "Exposes environment CRUD interface.", "PRE": "Settings page has loaded environments data.", @@ -131522,7 +137300,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131534,18 +137314,6 @@ "actual_complexity": 4, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -131560,7 +137328,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Test UX states and transitions", "SEMANTICS": [ @@ -131578,22 +137346,7 @@ "target_ref": "[SettingsPage]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:SettingsPageUxTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: settings, page, ux-tests, save-flow\n// @RELATION: DEPENDS_ON -> [SettingsPage]\n// @PURPOSE: Test UX states and transitions\n// @LAYER: UI (Tests)\n\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, fireEvent, waitFor } from '@testing-library/svelte';\nimport SettingsPage from '../+page.svelte';\nimport { api } from '$lib/api';\nimport { addToast } from '$lib/toasts';\n\nvi.mock('$lib/api', () => ({\n api: {\n getConsolidatedSettings: vi.fn(),\n requestApi: vi.fn(),\n putApi: vi.fn(),\n postApi: vi.fn(),\n updateConsolidatedSettings: vi.fn()\n }\n}));\n\nvi.mock('$lib/toasts', () => ({\n addToast: vi.fn()\n}));\n\nvi.mock('$lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n settings: {\n title: 'Settings',\n migration_sync: 'Migration Sync',\n migration_sync_title: 'Cross-Environment ID Synchronization',\n migration_sync_description: 'Sync IDs across environments',\n sync_schedule: 'Sync Schedule',\n sync_now: 'Sync Now',\n syncing: 'Syncing...',\n saving: 'Saving...',\n environments: 'Environments',\n logging: 'Logging',\n logging_description: 'Configure logging',\n log_level: 'Log Level',\n task_log_level: 'Task Log Level',\n enable_belief_state: 'Enable Belief State',\n connections: 'Connections',\n llm: 'LLM',\n storage: 'Storage',\n save_success: 'Settings saved',\n save_failed: 'Failed',\n save_logging: 'Save Logging Config',\n env_description: 'Configure environments',\n load_failed: 'Failed to load settings',\n migration_sync_failed: 'Sync failed'\n },\n common: { refresh: 'Refresh', retry: 'Retry', save: 'Save' },\n tasks: { cron_label: 'Cron Expression', cron_hint: 'Standard cron format' },\n nav: { settings_git: 'Git' },\n connections: { name: 'Name', user: 'User' }\n });\n return () => { };\n }\n },\n _: vi.fn((key) => key)\n}));\n\n// Mock child components\nvi.mock('../../components/llm/ProviderConfig.svelte', () => ({\n default: class ProviderConfigMock {\n constructor(options) {\n options.target.innerHTML = `
`;\n }\n }\n}));\n\ndescribe('SettingsPage UX Contracts', () => {\n const mockSettings = {\n environments: [],\n logging: { level: 'INFO', task_log_level: 'INFO', enable_belief_state: false },\n connections: [],\n llm: {}\n };\n\n const mockMigrationSettings = {\n cron: \"0 2 * * *\"\n };\n\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n // @UX_STATE: Loading -> Shows skeleton loader\n // @UX_STATE: Loaded -> Shows tabbed settings interface\n it('should transition from Loading to Loaded state', async () => {\n // Delay resolution to capture loading state\n let resolveSettings;\n api.getConsolidatedSettings.mockImplementation(() => new Promise(resolve => {\n resolveSettings = resolve;\n }));\n\n api.requestApi.mockImplementation((url) => {\n if (url === '/migration/settings') return Promise.resolve(mockMigrationSettings);\n if (url.includes('/migration/mappings-data')) return Promise.resolve({ items: [], total: 0 });\n return Promise.resolve({});\n });\n\n render(SettingsPage);\n\n // Assert Loading skeleton is present (by checking for the pulse class)\n // Note: checking for classes used in skeleton\n const skeletonElements = document.querySelectorAll('.animate-pulse');\n expect(skeletonElements.length).toBeGreaterThan(0);\n\n // Resolve the API call\n resolveSettings(mockSettings);\n\n // Assert Loaded state - use getAllByText because 'Environments' may appear\n // both as tab text and section heading on the default tab\n await waitFor(() => {\n expect(screen.getByText('Settings')).toBeTruthy();\n expect(screen.getAllByText('Environments').length).toBeGreaterThan(0);\n });\n });\n\n // @UX_STATE: Error -> Shows error banner with retry button\n it('should show error banner when loading fails', async () => {\n api.getConsolidatedSettings.mockRejectedValue(new Error('Network Error'));\n api.requestApi.mockResolvedValue(mockMigrationSettings);\n\n render(SettingsPage);\n\n await waitFor(() => {\n expect(screen.getByText('Network Error')).toBeTruthy();\n expect(screen.getByText('Retry')).toBeTruthy();\n });\n });\n\n // @UX_RECOVERY: Refresh button reloads settings data\n it('should reload settings data when retry button is clicked', async () => {\n let callCount = 0;\n api.getConsolidatedSettings.mockImplementation(async () => {\n callCount++;\n if (callCount === 1) throw new Error('First call failed');\n return mockSettings;\n });\n\n api.requestApi.mockImplementation((url) => {\n if (url === '/migration/settings') return Promise.resolve(mockMigrationSettings);\n if (url.includes('/migration/mappings-data')) return Promise.resolve({ items: [], total: 0 });\n return Promise.resolve({});\n });\n\n render(SettingsPage);\n\n // Wait for error state\n await waitFor(() => {\n expect(screen.getByText('First call failed')).toBeTruthy();\n });\n\n // Click retry\n const retryBtn = screen.getByText('Retry');\n await fireEvent.click(retryBtn);\n\n // Verify recovery (Loaded state) - use getAllByText because 'Environments'\n // appears both as tab text and section heading\n await waitFor(() => {\n expect(screen.queryByText('First call failed')).toBeNull();\n expect(screen.getAllByText('Environments').length).toBeGreaterThan(0);\n });\n // We expect it to have been called twice (1. initial mount, 2. retry click)\n expect(api.getConsolidatedSettings).toHaveBeenCalledTimes(2);\n });\n\n // @UX_FEEDBACK: Toast notifications on save success/failure\n it('should show success toast when settings are saved', async () => {\n api.getConsolidatedSettings.mockResolvedValue(mockSettings);\n api.requestApi.mockResolvedValue(mockMigrationSettings);\n api.updateConsolidatedSettings.mockResolvedValue({});\n\n render(SettingsPage);\n await waitFor(() => expect(screen.getByText('Settings')).toBeTruthy());\n\n // Navigate to Logging tab where the Save button is\n // 'Logging' appears in both the tab and section heading; use getAllByText\n await fireEvent.click(screen.getAllByText('Logging')[0]);\n\n const saveBtn = screen.getByText('Save Logging Config');\n await fireEvent.click(saveBtn);\n\n await waitFor(() => {\n expect(addToast).toHaveBeenCalledWith('Settings saved', 'success');\n });\n });\n\n it('should show error toast when settings save fails', async () => {\n api.getConsolidatedSettings.mockResolvedValue(mockSettings);\n api.requestApi.mockResolvedValue(mockMigrationSettings);\n api.updateConsolidatedSettings.mockRejectedValue(new Error('Save Error'));\n\n render(SettingsPage);\n await waitFor(() => expect(screen.getByText('Settings')).toBeTruthy());\n\n // Navigate to Logging tab where the Save button is\n // 'Logging' appears in both the tab and section heading; use getAllByText\n await fireEvent.click(screen.getAllByText('Logging')[0]);\n\n const saveBtn = screen.getByText('Save Logging Config');\n await fireEvent.click(saveBtn);\n\n await waitFor(() => {\n expect(addToast).toHaveBeenCalledWith('Failed', 'error');\n });\n });\n});\n\n// [/DEF:SettingsPageUxTest:Module]\n" }, @@ -131606,7 +137359,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI/Page", "PURPOSE": "Settings page for managing validation policies.", "UX_REATIVITY": "State: $state, Derived: $derived.", @@ -131628,7 +137381,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131641,32 +137396,12 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI/Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI/Page" - } - }, { "code": "unknown_tag", "tag": "UX_REATIVITY", "message": "@UX_REATIVITY is not defined in axiom_config.yaml tags", "detail": null }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -131679,6 +137414,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -131697,7 +137433,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Page for managing database connection configurations.", "SEMANTICS": [ @@ -131747,7 +137483,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131767,7 +137505,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -131779,12 +137520,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -131822,6 +137557,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -131836,7 +137580,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All configurations must be validated via connection test.", "LAYER": "Page", "PURPOSE": "Manage Git server configurations for dashboard versioning.", @@ -131897,7 +137641,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -131910,20 +137656,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -131931,7 +137663,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -131944,12 +137679,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -131962,6 +137691,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -131979,6 +137709,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -132020,6 +137751,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132057,6 +137797,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132094,6 +137843,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132128,6 +137886,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132145,7 +137912,17 @@ "PURPOSE": "Resets the configuration form." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " // [DEF:resetForm:Function]\n /**\n * @purpose Resets the configuration form.\n */\n function resetForm() {\n isEditing = false;\n editingConfigId = null;\n newConfig = {\n name: \"\",\n provider: \"GITHUB\",\n url: \"https://github.com\",\n pat: \"\",\n default_repository: \"\",\n default_branch: \"main\",\n };\n }\n // [/DEF:resetForm:Function]\n" }, @@ -132181,6 +137958,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132218,6 +138004,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132255,6 +138050,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132269,7 +138073,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (Tests)", "PURPOSE": "Test UX states and transitions for the Git Settings page", "SEMANTICS": [ @@ -132289,22 +138093,7 @@ "target_ref": "[GitSettingsPage]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "// [DEF:GitSettingsPageUxTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: settings, git, page, ux-tests, form, toast\n// @RELATION: DEPENDS_ON -> [GitSettingsPage]\n// @PURPOSE: Test UX states and transitions for the Git Settings page\n// @LAYER: UI (Tests)\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, fireEvent, waitFor } from '@testing-library/svelte';\nimport GitSettingsPage from '../+page.svelte';\nimport { gitService } from '../../../../services/gitService';\nimport { addToast } from '$lib/toasts';\n\nvi.mock('../../../../services/gitService', () => ({\n gitService: {\n getConfigs: vi.fn(),\n createConfig: vi.fn(),\n updateConfig: vi.fn(),\n deleteConfig: vi.fn(),\n testConnection: vi.fn(),\n listGiteaRepositories: vi.fn(),\n createGiteaRepository: vi.fn(),\n deleteGiteaRepository: vi.fn()\n }\n}));\n\nvi.mock('$lib/toasts', () => ({\n addToast: vi.fn()\n}));\n\nvi.mock('$lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n settings: {\n configured_servers: 'Configured Servers',\n add_git_server: 'Add Git Server',\n edit_git_server: 'Edit Git Server',\n display_name: 'Display Name',\n server_url: 'Server URL',\n personal_access_token: 'Personal Access Token',\n default_repository_optional: 'Default Repository (Optional)',\n default_branch: 'Default Branch',\n save_configuration: 'Save Configuration',\n update_configuration: 'Update Configuration',\n connection_success: 'Connection successful',\n connection_failed_short: 'Connection failed',\n git_config_saved: 'Git configuration saved',\n git_config_updated: 'Git configuration updated',\n git_delete_confirm: 'Are you sure?',\n git_config_deleted: 'Git configuration deleted'\n },\n llm: { type: 'Type', test: 'Test' },\n nav: { settings_git: 'Git Settings' },\n git: { no_servers_configured: 'No servers configured' },\n common: { edit: 'Edit', delete: 'Delete', cancel: 'Cancel' }\n });\n return () => { };\n }\n },\n _: vi.fn((key) => key)\n}));\n\n// Mock window.scrollTo\nObject.defineProperty(window, 'scrollTo', { value: vi.fn(), writable: true });\n\ndescribe('GitSettingsPage UX Contracts', () => {\n const mockConfigs = [\n {\n id: 'conf-1',\n name: 'Dev GitLab',\n provider: 'GITLAB',\n url: 'https://gitlab.com',\n pat: '********',\n status: 'CONNECTED'\n }\n ];\n\n beforeEach(() => {\n vi.clearAllMocks();\n // Default confirm to always accept for tests\n global.confirm = vi.fn(() => true);\n });\n\n // @UX_STATE: Initial Load\n it('should display configured servers on mount', async () => {\n gitService.getConfigs.mockResolvedValue(mockConfigs);\n render(GitSettingsPage);\n\n await waitFor(() => {\n expect(screen.getByText('Dev GitLab')).toBeTruthy();\n expect(screen.getByText('GITLAB')).toBeTruthy();\n expect(screen.getByText('https://gitlab.com')).toBeTruthy();\n });\n });\n\n it('should show empty state when no servers configured', async () => {\n gitService.getConfigs.mockResolvedValue([]);\n render(GitSettingsPage);\n\n await waitFor(() => {\n expect(screen.getByText('No servers configured')).toBeTruthy();\n });\n });\n\n // @UX_FEEDBACK: Connection testing feedback\n it('should test connection successfully and show success toast', async () => {\n gitService.getConfigs.mockResolvedValue([]);\n gitService.testConnection.mockResolvedValue({ status: 'success' });\n\n render(GitSettingsPage);\n await waitFor(() => expect(screen.getByText('Add Git Server')).toBeTruthy());\n\n // Fill form\n await fireEvent.input(screen.getByLabelText('Display Name'), { target: { value: 'New Gitea' } });\n await fireEvent.input(screen.getByLabelText('Server URL'), { target: { value: 'https://gitea.local' } });\n await fireEvent.input(screen.getByLabelText('Personal Access Token'), { target: { value: 'token123' } });\n\n // Click Test\n await fireEvent.click(screen.getByText('Test'));\n\n await waitFor(() => {\n expect(gitService.testConnection).toHaveBeenCalledWith(expect.objectContaining({\n name: 'New Gitea',\n url: 'https://gitea.local',\n pat: 'token123'\n }));\n expect(addToast).toHaveBeenCalledWith('Connection successful', 'success');\n });\n });\n\n // @UX_FEEDBACK: Save configuration\n it('should save configuration and show success toast', async () => {\n gitService.getConfigs.mockResolvedValue([]);\n gitService.createConfig.mockResolvedValue({\n id: 'conf-2',\n name: 'New Server',\n provider: 'GITHUB',\n url: 'https://github.com',\n pat: '********',\n status: 'CONNECTED'\n });\n\n render(GitSettingsPage);\n await waitFor(() => expect(screen.getByText('Add Git Server')).toBeTruthy());\n\n await fireEvent.input(screen.getByLabelText('Display Name'), { target: { value: 'New Server' } });\n await fireEvent.click(screen.getByText('Save Configuration'));\n\n await waitFor(() => {\n expect(gitService.createConfig).toHaveBeenCalled();\n expect(addToast).toHaveBeenCalledWith('Git configuration saved', 'success');\n // Check that it's added to the list\n expect(screen.getByText('New Server')).toBeTruthy();\n });\n });\n\n // @UX_STATE: Delete configuration\n it('should delete configuration upon confirmation', async () => {\n gitService.getConfigs.mockResolvedValue([...mockConfigs]);\n gitService.deleteConfig.mockResolvedValue({ status: 'success' });\n\n render(GitSettingsPage);\n await waitFor(() => expect(screen.getByText('Dev GitLab')).toBeTruthy());\n\n // Click delete button\n const deleteButton = screen.getByTitle('Delete');\n await fireEvent.click(deleteButton);\n\n await waitFor(() => {\n expect(global.confirm).toHaveBeenCalledWith('Are you sure?');\n expect(gitService.deleteConfig).toHaveBeenCalledWith('conf-1');\n expect(addToast).toHaveBeenCalledWith('Git configuration deleted', 'success');\n expect(screen.queryByText('Dev GitLab')).toBeNull(); // Should be removed from DOM\n });\n });\n\n // @UX_STATE: Editing form state\n it('should load config into form when edit is clicked', async () => {\n gitService.getConfigs.mockResolvedValue([...mockConfigs]);\n\n render(GitSettingsPage);\n await waitFor(() => expect(screen.getByText('Dev GitLab')).toBeTruthy());\n\n // Click edit button\n const editButton = screen.getByTitle('Edit');\n await fireEvent.click(editButton);\n\n await waitFor(() => {\n expect(screen.getByText('Edit Git Server')).toBeTruthy();\n const nameInput = screen.getByLabelText('Display Name');\n expect(nameInput.value).toBe('Dev GitLab');\n });\n\n // Save changes\n gitService.updateConfig.mockResolvedValue({\n ...mockConfigs[0],\n name: 'Updated GitLab'\n });\n\n await fireEvent.input(screen.getByLabelText('Display Name'), { target: { value: 'Updated GitLab' } });\n await fireEvent.click(screen.getByText('Update Configuration'));\n\n await waitFor(() => {\n expect(gitService.updateConfig).toHaveBeenCalledWith('conf-1', expect.objectContaining({\n name: 'Updated GitLab'\n }));\n expect(screen.getByText('Updated GitLab')).toBeTruthy();\n });\n });\n});\n// [/DEF:GitSettingsPageUxTest:Module]\n" }, @@ -132317,7 +138106,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Manage global notification provider configurations (SMTP, Telegram, Slack).", "UX_STATE": "Loading -> Default" @@ -132338,7 +138127,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -132351,12 +138142,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -132369,6 +138154,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -132527,7 +138313,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Always redirects to /storage/backups.", "LAYER": "Page", "PURPOSE": "Redirect to the backups page as the default storage view.", @@ -132565,7 +138351,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -132588,7 +138376,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -132608,7 +138399,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -132621,20 +138414,6 @@ "contract_type": "Page" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -132647,7 +138426,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -132661,10 +138442,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -132688,7 +138483,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Always redirects to /tools/storage.", "LAYER": "Page", "PURPOSE": "Temporary switch to legacy storage browser for backup UX validation.", @@ -132737,7 +138532,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -132760,7 +138557,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -132780,7 +138580,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -132793,20 +138595,6 @@ "contract_type": "Page" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -132819,7 +138607,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -132839,7 +138629,10 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -132853,10 +138646,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -132879,6 +138686,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "REDIRECTS_TO" @@ -132904,22 +138712,64 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Page" + } } ], "anchor_syntax": "def", @@ -132957,6 +138807,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -132994,6 +138853,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133031,6 +138899,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133045,7 +138922,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Page", "PURPOSE": "Entry point for the Backup Management interface.", "SEMANTICS": [ @@ -133089,7 +138966,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133102,20 +138981,6 @@ "contract_type": "Component" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Page' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -133123,7 +138988,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133136,12 +139004,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -133154,6 +139016,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -133172,7 +139035,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Page for system diagnostics and debugging.", "SEMANTICS": [ @@ -133222,7 +139085,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133242,7 +139107,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133254,12 +139122,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -133274,7 +139136,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Page for the dataset column mapper tool.", "SEMANTICS": [ @@ -133324,7 +139186,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133344,7 +139208,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133356,12 +139223,6 @@ "actual_complexity": 3, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -133376,7 +139237,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Always displays a unified storage view without category tabs.", "LAYER": "UI", "PURPOSE": "Main page for unified file storage management.", @@ -133424,7 +139285,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133444,7 +139307,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133457,12 +139323,6 @@ "contract_type": "Component" } }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -133475,6 +139335,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -133492,6 +139353,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -133533,6 +139395,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133570,6 +139441,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133614,6 +139494,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133658,6 +139547,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133695,6 +139593,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133732,6 +139639,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -133746,7 +139662,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "Dictionary list is rendered. Create, edit, delete actions are available.", "PRE": "Translate API is reachable before list hydration.", @@ -133789,7 +139705,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133809,7 +139727,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133821,18 +139742,6 @@ "actual_complexity": 4, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -133847,7 +139756,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "POST": "Dictionary entries are displayed, editable, and persistable.", "PRE": "Dictionary ID is provided in route params and API is reachable.", @@ -133899,7 +139808,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -133919,7 +139830,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -133931,18 +139845,6 @@ "actual_complexity": 4, "contract_type": "Component" } - }, - { - "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -133957,7 +139859,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI", "PURPOSE": "Filterable run history page with click-to-expand detail view, metrics summary.", "UX_STATE": "pruned -> Data older than 90 days; MetricSnapshot referenced" @@ -133982,7 +139884,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -134002,7 +139906,9 @@ "detail": { "actual_type": "Page", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -134027,7 +139933,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -134041,10 +139949,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } }, { "code": "tag_forbidden_by_complexity", @@ -134068,7 +139990,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra (Tests)", "POST": "Returns promotion metadata", "PRE": "Repo initialized", @@ -134088,20 +140010,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infra (Tests)" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -134133,7 +140041,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All requests must include valid Admin JWT token (handled by api client).", "LAYER": "Service", "PURPOSE": "Service for Admin-related API calls (User and Role management).", @@ -134162,20 +140070,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service" - } } ], "anchor_syntax": "def", @@ -134222,6 +140116,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134313,6 +140216,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134397,6 +140309,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134474,6 +140395,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134542,6 +140472,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134610,6 +140549,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134678,6 +140626,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134746,6 +140703,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134794,6 +140760,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134839,6 +140814,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134877,6 +140861,15 @@ }, "relations": [], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134922,6 +140915,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -134990,6 +140992,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135062,6 +141073,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135113,6 +141133,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135164,6 +141193,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135192,7 +141230,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Service", "PURPOSE": "Frontend API client for file storage management.", "SEMANTICS": [ @@ -135216,20 +141254,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Service' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Service" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -135242,6 +141266,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -135272,6 +141297,15 @@ "message": "@NOTE is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135317,6 +141351,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135382,6 +141425,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135450,6 +141502,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135518,6 +141579,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135593,6 +141663,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135661,6 +141740,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURNS", @@ -135720,6 +141808,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135771,6 +141868,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135822,6 +141928,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135873,6 +141988,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135924,6 +142048,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -135975,6 +142108,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136026,6 +142168,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136077,6 +142228,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136120,26 +142280,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " * [DEF:BackupTypes:Module]\n * @SEMANTICS: types, backup, interface\n * @PURPOSE: Defines types and interfaces for the Backup Management UI.\n */\n\nexport interface Backup {\n id: string;\n name: string;\n path: string;\n environment: string;\n created_at: string;\n size_bytes?: number;\n is_directory?: boolean;\n status: 'success' | 'failed' | 'in_progress';\n}\n\nexport interface BackupCreateRequest {\n environment_id: string;\n}\n\n/**\n * [/DEF:BackupTypes:Module]\n" }, @@ -136152,12 +142293,22 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Domain", "PURPOSE": "TypeScript interfaces for Dashboard entities" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "// [DEF:DashboardTypes:Module]\n/**\n * @COMPLEXITY: 1\n * @PURPOSE: TypeScript interfaces for Dashboard entities\n * @LAYER: Domain\n */\nexport interface DashboardMetadata {\n id: number;\n title: string;\n slug?: string;\n dashboard_slug?: string;\n url_slug?: string;\n url?: string;\n last_modified: string;\n status: string;\n}\n\nexport interface DashboardSelection {\n selected_ids: number[];\n source_env_id: string;\n target_env_id: string;\n replace_db_config?: boolean;\n fix_cross_filters?: boolean;\n}\n\nexport interface DiffObjectRef {\n uuid: string;\n title?: string;\n target_title?: string;\n}\n\nexport interface DiffBucket {\n create: DiffObjectRef[];\n update: DiffObjectRef[];\n delete: DiffObjectRef[];\n}\n\nexport interface DryRunRiskItem {\n code: string;\n severity: \"low\" | \"medium\" | \"high\";\n object_type: string;\n object_uuid: string;\n message: string;\n}\n\nexport interface MigrationDryRunResult {\n generated_at: string;\n selection: DashboardSelection;\n selected_dashboard_titles: string[];\n diff: {\n dashboards: DiffBucket;\n charts: DiffBucket;\n datasets: DiffBucket;\n };\n summary: {\n dashboards: Record<\"create\" | \"update\" | \"delete\", number>;\n charts: Record<\"create\" | \"update\" | \"delete\", number>;\n datasets: Record<\"create\" | \"update\" | \"delete\", number>;\n selected_dashboards: number;\n };\n risk: {\n score: number;\n level: \"low\" | \"medium\" | \"high\";\n items: DryRunRiskItem[];\n };\n}\n// [/DEF:DashboardTypes:Module]\n" }, @@ -136186,20 +142337,17 @@ "tier": "TRIVIAL", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "TIER": "TRIVIAL" }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -136214,20 +142362,17 @@ "tier": "TRIVIAL", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "TIER": "TRIVIAL" }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -136242,15 +142387,15 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Utility script for run" }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -136269,21 +142414,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Build and export an offline Docker bundle for enterprise-clean releases." }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:BuildOfflineDockerBundle:Module]\n# @PURPOSE: Build and export an offline Docker bundle for enterprise-clean releases.\n# @COMPLEXITY: 2\n# [/DEF:BuildOfflineDockerBundle:Module]\n" }, @@ -136296,20 +142431,26 @@ "tier": "TRIVIAL", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Utility script for scan_secrets", "TIER": "TRIVIAL" }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -136347,6 +142488,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136384,6 +142534,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136423,6 +142582,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -136474,6 +142642,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -136519,6 +142702,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -136545,18 +142737,19 @@ "relations": [], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain/Data' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain/Data" + "actual_complexity": 1, + "contract_type": "Module" } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -136583,6 +142776,21 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -136619,6 +142827,21 @@ "actual_complexity": 1, "contract_type": "Component" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -136651,7 +142874,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -136671,7 +142897,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -136696,7 +142924,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -136710,34 +142940,30 @@ } }, { - "code": "tag_not_for_contract_type", + "code": "unknown_tag", "tag": "TIER", - "message": "@TIER is not allowed for contract type 'Store'", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Store'", "detail": { "actual_type": "Store", "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block" + "Component" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TIER", - "message": "@TIER is forbidden for contract type 'Store' at C1", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C1", "detail": { "actual_complexity": 1, "contract_type": "Store" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -136770,7 +142996,10 @@ "Module", "Function", "Class", - "Component" + "Component", + "Block", + "Skill", + "Agent" ] } }, @@ -136790,7 +143019,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -136815,7 +143046,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -136829,34 +143062,30 @@ } }, { - "code": "tag_not_for_contract_type", + "code": "unknown_tag", "tag": "TIER", - "message": "@TIER is not allowed for contract type 'Store'", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Store'", "detail": { "actual_type": "Store", "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block" + "Component" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TIER", - "message": "@TIER is forbidden for contract type 'Store' at C1", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C1", "detail": { "actual_complexity": 1, "contract_type": "Store" } - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "def", @@ -136891,7 +143120,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -136916,7 +143147,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -136930,28 +143163,10 @@ } }, { - "code": "tag_not_for_contract_type", + "code": "unknown_tag", "tag": "TIER", - "message": "@TIER is not allowed for contract type 'Store'", - "detail": { - "actual_type": "Store", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TIER", - "message": "@TIER is forbidden for contract type 'Store' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Store" - } + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null }, { "code": "tag_forbidden_by_complexity", @@ -137031,6 +143246,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137109,6 +143339,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137175,6 +143420,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137258,6 +143518,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137334,6 +143609,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137404,6 +143694,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137493,6 +143798,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137566,28 +143886,26 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "invalid_relation_predicate", + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -137639,20 +143957,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Security' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Security" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -137673,28 +143977,26 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "invalid_relation_predicate", + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -137760,28 +144062,26 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "invalid_relation_predicate", + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -137828,20 +144128,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Interface' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Interface" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -137860,6 +144146,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137930,6 +144231,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -137981,6 +144297,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -138043,6 +144374,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -138107,8 +144447,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -138179,6 +144519,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -138243,8 +144592,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -138309,6 +144658,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -138373,8 +144731,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -138442,6 +144800,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -138506,27 +144873,69 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } }, { "code": "tag_forbidden_by_complexity", @@ -138587,19 +144996,20 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -138663,17 +145073,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infrastructure' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infrastructure" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -138740,8 +145145,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -138806,6 +145211,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -138870,8 +145284,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -138936,6 +145350,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -139000,8 +145423,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -139026,15 +145449,15 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Test Strategy and coverage matrices for Clean Repository Enterprise Profile." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -139083,6 +145506,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -139142,19 +145580,20 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infrastructure' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Infrastructure" + "actual_complexity": 1, + "contract_type": "Module" } }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -139223,6 +145662,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -139287,8 +145735,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -139354,17 +145802,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -139431,8 +145874,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -139497,17 +145940,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -139574,8 +146012,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -139643,6 +146081,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -139707,27 +146154,69 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } }, { "code": "tag_forbidden_by_complexity", @@ -139793,6 +146282,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -139857,27 +146355,69 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } }, { "code": "tag_forbidden_by_complexity", @@ -139955,6 +146495,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140019,8 +146568,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140110,20 +146659,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -140142,6 +146677,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140206,8 +146750,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140279,20 +146823,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -140311,6 +146841,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140375,8 +146914,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140448,20 +146987,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -140480,6 +147005,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140544,8 +147078,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140611,20 +147145,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -140643,6 +147163,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140707,8 +147236,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140799,20 +147328,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -140831,6 +147346,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -140895,8 +147419,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -140971,28 +147495,26 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "invalid_relation_predicate", + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -141054,20 +147576,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -141086,6 +147594,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -141150,8 +147667,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -141222,20 +147739,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Application' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Application" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -141254,6 +147757,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -141318,8 +147830,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -141418,96 +147930,26 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" - } + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null }, { - "code": "invalid_relation_predicate", + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate CALLED_BY is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "VERIFIES" - ], - "predicate": "CALLED_BY" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -141543,6 +147985,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141577,6 +148034,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141611,6 +148083,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141647,6 +148134,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141681,6 +148183,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141715,6 +148232,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141749,6 +148281,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141783,6 +148330,21 @@ "actual_complexity": 1, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null } ], "anchor_syntax": "def", @@ -141831,20 +148393,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Interface' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Interface" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -141863,6 +148411,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -141927,8 +148484,8 @@ }, { "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -141998,7 +148555,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -142029,6 +148588,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -142036,7 +148604,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -142112,27 +148683,30 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_INVARIANT is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Function" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "TEST_INVARIANT", + "message": "@TEST_INVARIANT is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } }, { "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -142183,7 +148757,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -142214,6 +148790,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -142221,7 +148806,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -142234,6 +148822,12 @@ "contract_type": "Class" } }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -142280,7 +148874,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -142294,17 +148890,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Class" } }, { @@ -142314,7 +148905,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -142327,6 +148921,12 @@ "contract_type": "Class" } }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -142368,20 +148968,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -142400,6 +148986,21 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -142450,7 +149051,9 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -142481,6 +149084,15 @@ "contract_type": "Component" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Component" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -142488,7 +149100,10 @@ "detail": { "actual_type": "Component", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -142503,14 +149118,8 @@ }, { "code": "unknown_tag", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", "detail": null }, { @@ -142560,7 +149169,9 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -142573,20 +149184,6 @@ "contract_type": "Store" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI-State' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI-State" - } - }, { "code": "tag_not_for_contract_type", "tag": "POST", @@ -142597,7 +149194,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -142620,7 +149218,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -142645,7 +149244,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -142665,7 +149266,10 @@ "detail": { "actual_type": "Store", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -142679,28 +149283,10 @@ } }, { - "code": "tag_not_for_contract_type", + "code": "unknown_tag", "tag": "TIER", - "message": "@TIER is not allowed for contract type 'Store'", - "detail": { - "actual_type": "Store", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TIER", - "message": "@TIER is forbidden for contract type 'Store' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Store" - } + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null }, { "code": "tag_forbidden_by_complexity", @@ -142725,17 +149311,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "### [DEF:TranslateJobList:Component]\n`frontend/src/routes/translate/+page.svelte`\n@COMPLEXITY 3\n@PURPOSE SvelteKit page listing all translation jobs with status/schedule indicators.\n@UX_STATE idle, loading, empty, populated, error\n[/DEF:TranslateJobList:Component]\n" }, @@ -142749,17 +149325,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "### [DEF:TranslationJobConfig:Component]\n`frontend/src/routes/translate/[id]/+page.svelte`\n@COMPLEXITY 3\n@PURPOSE SvelteKit page for job configuration: datasource selection, column mapping with source→target key mapping, target table/column, LLM settings, dictionary attachment (language-filtered), schedule tab with timezone.\n@UX_STATE idle, loading, configured, saving, validation_error, datasource_unavailable\n@UX_REACTIVITY Column list $derived from datasource; dictionary list filtered by target_language\n[/DEF:TranslationJobConfig:Component]\n" }, @@ -142773,17 +149339,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "### [DEF:TranslationHistory:Component]\n`frontend/src/routes/translate/history/+page.svelte`\n@COMPLEXITY 3\n@PURPOSE Filterable run history: datasource, target table, row count, translation_status, insert_status, date. Detail view with config snapshot, Superset reference, SQL. Pruned runs show metadata only.\n@UX_STATE idle, loading, empty, populated, detail_open, pruned\n[/DEF:TranslationHistory:Component]\n" }, @@ -142797,26 +149353,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "### [DEF:TranslateApiClient:Module]\n`frontend/src/lib/api/translate.js`\n@COMPLEXITY 2\n@PURPOSE API client wrapping requestApi/fetchApi for all translate endpoints.\n[/DEF:TranslateApiClient:Module]\n" }, diff --git a/backend/_convert_defs.py b/backend/_convert_defs.py new file mode 100644 index 00000000..966a99fe --- /dev/null +++ b/backend/_convert_defs.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +""" +Mass migration: [DEF:id:Type] -> #region id [C:N] [TYPE Type] [SEMANTICS ...] + +Processes all Python files under backend/src/ that contain [DEF: annotations. + +STRATEGY: For each [DEF:...[/DEF:...] block, only replace the two boundary lines +and remove the intermediate metadata tag lines. This preserves any already-converted +inner #region blocks when processing nested DEF blocks. + +Consolidation rules: +- COMPLEXITY + [C:N]: keep only inline [C:N], remove @COMPLEXITY line +- Only @COMPLEXITY: move to inline [C:N], remove @COMPLEXITY line +- BRIEF + PURPOSE: keep @BRIEF, remove @PURPOSE line +- Only @PURPOSE: rename to @BRIEF +- @SEMANTICS: ... -> [SEMANTICS ...] inline in region header +- @LAYER: ... -> @LAYER ... (normalized format) +- @RELATION: ..., @PRE:..., @POST:..., etc -> normalize format +""" + +import re +import os +import sys + +SOURCE_DIR = os.path.join(os.path.dirname(__file__), "src") + +# Matches: # [DEF:ContractId:Type] +DEF_OPEN_RE = re.compile(r'^(\s*)#\s*\[DEF:([^:]+):([^\]]+)\]') + +# Matches: # [/DEF:ContractId] or # [/DEF:ContractId:Type] +DEF_CLOSE_RE = re.compile(r'^(\s*)#\s*\[/DEF:([^:\]]+)') + +# Inline [C:N] pattern +C_INLINE_RE = re.compile(r'\[C:\s*(\d+)\]') + +# Tag lines to match +TAG_LINE_RE = re.compile( + r'^(\s*)#\s*@(COMPLEXITY|BRIEF|PURPOSE|SEMANTICS|LAYER|RELATION|' + r'PRE|POST|SIDE_EFFECT|DATA_CONTRACT|INVARIANT|RETURN|RATIONALE|REJECTED|' + r'STATUS|DEPRECATED)\s*:\s*(.+?)\s*$', + re.IGNORECASE +) + + +def _ids_match(close_id, open_id): + """Check if close_id matches open_id, handling fully qualified paths.""" + if close_id == open_id: + return True + # Case-insensitive comparison + c_lower = close_id.lower().rstrip('.') + o_lower = open_id.lower().rstrip('.') + if c_lower == o_lower: + return True + # Fully qualified close e.g. backend.src.services.X.Y + if c_lower.endswith('.' + o_lower): + return True + if o_lower.endswith('.' + c_lower): + return True + # Last component matches + if c_lower.split('.')[-1] == o_lower: + return True + if o_lower.split('.')[-1] == c_lower: + return True + # One contains the other + if o_lower in c_lower or c_lower in o_lower: + return True + return False + + +def normalize_relation(text): + text = text.strip() + # [PRED] -> [Target] + m = re.match(r'\[(\w+)\]\s*->\s*\[(.+?)\]$', text) + if m: + return f"{m.group(1)} -> [{m.group(2)}]" + # PRED -> [Target] + m = re.match(r'(\w+)\s*->\s*\[(.+?)\]$', text) + if m: + return f"{m.group(1)} -> [{m.group(2)}]" + # PRED -> Target + m = re.match(r'(\w+)\s*->\s*(.+)$', text) + if m: + target = m.group(2).strip() + if not target.startswith('['): + target = f'[{target}]' + return f"{m.group(1)} -> {target}" + return text + + +def process_file(filepath): + """Process a single file, converting [DEF:... blocks to #region/#endregion. + + Only replaces boundary lines and removes metadata tags between them. + Does NOT replace the body content, preserving nested conversions. + """ + with open(filepath, 'r', encoding='utf-8') as f: + lines = f.readlines() + + if not lines: + return False + + # Quick check + if not any('[DEF:' in line for line in lines): + return False + + ############################################################### + # PASS 1: Find all opens and match them to closes + ############################################################### + opens = [] # (line_idx, contract_id, type_str, indent) + for i, line in enumerate(lines): + m = DEF_OPEN_RE.match(line) + if m: + opens.append((i, m.group(2).strip(), m.group(3).strip(), m.group(1))) + + # Match closes to opens using stack + close_for_open = {} # open_idx -> close_idx + open_stack = [] + for i, line in enumerate(lines): + m = DEF_CLOSE_RE.match(line) + if m: + close_id = m.group(2).strip() + matched = False + for j in range(len(open_stack) - 1, -1, -1): + oi, oid, otype, oindent = open_stack[j] + if _ids_match(close_id, oid): + close_for_open[oi] = i + open_stack.pop(j) + matched = True + break + if not matched: + pass # Silently skip unmatched closes + else: + m2 = DEF_OPEN_RE.match(line) + if m2: + open_stack.append((i, m2.group(2).strip(), m2.group(3).strip(), m2.group(1))) + + if not close_for_open: + return False + + ############################################################### + # PASS 2: For each matched block, find metadata and plan edits + ############################################################### + # Edits: list of (line_idx, action, data) + # action = 'replace_header' | 'replace_footer' | 'remove_line' + edits = [] + + for open_idx, contract_id, type_str, indent in opens: + close_idx = close_for_open.get(open_idx) + if close_idx is None: + continue + + # Find nested opens to know where to stop scanning for metadata + nested_opens = [oi for oi, _, _, _ in opens if open_idx < oi < close_idx] + + # Scan for metadata lines between open and first code/nested open + metadata_indices = set() + metadata = {} # line_idx -> (tagname, content) + + for i in range(open_idx + 1, close_idx): + # Stop at nested DEF open + if i in nested_opens: + break + + stripped = lines[i].strip() + + # Stop at code line + if stripped and not stripped.startswith('#'): + break + + # Skip blank lines, bare comments, and inline comments + if not stripped or stripped == '#': + continue + + # Check for tag + m = TAG_LINE_RE.match(lines[i]) + if m: + tagname = m.group(2).upper() + content = m.group(3).strip() + metadata_indices.add(i) + metadata[i] = (tagname, content) + else: + # Non-tag comment - stop here + break + + # Check for inline [C:N] + c_val = None + c_inline = C_INLINE_RE.search(lines[open_idx]) + if c_inline: + c_val = int(c_inline.group(1)) + + # Process metadata with deduplication + brief_val = None + purpose_val = None + sem_val = None + layer_val = None + relations = [] + pres = [] + posts = [] + side_effects = [] + data_contracts = [] + invariants = [] + returns = [] + rationales = [] + rejecteds = [] + statuses = [] + + for line_idx, (tagname, content) in sorted(metadata.items()): + if tagname == 'COMPLEXITY': + if c_val is None: + try: + c_val = int(content) + except ValueError: + pass + elif tagname == 'BRIEF': + if brief_val is None: + brief_val = content + elif tagname == 'PURPOSE': + if brief_val is not None: + pass # Drop purpose when brief exists + elif purpose_val is None: + purpose_val = content + elif tagname == 'SEMANTICS': + if sem_val is None: + sem_val = content + elif tagname == 'LAYER': + if layer_val is None: + layer_val = content + elif tagname == 'RELATION': + relations.append(normalize_relation(content)) + elif tagname == 'PRE': + pres.append(content) + elif tagname == 'POST': + posts.append(content) + elif tagname == 'SIDE_EFFECT': + side_effects.append(content) + elif tagname == 'DATA_CONTRACT': + data_contracts.append(content) + elif tagname == 'INVARIANT': + invariants.append(content) + elif tagname == 'RETURN': + returns.append(content) + elif tagname == 'RATIONALE': + rationales.append(content) + elif tagname == 'REJECTED': + rejecteds.append(content) + elif tagname in ('STATUS', 'DEPRECATED'): + statuses.append(f'{tagname.upper()} {content}') + + # Consolidate brief/purpose + if purpose_val and not brief_val: + brief_val = purpose_val + # If both existed, purpose was already discarded above + + # Build region header + c_part = f' [C:{c_val}]' if c_val is not None else '' + sem_part = f' [SEMANTICS {sem_val}]' if sem_val else '' + region_header = f'{indent}# #region {contract_id}{c_part} [TYPE {type_str}]{sem_part}' + + # Build metadata body + meta_lines = [] + if brief_val: + meta_lines.append(f'{indent}# @BRIEF {brief_val}') + if layer_val: + meta_lines.append(f'{indent}# @LAYER {layer_val}') + for pre in pres: + meta_lines.append(f'{indent}# @PRE {pre}') + for post in posts: + meta_lines.append(f'{indent}# @POST {post}') + for se in side_effects: + meta_lines.append(f'{indent}# @SIDE_EFFECT {se}') + for dc in data_contracts: + meta_lines.append(f'{indent}# @DATA_CONTRACT {dc}') + for inv in invariants: + meta_lines.append(f'{indent}# @INVARIANT {inv}') + for ret in returns: + meta_lines.append(f'{indent}# @RETURN {ret}') + for rel in relations: + meta_lines.append(f'{indent}# @RELATION {rel}') + for st in statuses: + meta_lines.append(f'{indent}# @{st}') + for rat in rationales: + meta_lines.append(f'{indent}# @RATIONALE {rat}') + for rej in rejecteds: + meta_lines.append(f'{indent}# @REJECTED {rej}') + + region_footer = f'{indent}# #endregion {contract_id}' + + # Plan edits: + # 1. Replace [DEF: line with region_header + meta_lines + edits.append(('replace_multi', open_idx, [region_header] + meta_lines)) + + # 2. Remove each metadata tag line + for mi in metadata_indices: + edits.append(('remove', mi, None)) + + # 3. Replace [/DEF: line with #endregion + edits.append(('replace_single', close_idx, region_footer)) + + if not edits: + return False + + # Process from bottom to top (descending line number) to preserve indices + edits.sort(key=lambda e: e[1], reverse=True) + + for action, line_idx, data in edits: + if action == 'replace_multi': + lines[line_idx] = data[0] + '\n' + # Insert remaining lines after + for j, extra_line in enumerate(data[1:], 1): + lines.insert(line_idx + j, extra_line + '\n') + elif action == 'replace_single': + lines[line_idx] = data + '\n' + elif action == 'remove': + lines[line_idx] = None + + # Strip None lines + lines = [l for l in lines if l is not None] + + # Remove empty bare comment lines that might have been left + cleaned = [] + for line in lines: + if line.strip() == '#': + continue + cleaned.append(line) + + with open(filepath, 'w', encoding='utf-8') as f: + f.writelines(cleaned) + + return True + + +def find_remaining_defs(): + """Find all files that still have [DEF: patterns.""" + remaining = [] + for root, dirs, files in os.walk(SOURCE_DIR): + dirs[:] = [d for d in dirs if d not in ('__pycache__', '.venv', 'node_modules')] + for f in files: + if f.endswith('.py'): + fp = os.path.join(root, f) + try: + with open(fp, 'r', encoding='utf-8') as fh: + content = fh.read() + if '[DEF:' in content: + remaining.append(fp) + except Exception: + pass + return remaining + + +def main(): + all_files = [] + for root, dirs, files in os.walk(SOURCE_DIR): + dirs[:] = [d for d in dirs if d not in ('__pycache__', '.venv', 'node_modules')] + for f in files: + if f.endswith('.py'): + all_files.append(os.path.join(root, f)) + + print(f"Scanning {len(all_files)} Python files...") + + # Multi-pass until stable + max_passes = 5 + for pass_num in range(1, max_passes + 1): + remaining = find_remaining_defs() + if not remaining: + print(f"\n✅ All DEF annotations migrated after pass {pass_num - 1}!") + return + + print(f"\nPass {pass_num}: Processing {len(remaining)} remaining files...") + modified = 0 + errors = 0 + + for fp in sorted(remaining): + rel_path = os.path.relpath(fp, SOURCE_DIR) + try: + changed = process_file(fp) + if changed: + print(f" ✓ {rel_path}") + modified += 1 + except Exception as e: + print(f" ✗ {rel_path}: {e}") + errors += 1 + import traceback + traceback.print_exc() + + print(f" Modified: {modified}, Errors: {errors}") + + if modified == 0 and errors == 0: + # No changes and no errors - files might have DEF patterns + # that can't be matched (e.g. orphaned opens/closes) + break + + # Final report + remaining = find_remaining_defs() + if remaining: + print(f"\n⚠️ {len(remaining)} files still have unconverted [DEF: patterns:") + for fp in sorted(remaining): + rel = os.path.relpath(fp, SOURCE_DIR) + # Count the DEF patterns + with open(fp, 'r') as f: + content = f.read() + opens = content.count('[DEF:') + closes = content.count('[/DEF:') + print(f" {rel} ({opens} opens, {closes} closes)") + else: + print(f"\n✅ All [DEF:] annotations have been migrated to #region format!") + + +if __name__ == '__main__': + main() diff --git a/backend/src/__init__.py b/backend/src/__init__.py index e791e586..2de99616 100644 --- a/backend/src/__init__.py +++ b/backend/src/__init__.py @@ -1,3 +1,3 @@ -# [DEF:SrcRoot:Module] -# @PURPOSE: Canonical backend package root for application, scripts, and tests. -# [/DEF:SrcRoot:Module] +# #region SrcRoot [TYPE Module] +# @BRIEF Canonical backend package root for application, scripts, and tests. +# #endregion SrcRoot diff --git a/backend/src/api/__init__.py b/backend/src/api/__init__.py index 4b6a6c7e..8a88203f 100644 --- a/backend/src/api/__init__.py +++ b/backend/src/api/__init__.py @@ -1,3 +1,3 @@ -# [DEF:src.api:Package] -# @PURPOSE: Backend API package root. -# [/DEF:src.api:Package] +# #region src.api [TYPE Package] +# @BRIEF Backend API package root. +# #endregion src.api diff --git a/backend/src/api/auth.py b/backend/src/api/auth.py index 81ca9209..3c19c27a 100755 --- a/backend/src/api/auth.py +++ b/backend/src/api/auth.py @@ -1,14 +1,12 @@ -# [DEF:AuthApi:Module] +# #region AuthApi [C:3] [TYPE Module] [SEMANTICS api, auth, routes, login, logout] +# @BRIEF Authentication API endpoints. +# @LAYER API +# @INVARIANT All auth endpoints must return consistent error codes. +# @RELATION DEPENDS_ON -> [AuthService] +# @RELATION DEPENDS_ON -> [get_auth_db] +# @RELATION DEPENDS_ON -> [get_current_user] +# @RELATION DEPENDS_ON -> [is_adfs_configured] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, auth, routes, login, logout -# @PURPOSE: Authentication API endpoints. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AuthService] -# @RELATION: DEPENDS_ON -> [get_auth_db] -# @RELATION: DEPENDS_ON -> [get_current_user] -# @RELATION: DEPENDS_ON -> [is_adfs_configured] -# @INVARIANT: All auth endpoints must return consistent error codes. # [SECTION: IMPORTS] from fastapi import APIRouter, Depends, HTTPException, status @@ -24,19 +22,17 @@ from ..core.logger import belief_scope import starlette.requests # [/SECTION] -# [DEF:router:Variable] -# @RELATION: DEPENDS_ON -> [fastapi.APIRouter] -# @COMPLEXITY: 1 -# @PURPOSE: APIRouter instance for authentication routes. +# #region router [C:1] [TYPE Variable] +# @BRIEF APIRouter instance for authentication routes. +# @RELATION DEPENDS_ON -> [fastapi.APIRouter] router = APIRouter(prefix="/api/auth", tags=["auth"]) -# [/DEF:router:Variable] +# #endregion router -# [DEF:login_for_access_token:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Authenticates a user and returns a JWT access token. -# @PRE: form_data contains username and password. -# @POST: Returns a Token object on success. +# #region login_for_access_token [C:3] [TYPE Function] +# @BRIEF Authenticates a user and returns a JWT access token. +# @PRE form_data contains username and password. +# @POST Returns a Token object on success. # @THROW: HTTPException 401 if authentication fails. # @PARAM: form_data (OAuth2PasswordRequestForm) - Login credentials. # @PARAM: db (Session) - Auth database session. @@ -63,14 +59,13 @@ async def login_for_access_token( return auth_service.create_session(user) -# [/DEF:login_for_access_token:Function] +# #endregion login_for_access_token -# [DEF:read_users_me:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Retrieves the profile of the currently authenticated user. -# @PRE: Valid JWT token provided. -# @POST: Returns the current user's data. +# #region read_users_me [C:3] [TYPE Function] +# @BRIEF Retrieves the profile of the currently authenticated user. +# @PRE Valid JWT token provided. +# @POST Returns the current user's data. # @PARAM: current_user (UserSchema) - The user extracted from the token. # @RETURN: UserSchema - The current user profile. # @RELATION: DEPENDS_ON -> [get_current_user] @@ -80,14 +75,13 @@ async def read_users_me(current_user: UserSchema = Depends(get_current_user)): return current_user -# [/DEF:read_users_me:Function] +# #endregion read_users_me -# [DEF:logout:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Logs out the current user (placeholder for session revocation). -# @PRE: Valid JWT token provided. -# @POST: Returns success message. +# #region logout [C:3] [TYPE Function] +# @BRIEF Logs out the current user (placeholder for session revocation). +# @PRE Valid JWT token provided. +# @POST Returns success message. # @PARAM: current_user (UserSchema) - The user extracted from the token. # @RELATION: DEPENDS_ON -> [get_current_user] @router.post("/logout") @@ -99,14 +93,13 @@ async def logout(current_user: UserSchema = Depends(get_current_user)): return {"message": "Successfully logged out"} -# [/DEF:logout:Function] +# #endregion logout -# [DEF:login_adfs:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Initiates the ADFS OIDC login flow. -# @POST: Redirects the user to ADFS. -# @RELATION: USES -> [is_adfs_configured] +# #region login_adfs [C:3] [TYPE Function] +# @BRIEF Initiates the ADFS OIDC login flow. +# @POST Redirects the user to ADFS. +# @RELATION USES -> [is_adfs_configured] @router.get("/login/adfs") async def login_adfs(request: starlette.requests.Request): with belief_scope("api.auth.login_adfs"): @@ -119,16 +112,15 @@ async def login_adfs(request: starlette.requests.Request): return await oauth.adfs.authorize_redirect(request, str(redirect_uri)) -# [/DEF:login_adfs:Function] +# #endregion login_adfs -# [DEF:auth_callback_adfs:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Handles the callback from ADFS after successful authentication. -# @POST: Provisions user JIT and returns session token. -# @RELATION: DEPENDS_ON -> [is_adfs_configured] -# @RELATION: CALLS -> [AuthService.provision_adfs_user] -# @RELATION: CALLS -> [AuthService.create_session] +# #region auth_callback_adfs [C:3] [TYPE Function] +# @BRIEF Handles the callback from ADFS after successful authentication. +# @POST Provisions user JIT and returns session token. +# @RELATION DEPENDS_ON -> [is_adfs_configured] +# @RELATION CALLS -> [AuthService.provision_adfs_user] +# @RELATION CALLS -> [AuthService.create_session] @router.get("/callback/adfs", name="auth_callback_adfs") async def auth_callback_adfs( request: starlette.requests.Request, db: Session = Depends(get_auth_db) @@ -151,6 +143,6 @@ async def auth_callback_adfs( return auth_service.create_session(user) -# [/DEF:auth_callback_adfs:Function] +# #endregion auth_callback_adfs -# [/DEF:AuthApi:Module] +# #endregion AuthApi diff --git a/backend/src/api/routes/__init__.py b/backend/src/api/routes/__init__.py index 06778f95..ca993c24 100755 --- a/backend/src/api/routes/__init__.py +++ b/backend/src/api/routes/__init__.py @@ -1,21 +1,18 @@ -# [DEF:ApiRoutesModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: routes, lazy-import, module-registry -# @PURPOSE: Provide lazy route module loading to avoid heavyweight imports during tests. -# @LAYER: API -# @RELATION: [CALLS] ->[ApiRoutesGetAttr] -# @RELATION: [BINDS_TO] ->[Route_Group_Contracts] -# @INVARIANT: Only names listed in __all__ are importable via __getattr__. +# #region ApiRoutesModule [C:3] [TYPE Module] [SEMANTICS routes, lazy-import, module-registry] +# @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests. +# @LAYER API +# @INVARIANT Only names listed in __all__ are importable via __getattr__. +# @RELATION CALLS -> [ApiRoutesGetAttr] +# @RELATION BINDS_TO -> [Route_Group_Contracts] -# [DEF:Route_Group_Contracts:Block] -# @COMPLEXITY: 3 -# @PURPOSE: Declare the canonical route-module registry used by lazy imports and app router inclusion. -# @RELATION: DEPENDS_ON -> [PluginsRouter] -# @RELATION: DEPENDS_ON -> [TasksRouter] -# @RELATION: DEPENDS_ON -> [SettingsRouter] -# @RELATION: DEPENDS_ON -> [ConnectionsRouter] -# @RELATION: DEPENDS_ON -> [ReportsRouter] -# @RELATION: DEPENDS_ON -> [LlmRoutes] +# #region Route_Group_Contracts [C:3] [TYPE Block] +# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion. +# @RELATION DEPENDS_ON -> [PluginsRouter] +# @RELATION DEPENDS_ON -> [TasksRouter] +# @RELATION DEPENDS_ON -> [SettingsRouter] +# @RELATION DEPENDS_ON -> [ConnectionsRouter] +# @RELATION DEPENDS_ON -> [ReportsRouter] +# @RELATION DEPENDS_ON -> [LlmRoutes] __all__ = [ "plugins", "tasks", @@ -39,15 +36,14 @@ __all__ = [ "health", "translate", ] -# [/DEF:Route_Group_Contracts:Block] +# #endregion Route_Group_Contracts -# [DEF:ApiRoutesGetAttr:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lazily import route module by attribute name. -# @RELATION: [DEPENDS_ON] ->[ApiRoutesModule] -# @PRE: name is module candidate exposed in __all__. -# @POST: Returns imported submodule or raises AttributeError. +# #region ApiRoutesGetAttr [C:3] [TYPE Function] +# @BRIEF Lazily import route module by attribute name. +# @PRE name is module candidate exposed in __all__. +# @POST Returns imported submodule or raises AttributeError. +# @RELATION DEPENDS_ON -> [ApiRoutesModule] def __getattr__(name): if name in __all__: import importlib @@ -56,5 +52,5 @@ def __getattr__(name): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -# [/DEF:ApiRoutesGetAttr:Function] -# [/DEF:ApiRoutesModule:Module] +# #endregion ApiRoutesGetAttr +# #endregion ApiRoutesModule diff --git a/backend/src/api/routes/__tests__/conftest.py b/backend/src/api/routes/__tests__/conftest.py index 3fc1280d..266ed26b 100644 --- a/backend/src/api/routes/__tests__/conftest.py +++ b/backend/src/api/routes/__tests__/conftest.py @@ -1,6 +1,5 @@ -# [DEF:RoutesTestsConftest:Module] -# @COMPLEXITY: 1 -# @PURPOSE: Shared low-fidelity test doubles for API route test modules. +# #region RoutesTestsConftest [C:1] [TYPE Module] +# @BRIEF Shared low-fidelity test doubles for API route test modules. class FakeQuery: @@ -42,4 +41,4 @@ class FakeQuery: return len(self._rows) -# [/DEF:RoutesTestsConftest:Module] +# #endregion RoutesTestsConftest diff --git a/backend/src/api/routes/__tests__/test_assistant_api.py b/backend/src/api/routes/__tests__/test_assistant_api.py index 379b42c9..0a6978e5 100644 --- a/backend/src/api/routes/__tests__/test_assistant_api.py +++ b/backend/src/api/routes/__tests__/test_assistant_api.py @@ -1,12 +1,10 @@ import os os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w=" -# [DEF:AssistantApiTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, assistant, api -# @PURPOSE: Validate assistant API endpoint logic via direct async handler invocation. -# @RELATION: DEPENDS_ON -> [AssistantApi] -# @INVARIANT: Every test clears assistant in-memory state before execution. +# #region AssistantApiTests [C:3] [TYPE Module] [SEMANTICS tests, assistant, api] +# @BRIEF Validate assistant API endpoint logic via direct async handler invocation. +# @INVARIANT Every test clears assistant in-memory state before execution. +# @RELATION DEPENDS_ON -> [AssistantApi] import asyncio import uuid @@ -37,20 +35,19 @@ from src.models.dataset_review import ( ) -# [DEF:_run_async:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] +# #region _run_async [TYPE Function] +# @RELATION BINDS_TO -> [AssistantApiTests] def _run_async(coro): return asyncio.run(coro) -# [/DEF:_run_async:Function] +# #endregion _run_async -# [DEF:_FakeTask:Class] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 1 -# @PURPOSE: Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests. -# @INVARIANT: status is a bare string not a TaskStatus enum; callers must not depend on enum semantics. +# #region _FakeTask [C:1] [TYPE Class] +# @BRIEF Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests. +# @INVARIANT status is a bare string not a TaskStatus enum; callers must not depend on enum semantics. +# @RELATION BINDS_TO -> [AssistantApiTests] class _FakeTask: def __init__( self, @@ -71,15 +68,14 @@ class _FakeTask: self.finished_at = datetime.utcnow() -# [/DEF:_FakeTask:Class] +# #endregion _FakeTask # @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks(). -# [DEF:_FakeTaskManager:Class] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 2 -# @PURPOSE: In-memory task manager stub that records created tasks for route-level assertions. -# @INVARIANT: create_task stores tasks retrievable by get_task/get_tasks without external side effects. +# #region _FakeTaskManager [C:2] [TYPE Class] +# @BRIEF In-memory task manager stub that records created tasks for route-level assertions. +# @INVARIANT create_task stores tasks retrievable by get_task/get_tasks without external side effects. +# @RELATION BINDS_TO -> [AssistantApiTests] class _FakeTaskManager: def __init__(self): self.tasks = {} @@ -108,14 +104,13 @@ class _FakeTaskManager: return list(self.tasks.values()) -# [/DEF:_FakeTaskManager:Class] +# #endregion _FakeTaskManager -# [DEF:_FakeConfigManager:Class] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 2 -# @PURPOSE: Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests. -# @INVARIANT: get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access. +# #region _FakeConfigManager [C:2] [TYPE Class] +# @BRIEF Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests. +# @INVARIANT get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access. +# @RELATION BINDS_TO -> [AssistantApiTests] class _FakeConfigManager: class _Env: def __init__(self, id, name): @@ -137,13 +132,12 @@ class _FakeConfigManager: return _Config() -# [/DEF:_FakeConfigManager:Class] +# #endregion _FakeConfigManager -# [DEF:_admin_user:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 1 -# @PURPOSE: Build admin principal with spec=User for assistant route authorization tests. +# #region _admin_user [C:1] [TYPE Function] +# @BRIEF Build admin principal with spec=User for assistant route authorization tests. +# @RELATION BINDS_TO -> [AssistantApiTests] def _admin_user(): user = MagicMock(spec=User) user.id = "u-admin" @@ -154,13 +148,12 @@ def _admin_user(): return user -# [/DEF:_admin_user:Function] +# #endregion _admin_user -# [DEF:_limited_user:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 1 -# @PURPOSE: Build limited user principal with empty roles for assistant route denial tests. +# #region _limited_user [C:1] [TYPE Function] +# @BRIEF Build limited user principal with empty roles for assistant route denial tests. +# @RELATION BINDS_TO -> [AssistantApiTests] def _limited_user(): user = MagicMock(spec=User) user.id = "u-limited" @@ -169,14 +162,13 @@ def _limited_user(): return user -# [/DEF:_limited_user:Function] +# #endregion _limited_user -# [DEF:_FakeQuery:Class] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 2 -# @PURPOSE: Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths. -# @INVARIANT: filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated. +# #region _FakeQuery [C:2] [TYPE Class] +# @BRIEF Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths. +# @INVARIANT filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated. +# @RELATION BINDS_TO -> [AssistantApiTests] class _FakeQuery: def __init__(self, items): self.items = items @@ -212,14 +204,13 @@ class _FakeQuery: return len(self.items) -# [/DEF:_FakeQuery:Class] +# #endregion _FakeQuery -# [DEF:_FakeDb:Class] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 2 -# @PURPOSE: Explicit in-memory DB session double limited to assistant message persistence paths. -# @INVARIANT: query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior. +# #region _FakeDb [C:2] [TYPE Class] +# @BRIEF Explicit in-memory DB session double limited to assistant message persistence paths. +# @INVARIANT query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior. +# @RELATION BINDS_TO -> [AssistantApiTests] class _FakeDb: def __init__(self): self.added = [] @@ -248,11 +239,11 @@ class _FakeDb: pass -# [/DEF:_FakeDb:Class] +# #endregion _FakeDb -# [DEF:_clear_assistant_state:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] +# #region _clear_assistant_state [TYPE Function] +# @RELATION BINDS_TO -> [AssistantApiTests] def _clear_assistant_state(): assistant_routes.CONVERSATIONS.clear() assistant_routes.USER_ACTIVE_CONVERSATION.clear() @@ -260,13 +251,12 @@ def _clear_assistant_state(): assistant_routes.ASSISTANT_AUDIT.clear() -# [/DEF:_clear_assistant_state:Function] +# #endregion _clear_assistant_state -# [DEF:_dataset_review_session:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 1 -# @PURPOSE: Build minimal owned dataset-review session fixture for assistant scoped routing tests. +# #region _dataset_review_session [C:1] [TYPE Function] +# @BRIEF Build minimal owned dataset-review session fixture for assistant scoped routing tests. +# @RELATION BINDS_TO -> [AssistantApiTests] def _dataset_review_session(): session = DatasetReviewSession( session_id="sess-1", @@ -364,23 +354,22 @@ def _dataset_review_session(): return session -# [/DEF:_dataset_review_session:Function] +# #endregion _dataset_review_session -# [DEF:_await_none:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @COMPLEXITY: 1 -# @PURPOSE: Async helper returning None for planner fallback tests. +# #region _await_none [C:1] [TYPE Function] +# @BRIEF Async helper returning None for planner fallback tests. +# @RELATION BINDS_TO -> [AssistantApiTests] async def _await_none(*args, **kwargs): return None -# [/DEF:_await_none:Function] +# #endregion _await_none -# [DEF:test_unknown_command_returns_needs_clarification:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Unknown command should return clarification state and unknown intent. +# #region test_unknown_command_returns_needs_clarification [TYPE Function] +# @BRIEF Unknown command should return clarification state and unknown intent. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_unknown_command_returns_needs_clarification(monkeypatch): _clear_assistant_state() req = assistant_routes.AssistantMessageRequest(message="some random gibberish") @@ -402,12 +391,12 @@ def test_unknown_command_returns_needs_clarification(monkeypatch): assert "уточните" in resp.text.lower() or "неоднозначна" in resp.text.lower() -# [/DEF:test_unknown_command_returns_needs_clarification:Function] +# #endregion test_unknown_command_returns_needs_clarification -# [DEF:test_capabilities_question_returns_successful_help:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Capability query should return deterministic help response. +# #region test_capabilities_question_returns_successful_help [TYPE Function] +# @BRIEF Capability query should return deterministic help response. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_capabilities_question_returns_successful_help(monkeypatch): _clear_assistant_state() req = assistant_routes.AssistantMessageRequest(message="что ты умеешь?") @@ -426,12 +415,12 @@ def test_capabilities_question_returns_successful_help(monkeypatch): assert "я могу сделать" in resp.text.lower() -# [/DEF:test_capabilities_question_returns_successful_help:Function] +# #endregion test_capabilities_question_returns_successful_help -# [DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Assistant request schema should accept active dataset review session binding for scoped orchestration. +# #region test_assistant_message_request_accepts_dataset_review_session_binding [TYPE Function] +# @BRIEF Assistant request schema should accept active dataset review session binding for scoped orchestration. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_assistant_message_request_accepts_dataset_review_session_binding(): request = assistant_routes.AssistantMessageRequest( message="approve mappings", @@ -441,12 +430,12 @@ def test_assistant_message_request_accepts_dataset_review_session_binding(): assert request.dataset_review_session_id == "sess-1" -# [/DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function] +# #endregion test_assistant_message_request_accepts_dataset_review_session_binding -# [DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted. +# #region test_dataset_review_scoped_message_uses_masked_filter_context [TYPE Function] +# @BRIEF Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch): _clear_assistant_state() db = _FakeDb() @@ -489,12 +478,12 @@ def test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch): assert imported_filters[0]["raw_value_masked"] is True -# [/DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function] +# #endregion test_dataset_review_scoped_message_uses_masked_filter_context -# [DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata. +# #region test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval [TYPE Function] +# @BRIEF Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval(): _clear_assistant_state() db = _FakeDb() @@ -522,12 +511,12 @@ def test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval assert resp.intent["entities"]["mapping_ids"] == ["map-1"] -# [/DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function] +# #endregion test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval -# [DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function] -# @RELATION: BINDS_TO -> [AssistantApiTests] -# @PURPOSE: Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata. +# #region test_dataset_review_scoped_command_routes_field_semantics_update [TYPE Function] +# @BRIEF Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata. +# @RELATION BINDS_TO -> [AssistantApiTests] def test_dataset_review_scoped_command_routes_field_semantics_update(): _clear_assistant_state() db = _FakeDb() @@ -556,7 +545,7 @@ def test_dataset_review_scoped_command_routes_field_semantics_update(): assert resp.intent["entities"]["lock_field"] is True -# [/DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function] +# #endregion test_dataset_review_scoped_command_routes_field_semantics_update -# [/DEF:AssistantApiTests:Module] +# #endregion AssistantApiTests diff --git a/backend/src/api/routes/__tests__/test_assistant_authz.py b/backend/src/api/routes/__tests__/test_assistant_authz.py index ad8e9078..9f9e6be2 100644 --- a/backend/src/api/routes/__tests__/test_assistant_authz.py +++ b/backend/src/api/routes/__tests__/test_assistant_authz.py @@ -1,14 +1,12 @@ import os os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w=" -# [DEF:TestAssistantAuthz:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, assistant, authz, confirmation, rbac -# @PURPOSE: Verify assistant confirmation ownership, expiration, and deny behavior for restricted users. -# @LAYER: UI (API Tests) +# #region TestAssistantAuthz [C:3] [TYPE Module] [SEMANTICS tests, assistant, authz, confirmation, rbac] +# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users. +# @LAYER UI (API Tests) +# @INVARIANT Security-sensitive flows fail closed for unauthorized actors. +# @RELATION DEPENDS_ON -> [AssistantApi] -# @RELATION: DEPENDS_ON -> AssistantApi -# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors. import os import asyncio @@ -35,25 +33,23 @@ from src.models.assistant import ( ) -# [DEF:_run_async:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Execute async endpoint handler in synchronous test context. -# @PRE: coroutine is awaitable endpoint invocation. -# @POST: Returns coroutine result or raises propagated exception. +# #region _run_async [C:1] [TYPE Function] +# @BRIEF Execute async endpoint handler in synchronous test context. +# @PRE coroutine is awaitable endpoint invocation. +# @POST Returns coroutine result or raises propagated exception. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def _run_async(coroutine): return asyncio.run(coroutine) -# [/DEF:_run_async:Function] +# #endregion _run_async -# [DEF:_FakeTask:Class] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Lightweight task model used for assistant authz tests. -# @PRE: task_id is non-empty string. -# @POST: Returns task with provided id, status, and user_id accessible as attributes. +# #region _FakeTask [C:1] [TYPE Class] +# @BRIEF Lightweight task model used for assistant authz tests. +# @PRE task_id is non-empty string. +# @POST Returns task with provided id, status, and user_id accessible as attributes. +# @RELATION BINDS_TO -> [TestAssistantAuthz] class _FakeTask: def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"): self.id = task_id @@ -61,13 +57,12 @@ class _FakeTask: self.user_id = user_id -# [/DEF:_FakeTask:Class] +# #endregion _FakeTask # @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks(). -# [DEF:_FakeTaskManager:Class] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 2 -# @PURPOSE: In-memory task manager double that records assistant-created tasks deterministically. -# @INVARIANT: Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated. +# #region _FakeTaskManager [C:2] [TYPE Class] +# @BRIEF In-memory task manager double that records assistant-created tasks deterministically. +# @INVARIANT Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated. +# @RELATION BINDS_TO -> [TestAssistantAuthz] class _FakeTaskManager: def __init__(self): self._created = [] @@ -93,15 +88,14 @@ class _FakeTaskManager: ) -# [/DEF:_FakeTaskManager:Class] +# #endregion _FakeTaskManager # @CONTRACT: Partial ConfigManager stub for authz tests. Missing: get_config(). -# [DEF:_FakeConfigManager:Class] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Provide deterministic environment aliases required by intent parsing. -# @PRE: No external config or DB state is required. -# @POST: get_environments() returns two deterministic SimpleNamespace stubs with id/name. -# @INVARIANT: get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake. +# #region _FakeConfigManager [C:1] [TYPE Class] +# @BRIEF Provide deterministic environment aliases required by intent parsing. +# @PRE No external config or DB state is required. +# @POST get_environments() returns two deterministic SimpleNamespace stubs with id/name. +# @INVARIANT get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake. +# @RELATION BINDS_TO -> [TestAssistantAuthz] class _FakeConfigManager: def get_environments(self): return [ @@ -115,50 +109,46 @@ class _FakeConfigManager: ) -# [/DEF:_FakeConfigManager:Class] -# [DEF:_admin_user:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Build admin principal fixture. -# @PRE: Test requires privileged principal for risky operations. -# @POST: Returns admin-like user stub with Admin role. +# #endregion _FakeConfigManager +# #region _admin_user [C:1] [TYPE Function] +# @BRIEF Build admin principal fixture. +# @PRE Test requires privileged principal for risky operations. +# @POST Returns admin-like user stub with Admin role. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def _admin_user(): role = SimpleNamespace(name="Admin", permissions=[]) return SimpleNamespace(id="u-admin", username="admin", roles=[role]) -# [/DEF:_admin_user:Function] -# [DEF:_other_admin_user:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Build second admin principal fixture for ownership tests. -# @PRE: Ownership mismatch scenario needs distinct authenticated actor. -# @POST: Returns alternate admin-like user stub. +# #endregion _admin_user +# #region _other_admin_user [C:1] [TYPE Function] +# @BRIEF Build second admin principal fixture for ownership tests. +# @PRE Ownership mismatch scenario needs distinct authenticated actor. +# @POST Returns alternate admin-like user stub. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def _other_admin_user(): role = SimpleNamespace(name="Admin", permissions=[]) return SimpleNamespace(id="u-admin-2", username="admin2", roles=[role]) -# [/DEF:_other_admin_user:Function] -# [DEF:_limited_user:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Build limited principal without required assistant execution privileges. -# @PRE: Permission denial scenario needs non-admin actor. -# @POST: Returns restricted user stub. +# #endregion _other_admin_user +# #region _limited_user [C:1] [TYPE Function] +# @BRIEF Build limited principal without required assistant execution privileges. +# @PRE Permission denial scenario needs non-admin actor. +# @POST Returns restricted user stub. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def _limited_user(): role = SimpleNamespace(name="Operator", permissions=[]) return SimpleNamespace(id="u-limited", username="limited", roles=[role]) -# [/DEF:_limited_user:Function] +# #endregion _limited_user -# [DEF:_FakeQuery:Class] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal chainable query object for fake DB interactions. -# @INVARIANT: filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation. +# #region _FakeQuery [C:1] [TYPE Class] +# @BRIEF Minimal chainable query object for fake DB interactions. +# @INVARIANT filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation. +# @RELATION BINDS_TO -> [TestAssistantAuthz] class _FakeQuery: def __init__(self, rows): self._rows = list(rows) @@ -188,12 +178,11 @@ class _FakeQuery: return len(self._rows) -# [/DEF:_FakeQuery:Class] -# [DEF:_FakeDb:Class] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 2 -# @PURPOSE: In-memory DB session double constrained to assistant message/confirmation/audit persistence paths. -# @INVARIANT: query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics. +# #endregion _FakeQuery +# #region _FakeDb [C:2] [TYPE Class] +# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths. +# @INVARIANT query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics. +# @RELATION BINDS_TO -> [TestAssistantAuthz] class _FakeDb: def __init__(self): self._messages = [] @@ -237,13 +226,12 @@ class _FakeDb: return None -# [/DEF:_FakeDb:Class] -# [DEF:_clear_assistant_state:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @COMPLEXITY: 1 -# @PURPOSE: Reset assistant process-local state between test cases. -# @PRE: Assistant globals may contain state from prior tests. -# @POST: Assistant in-memory state dictionaries are cleared. +# #endregion _FakeDb +# #region _clear_assistant_state [C:1] [TYPE Function] +# @BRIEF Reset assistant process-local state between test cases. +# @PRE Assistant globals may contain state from prior tests. +# @POST Assistant in-memory state dictionaries are cleared. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def _clear_assistant_state(): assistant_module.CONVERSATIONS.clear() assistant_module.USER_ACTIVE_CONVERSATION.clear() @@ -251,14 +239,14 @@ def _clear_assistant_state(): assistant_module.ASSISTANT_AUDIT.clear() -# [/DEF:_clear_assistant_state:Function] +# #endregion _clear_assistant_state -# [DEF:test_confirmation_owner_mismatch_returns_403:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @PURPOSE: Confirm endpoint should reject requests from user that does not own the confirmation token. -# @PRE: Confirmation token is created by first admin actor. -# @POST: Second actor receives 403 on confirm operation. +# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function] +# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token. +# @PRE Confirmation token is created by first admin actor. +# @POST Second actor receives 403 on confirm operation. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def test_confirmation_owner_mismatch_returns_403(): _clear_assistant_state() task_manager = _FakeTaskManager() @@ -290,14 +278,14 @@ def test_confirmation_owner_mismatch_returns_403(): assert exc.value.status_code == 403 -# [/DEF:test_confirmation_owner_mismatch_returns_403:Function] +# #endregion test_confirmation_owner_mismatch_returns_403 -# [DEF:test_expired_confirmation_cannot_be_confirmed:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @PURPOSE: Expired confirmation token should be rejected and not create task. -# @PRE: Confirmation token exists and is manually expired before confirm request. -# @POST: Confirm endpoint raises 400 and no task is created. +# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function] +# @BRIEF Expired confirmation token should be rejected and not create task. +# @PRE Confirmation token exists and is manually expired before confirm request. +# @POST Confirm endpoint raises 400 and no task is created. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def test_expired_confirmation_cannot_be_confirmed(): _clear_assistant_state() task_manager = _FakeTaskManager() @@ -332,14 +320,14 @@ def test_expired_confirmation_cannot_be_confirmed(): assert task_manager.get_tasks(limit=10, offset=0) == [] -# [/DEF:test_expired_confirmation_cannot_be_confirmed:Function] +# #endregion test_expired_confirmation_cannot_be_confirmed -# [DEF:test_limited_user_cannot_launch_restricted_operation:Function] -# @RELATION: BINDS_TO -> [TestAssistantAuthz] -# @PURPOSE: Limited user should receive denied state for privileged operation. -# @PRE: Restricted user attempts dangerous deploy command. -# @POST: Assistant returns denied state and does not execute operation. +# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function] +# @BRIEF Limited user should receive denied state for privileged operation. +# @PRE Restricted user attempts dangerous deploy command. +# @POST Assistant returns denied state and does not execute operation. +# @RELATION BINDS_TO -> [TestAssistantAuthz] def test_limited_user_cannot_launch_restricted_operation(): _clear_assistant_state() response = _run_async( @@ -356,5 +344,5 @@ def test_limited_user_cannot_launch_restricted_operation(): assert response.state == "denied" -# [/DEF:test_limited_user_cannot_launch_restricted_operation:Function] -# [/DEF:TestAssistantAuthz:Module] +# #endregion test_limited_user_cannot_launch_restricted_operation +# #endregion TestAssistantAuthz diff --git a/backend/src/api/routes/__tests__/test_clean_release_api.py b/backend/src/api/routes/__tests__/test_clean_release_api.py index 8c0bb70b..bbdbe570 100644 --- a/backend/src/api/routes/__tests__/test_clean_release_api.py +++ b/backend/src/api/routes/__tests__/test_clean_release_api.py @@ -1,10 +1,8 @@ -# [DEF:TestCleanReleaseApi:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, api, clean-release, checks, reports -# @PURPOSE: Contract tests for clean release checks and reports endpoints. -# @LAYER: Domain -# @INVARIANT: API returns deterministic payload shapes for checks and reports. +# #region TestCleanReleaseApi [C:3] [TYPE Module] [SEMANTICS tests, api, clean-release, checks, reports] +# @BRIEF Contract tests for clean release checks and reports endpoints. +# @LAYER Domain +# @INVARIANT API returns deterministic payload shapes for checks and reports. +# @RELATION BELONGS_TO -> [SrcRoot] from datetime import datetime, timezone @@ -25,8 +23,8 @@ from src.models.clean_release import ( from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:_repo_with_seed_data:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseApi +# #region _repo_with_seed_data [TYPE Function] +# @RELATION BINDS_TO -> [TestCleanReleaseApi] def _repo_with_seed_data() -> CleanReleaseRepository: repo = CleanReleaseRepository() repo.save_candidate( @@ -74,12 +72,12 @@ def _repo_with_seed_data() -> CleanReleaseRepository: return repo -# [/DEF:_repo_with_seed_data:Function] +# #endregion _repo_with_seed_data -# [DEF:test_start_check_and_get_status_contract:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseApi -# @PURPOSE: Validate checks start endpoint returns expected identifiers and status endpoint reflects the same run. +# #region test_start_check_and_get_status_contract [TYPE Function] +# @BRIEF Validate checks start endpoint returns expected identifiers and status endpoint reflects the same run. +# @RELATION BINDS_TO -> [TestCleanReleaseApi] def test_start_check_and_get_status_contract(): repo = _repo_with_seed_data() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -112,12 +110,12 @@ def test_start_check_and_get_status_contract(): app.dependency_overrides.clear() -# [/DEF:test_start_check_and_get_status_contract:Function] +# #endregion test_start_check_and_get_status_contract -# [DEF:test_get_report_not_found_returns_404:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseApi -# @PURPOSE: Validate reports endpoint returns 404 for an unknown report identifier. +# #region test_get_report_not_found_returns_404 [TYPE Function] +# @BRIEF Validate reports endpoint returns 404 for an unknown report identifier. +# @RELATION BINDS_TO -> [TestCleanReleaseApi] def test_get_report_not_found_returns_404(): repo = _repo_with_seed_data() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -129,12 +127,12 @@ def test_get_report_not_found_returns_404(): app.dependency_overrides.clear() -# [/DEF:test_get_report_not_found_returns_404:Function] +# #endregion test_get_report_not_found_returns_404 -# [DEF:test_get_report_success:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseApi -# @PURPOSE: Validate reports endpoint returns persisted report payload for an existing report identifier. +# #region test_get_report_success [TYPE Function] +# @BRIEF Validate reports endpoint returns persisted report payload for an existing report identifier. +# @RELATION BINDS_TO -> [TestCleanReleaseApi] def test_get_report_success(): repo = _repo_with_seed_data() report = ComplianceReport( @@ -159,12 +157,12 @@ def test_get_report_success(): app.dependency_overrides.clear() -# [/DEF:test_get_report_success:Function] +# #endregion test_get_report_success -# [DEF:test_prepare_candidate_api_success:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseApi -# @PURPOSE: Validate candidate preparation endpoint returns prepared status and manifest identifier on valid input. +# #region test_prepare_candidate_api_success [TYPE Function] +# @BRIEF Validate candidate preparation endpoint returns prepared status and manifest identifier on valid input. +# @RELATION BINDS_TO -> [TestCleanReleaseApi] def test_prepare_candidate_api_success(): repo = _repo_with_seed_data() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -189,5 +187,5 @@ def test_prepare_candidate_api_success(): app.dependency_overrides.clear() -# [/DEF:test_prepare_candidate_api_success:Function] -# [/DEF:TestCleanReleaseApi:Module] +# #endregion test_prepare_candidate_api_success +# #endregion TestCleanReleaseApi diff --git a/backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py b/backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py index 90a02f0e..43767f99 100644 --- a/backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py +++ b/backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py @@ -1,8 +1,7 @@ -# [DEF:TestCleanReleaseLegacyCompat:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @PURPOSE: Compatibility tests for legacy clean-release API paths retained during v2 migration. -# @LAYER: Tests +# #region TestCleanReleaseLegacyCompat [C:3] [TYPE Module] +# @BRIEF Compatibility tests for legacy clean-release API paths retained during v2 migration. +# @LAYER Tests +# @RELATION BELONGS_TO -> [SrcRoot] from __future__ import annotations @@ -30,11 +29,11 @@ from src.models.clean_release import ( from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:_seed_legacy_repo:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat -# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts. -# @PRE: Repository is empty. -# @POST: Candidate, policy, registry and manifest are available for legacy checks flow. +# #region _seed_legacy_repo [TYPE Function] +# @BRIEF Seed in-memory repository with minimum trusted data for legacy endpoint contracts. +# @PRE Repository is empty. +# @POST Candidate, policy, registry and manifest are available for legacy checks flow. +# @RELATION BINDS_TO -> [TestCleanReleaseLegacyCompat] def _seed_legacy_repo() -> CleanReleaseRepository: repo = CleanReleaseRepository() now = datetime.now(timezone.utc) @@ -116,12 +115,12 @@ def _seed_legacy_repo() -> CleanReleaseRepository: return repo -# [/DEF:_seed_legacy_repo:Function] +# #endregion _seed_legacy_repo -# [DEF:test_legacy_prepare_endpoint_still_available:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat -# @PURPOSE: Verify legacy prepare endpoint remains reachable and returns a status payload. +# #region test_legacy_prepare_endpoint_still_available [TYPE Function] +# @BRIEF Verify legacy prepare endpoint remains reachable and returns a status payload. +# @RELATION BINDS_TO -> [TestCleanReleaseLegacyCompat] def test_legacy_prepare_endpoint_still_available() -> None: repo = _seed_legacy_repo() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -146,12 +145,12 @@ def test_legacy_prepare_endpoint_still_available() -> None: app.dependency_overrides.clear() -# [/DEF:test_legacy_prepare_endpoint_still_available:Function] +# #endregion test_legacy_prepare_endpoint_still_available -# [DEF:test_legacy_checks_endpoints_still_available:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat -# @PURPOSE: Verify legacy checks start/status endpoints remain available during v2 transition. +# #region test_legacy_checks_endpoints_still_available [TYPE Function] +# @BRIEF Verify legacy checks start/status endpoints remain available during v2 transition. +# @RELATION BINDS_TO -> [TestCleanReleaseLegacyCompat] def test_legacy_checks_endpoints_still_available() -> None: repo = _seed_legacy_repo() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -183,5 +182,5 @@ def test_legacy_checks_endpoints_still_available() -> None: app.dependency_overrides.clear() -# [/DEF:test_legacy_checks_endpoints_still_available:Function] -# [/DEF:TestCleanReleaseLegacyCompat:Module] +# #endregion test_legacy_checks_endpoints_still_available +# #endregion TestCleanReleaseLegacyCompat diff --git a/backend/src/api/routes/__tests__/test_clean_release_source_policy.py b/backend/src/api/routes/__tests__/test_clean_release_source_policy.py index 4544bf6f..298876eb 100644 --- a/backend/src/api/routes/__tests__/test_clean_release_source_policy.py +++ b/backend/src/api/routes/__tests__/test_clean_release_source_policy.py @@ -1,10 +1,8 @@ -# [DEF:TestCleanReleaseSourcePolicy:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, api, clean-release, source-policy -# @PURPOSE: Validate API behavior for source isolation violations in clean release preparation. -# @LAYER: Domain -# @INVARIANT: External endpoints must produce blocking violation entries. +# #region TestCleanReleaseSourcePolicy [C:3] [TYPE Module] [SEMANTICS tests, api, clean-release, source-policy] +# @BRIEF Validate API behavior for source isolation violations in clean release preparation. +# @LAYER Domain +# @INVARIANT External endpoints must produce blocking violation entries. +# @RELATION BELONGS_TO -> [SrcRoot] from datetime import datetime, timezone from fastapi.testclient import TestClient @@ -22,9 +20,9 @@ from src.models.clean_release import ( from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:_repo_with_seed_data:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy -# @PURPOSE: Seed repository with candidate, registry, and active policy for source isolation test flow. +# #region _repo_with_seed_data [TYPE Function] +# @BRIEF Seed repository with candidate, registry, and active policy for source isolation test flow. +# @RELATION BINDS_TO -> [TestCleanReleaseSourcePolicy] def _repo_with_seed_data() -> CleanReleaseRepository: repo = CleanReleaseRepository() @@ -75,12 +73,12 @@ def _repo_with_seed_data() -> CleanReleaseRepository: return repo -# [/DEF:_repo_with_seed_data:Function] +# #endregion _repo_with_seed_data -# [DEF:test_prepare_candidate_blocks_external_source:Function] -# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy -# @PURPOSE: Verify candidate preparation is blocked when at least one source host is external to the trusted registry. +# #region test_prepare_candidate_blocks_external_source [TYPE Function] +# @BRIEF Verify candidate preparation is blocked when at least one source host is external to the trusted registry. +# @RELATION BINDS_TO -> [TestCleanReleaseSourcePolicy] def test_prepare_candidate_blocks_external_source(): repo = _repo_with_seed_data() app.dependency_overrides[get_clean_release_repository] = lambda: repo @@ -110,5 +108,5 @@ def test_prepare_candidate_blocks_external_source(): app.dependency_overrides.clear() -# [/DEF:test_prepare_candidate_blocks_external_source:Function] -# [/DEF:TestCleanReleaseSourcePolicy:Module] +# #endregion test_prepare_candidate_blocks_external_source +# #endregion TestCleanReleaseSourcePolicy diff --git a/backend/src/api/routes/__tests__/test_clean_release_v2_api.py b/backend/src/api/routes/__tests__/test_clean_release_v2_api.py index afc7bed7..d5b233e7 100644 --- a/backend/src/api/routes/__tests__/test_clean_release_v2_api.py +++ b/backend/src/api/routes/__tests__/test_clean_release_v2_api.py @@ -1,8 +1,7 @@ -# [DEF:CleanReleaseV2ApiTests:Module] -# @COMPLEXITY: 3 -# @PURPOSE: API contract tests for redesigned clean release endpoints. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api] +# #region CleanReleaseV2ApiTests [C:3] [TYPE Module] +# @BRIEF API contract tests for redesigned clean release endpoints. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [CleanReleaseV2Api] from datetime import datetime, timezone from types import SimpleNamespace @@ -25,9 +24,9 @@ client = TestClient(app) # [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012). -# [DEF:test_candidate_registration_contract:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests -# @PURPOSE: Validate candidate registration endpoint creates a draft candidate with expected identifier contract. +# #region test_candidate_registration_contract [TYPE Function] +# @BRIEF Validate candidate registration endpoint creates a draft candidate with expected identifier contract. +# @RELATION BINDS_TO -> [CleanReleaseV2ApiTests] def test_candidate_registration_contract(): """ @TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO. @@ -46,12 +45,12 @@ def test_candidate_registration_contract(): assert data["status"] == CandidateStatus.DRAFT.value -# [/DEF:test_candidate_registration_contract:Function] +# #endregion test_candidate_registration_contract -# [DEF:test_artifact_import_contract:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests -# @PURPOSE: Validate artifact import endpoint accepts candidate artifacts and returns success status payload. +# #region test_artifact_import_contract [TYPE Function] +# @BRIEF Validate artifact import endpoint accepts candidate artifacts and returns success status payload. +# @RELATION BINDS_TO -> [CleanReleaseV2ApiTests] def test_artifact_import_contract(): """ @TEST_SCENARIO: artifact_import -> Should return 200 and success status. @@ -81,12 +80,12 @@ def test_artifact_import_contract(): assert response.json()["status"] == "success" -# [/DEF:test_artifact_import_contract:Function] +# #endregion test_artifact_import_contract -# [DEF:test_manifest_build_contract:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests -# @PURPOSE: Validate manifest build endpoint produces manifest payload linked to the target candidate. +# #region test_manifest_build_contract [TYPE Function] +# @BRIEF Validate manifest build endpoint produces manifest payload linked to the target candidate. +# @RELATION BINDS_TO -> [CleanReleaseV2ApiTests] def test_manifest_build_contract(): """ @TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO. @@ -111,5 +110,5 @@ def test_manifest_build_contract(): assert data["candidate_id"] == candidate_id -# [/DEF:test_manifest_build_contract:Function] -# [/DEF:CleanReleaseV2ApiTests:Module] +# #endregion test_manifest_build_contract +# #endregion CleanReleaseV2ApiTests diff --git a/backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py b/backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py index 11a7441a..66127e1e 100644 --- a/backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py +++ b/backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py @@ -1,8 +1,7 @@ -# [DEF:CleanReleaseV2ReleaseApiTests:Module] -# @COMPLEXITY: 3 -# @PURPOSE: API contract test scaffolding for clean release approval and publication endpoints. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api] +# #region CleanReleaseV2ReleaseApiTests [C:3] [TYPE Module] +# @BRIEF API contract test scaffolding for clean release approval and publication endpoints. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [CleanReleaseV2Api] """Contract tests for redesigned approval/publication API endpoints.""" @@ -23,9 +22,9 @@ test_app.include_router(clean_release_v2_router) client = TestClient(test_app) -# [DEF:_seed_candidate_and_passed_report:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests -# @PURPOSE: Seed repository with approvable candidate and passed report for release endpoint contracts. +# #region _seed_candidate_and_passed_report [TYPE Function] +# @BRIEF Seed repository with approvable candidate and passed report for release endpoint contracts. +# @RELATION BINDS_TO -> [CleanReleaseV2ReleaseApiTests] def _seed_candidate_and_passed_report() -> tuple[str, str]: repository = get_clean_release_repository() candidate_id = f"api-release-candidate-{uuid4()}" @@ -59,12 +58,12 @@ def _seed_candidate_and_passed_report() -> tuple[str, str]: return candidate_id, report_id -# [/DEF:_seed_candidate_and_passed_report:Function] +# #endregion _seed_candidate_and_passed_report -# [DEF:test_release_approve_and_publish_revoke_contract:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests -# @PURPOSE: Verify approve, publish, and revoke endpoints preserve expected release lifecycle contract. +# #region test_release_approve_and_publish_revoke_contract [TYPE Function] +# @BRIEF Verify approve, publish, and revoke endpoints preserve expected release lifecycle contract. +# @RELATION BINDS_TO -> [CleanReleaseV2ReleaseApiTests] def test_release_approve_and_publish_revoke_contract() -> None: """Contract for approve -> publish -> revoke lifecycle endpoints.""" candidate_id, report_id = _seed_candidate_and_passed_report() @@ -103,12 +102,12 @@ def test_release_approve_and_publish_revoke_contract() -> None: assert revoke_payload["publication"]["status"] == "REVOKED" -# [/DEF:test_release_approve_and_publish_revoke_contract:Function] +# #endregion test_release_approve_and_publish_revoke_contract -# [DEF:test_release_reject_contract:Function] -# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests -# @PURPOSE: Verify reject endpoint returns successful rejection decision payload. +# #region test_release_reject_contract [TYPE Function] +# @BRIEF Verify reject endpoint returns successful rejection decision payload. +# @RELATION BINDS_TO -> [CleanReleaseV2ReleaseApiTests] def test_release_reject_contract() -> None: """Contract for reject endpoint.""" candidate_id, report_id = _seed_candidate_and_passed_report() @@ -123,5 +122,5 @@ def test_release_reject_contract() -> None: assert payload["decision"] == "REJECTED" -# [/DEF:test_release_reject_contract:Function] -# [/DEF:CleanReleaseV2ReleaseApiTests:Module] +# #endregion test_release_reject_contract +# #endregion CleanReleaseV2ReleaseApiTests diff --git a/backend/src/api/routes/__tests__/test_connections_routes.py b/backend/src/api/routes/__tests__/test_connections_routes.py index 3e352d64..9f7082e1 100644 --- a/backend/src/api/routes/__tests__/test_connections_routes.py +++ b/backend/src/api/routes/__tests__/test_connections_routes.py @@ -1,8 +1,7 @@ -# [DEF:ConnectionsRoutesTests:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Verifies connection routes bootstrap their table before CRUD access. -# @LAYER: API -# @RELATION: DEPENDS_ON -> ConnectionsRouter +# #region ConnectionsRoutesTests [C:3] [TYPE Module] +# @BRIEF Verifies connection routes bootstrap their table before CRUD access. +# @LAYER API +# @RELATION DEPENDS_ON -> [ConnectionsRouter] import os import sys @@ -43,9 +42,9 @@ def db_session(): session.close() -# [DEF:test_list_connections_bootstraps_missing_table:Function] -# @RELATION: BINDS_TO -> ConnectionsRoutesTests -# @PURPOSE: Ensure listing connections auto-creates missing table and returns empty payload. +# #region test_list_connections_bootstraps_missing_table [TYPE Function] +# @BRIEF Ensure listing connections auto-creates missing table and returns empty payload. +# @RELATION BINDS_TO -> [ConnectionsRoutesTests] def test_list_connections_bootstraps_missing_table(db_session): from src.api.routes.connections import list_connections @@ -56,12 +55,12 @@ def test_list_connections_bootstraps_missing_table(db_session): assert "connection_configs" in inspector.get_table_names() -# [/DEF:test_list_connections_bootstraps_missing_table:Function] +# #endregion test_list_connections_bootstraps_missing_table -# [DEF:test_create_connection_bootstraps_missing_table:Function] -# @RELATION: BINDS_TO -> ConnectionsRoutesTests -# @PURPOSE: Ensure connection creation bootstraps table and persists returned connection fields. +# #region test_create_connection_bootstraps_missing_table [TYPE Function] +# @BRIEF Ensure connection creation bootstraps table and persists returned connection fields. +# @RELATION BINDS_TO -> [ConnectionsRoutesTests] def test_create_connection_bootstraps_missing_table(db_session): from src.api.routes.connections import ConnectionCreate, create_connection @@ -83,5 +82,5 @@ def test_create_connection_bootstraps_missing_table(db_session): assert "connection_configs" in inspector.get_table_names() -# [/DEF:test_create_connection_bootstraps_missing_table:Function] -# [/DEF:ConnectionsRoutesTests:Module] +# #endregion test_create_connection_bootstraps_missing_table +# #endregion ConnectionsRoutesTests diff --git a/backend/src/api/routes/__tests__/test_dashboards.py b/backend/src/api/routes/__tests__/test_dashboards.py index e6a6231d..6e52a743 100644 --- a/backend/src/api/routes/__tests__/test_dashboards.py +++ b/backend/src/api/routes/__tests__/test_dashboards.py @@ -1,8 +1,7 @@ -# [DEF:DashboardsApiTests:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for dashboards API endpoints. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DashboardsApi] +# #region DashboardsApiTests [C:3] [TYPE Module] +# @BRIEF Unit tests for dashboards API endpoints. +# @LAYER API +# @RELATION DEPENDS_ON -> [DashboardsApi] import pytest from unittest.mock import MagicMock, patch, AsyncMock @@ -71,9 +70,9 @@ def mock_deps(): client = TestClient(app) -# [DEF:test_get_dashboards_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing returns a populated response that satisfies the schema contract. +# #region test_get_dashboards_success [TYPE Function] +# @BRIEF Validate dashboards listing returns a populated response that satisfies the schema contract. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards returns 200 and valid schema # @PRE: env_id exists # @POST: Response matches DashboardsResponse schema @@ -110,12 +109,12 @@ def test_get_dashboards_success(mock_deps): DashboardsResponse(**data) -# [/DEF:test_get_dashboards_success:Function] +# #endregion test_get_dashboards_success -# [DEF:test_get_dashboards_with_search:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing applies the search filter and returns only matching rows. +# #region test_get_dashboards_with_search [TYPE Function] +# @BRIEF Validate dashboards listing applies the search filter and returns only matching rows. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards filters by search term # @PRE: search parameter provided # @POST: Only matching dashboards returned @@ -156,12 +155,12 @@ def test_get_dashboards_with_search(mock_deps): assert data["dashboards"][0]["title"] == "Sales Report" -# [/DEF:test_get_dashboards_with_search:Function] +# #endregion test_get_dashboards_with_search -# [DEF:test_get_dashboards_empty:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing returns an empty payload for an environment without dashboards. +# #region test_get_dashboards_empty [TYPE Function] +# @BRIEF Validate dashboards listing returns an empty payload for an environment without dashboards. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0} def test_get_dashboards_empty(mock_deps): """@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}""" @@ -180,12 +179,12 @@ def test_get_dashboards_empty(mock_deps): DashboardsResponse(**data) -# [/DEF:test_get_dashboards_empty:Function] +# #endregion test_get_dashboards_empty -# [DEF:test_get_dashboards_superset_failure:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing surfaces a 503 contract when Superset access fails. +# #region test_get_dashboards_superset_failure [TYPE Function] +# @BRIEF Validate dashboards listing surfaces a 503 contract when Superset access fails. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503} def test_get_dashboards_superset_failure(mock_deps): """@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}""" @@ -202,12 +201,12 @@ def test_get_dashboards_superset_failure(mock_deps): assert "Failed to fetch dashboards" in response.json()["detail"] -# [/DEF:test_get_dashboards_superset_failure:Function] +# #endregion test_get_dashboards_superset_failure -# [DEF:test_get_dashboards_env_not_found:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing returns 404 when the requested environment does not exist. +# #region test_get_dashboards_env_not_found [TYPE Function] +# @BRIEF Validate dashboards listing returns 404 when the requested environment does not exist. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards returns 404 if env_id missing # @PRE: env_id does not exist # @POST: Returns 404 error @@ -219,12 +218,12 @@ def test_get_dashboards_env_not_found(mock_deps): assert "Environment not found" in response.json()["detail"] -# [/DEF:test_get_dashboards_env_not_found:Function] +# #endregion test_get_dashboards_env_not_found -# [DEF:test_get_dashboards_invalid_pagination:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboards listing rejects invalid pagination parameters with 400 responses. +# #region test_get_dashboards_invalid_pagination [TYPE Function] +# @BRIEF Validate dashboards listing rejects invalid pagination parameters with 400 responses. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards returns 400 for invalid page/page_size # @PRE: page < 1 or page_size > 100 # @POST: Returns 400 error @@ -243,12 +242,12 @@ def test_get_dashboards_invalid_pagination(mock_deps): assert "Page size must be between 1 and 100" in response.json()["detail"] -# [/DEF:test_get_dashboards_invalid_pagination:Function] +# #endregion test_get_dashboards_invalid_pagination -# [DEF:test_get_dashboard_detail_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboard detail returns charts and datasets for an existing dashboard. +# #region test_get_dashboard_detail_success [TYPE Function] +# @BRIEF Validate dashboard detail returns charts and datasets for an existing dashboard. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets def test_get_dashboard_detail_success(mock_deps): with patch("src.api.routes.dashboards._detail_routes.SupersetClient") as mock_client_cls: @@ -299,12 +298,12 @@ def test_get_dashboard_detail_success(mock_deps): assert payload["dataset_count"] == 1 -# [/DEF:test_get_dashboard_detail_success:Function] +# #endregion test_get_dashboard_detail_success -# [DEF:test_get_dashboard_detail_env_not_found:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboard detail returns 404 when the requested environment is missing. +# #region test_get_dashboard_detail_env_not_found [TYPE Function] +# @BRIEF Validate dashboard detail returns 404 when the requested environment is missing. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards/{id} returns 404 for missing environment def test_get_dashboard_detail_env_not_found(mock_deps): mock_deps["config"].get_environments.return_value = [] @@ -315,11 +314,11 @@ def test_get_dashboard_detail_env_not_found(mock_deps): assert "Environment not found" in response.json()["detail"] -# [/DEF:test_get_dashboard_detail_env_not_found:Function] +# #endregion test_get_dashboard_detail_env_not_found -# [DEF:test_migrate_dashboards_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_migrate_dashboards_success [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: POST /api/dashboards/migrate creates migration task # @PRE: Valid source_env_id, target_env_id, dashboard_ids # @PURPOSE: Validate dashboard migration request creates an async task and returns its identifier. @@ -352,11 +351,11 @@ def test_migrate_dashboards_success(mock_deps): mock_deps["task"].create_task.assert_called_once() -# [/DEF:test_migrate_dashboards_success:Function] +# #endregion test_migrate_dashboards_success -# [DEF:test_migrate_dashboards_no_ids:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_migrate_dashboards_no_ids [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids # @PRE: dashboard_ids is empty # @PURPOSE: Validate dashboard migration rejects empty dashboard identifier lists. @@ -375,13 +374,13 @@ def test_migrate_dashboards_no_ids(mock_deps): assert "At least one dashboard ID must be provided" in response.json()["detail"] -# [/DEF:test_migrate_dashboards_no_ids:Function] +# #endregion test_migrate_dashboards_no_ids -# [DEF:test_migrate_dashboards_env_not_found:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate migration creation returns 404 when the source environment cannot be resolved. -# @PRE: source_env_id and target_env_id are valid environment IDs +# #region test_migrate_dashboards_env_not_found [TYPE Function] +# @BRIEF Validate migration creation returns 404 when the source environment cannot be resolved. +# @PRE source_env_id and target_env_id are valid environment IDs +# @RELATION BINDS_TO -> [DashboardsApiTests] def test_migrate_dashboards_env_not_found(mock_deps): """@PRE: source_env_id and target_env_id are valid environment IDs.""" mock_deps["config"].get_environments.return_value = [] @@ -393,11 +392,11 @@ def test_migrate_dashboards_env_not_found(mock_deps): assert "Source environment not found" in response.json()["detail"] -# [/DEF:test_migrate_dashboards_env_not_found:Function] +# #endregion test_migrate_dashboards_env_not_found -# [DEF:test_backup_dashboards_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_backup_dashboards_success [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: POST /api/dashboards/backup creates backup task # @PRE: Valid env_id, dashboard_ids # @PURPOSE: Validate dashboard backup request creates an async backup task and returns its identifier. @@ -423,13 +422,13 @@ def test_backup_dashboards_success(mock_deps): mock_deps["task"].create_task.assert_called_once() -# [/DEF:test_backup_dashboards_success:Function] +# #endregion test_backup_dashboards_success -# [DEF:test_backup_dashboards_env_not_found:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate backup task creation returns 404 when the target environment is missing. -# @PRE: env_id is a valid environment ID +# #region test_backup_dashboards_env_not_found [TYPE Function] +# @BRIEF Validate backup task creation returns 404 when the target environment is missing. +# @PRE env_id is a valid environment ID +# @RELATION BINDS_TO -> [DashboardsApiTests] def test_backup_dashboards_env_not_found(mock_deps): """@PRE: env_id is a valid environment ID.""" mock_deps["config"].get_environments.return_value = [] @@ -440,11 +439,11 @@ def test_backup_dashboards_env_not_found(mock_deps): assert "Environment not found" in response.json()["detail"] -# [/DEF:test_backup_dashboards_env_not_found:Function] +# #endregion test_backup_dashboards_env_not_found -# [DEF:test_get_database_mappings_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_database_mappings_success [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards/db-mappings returns mapping suggestions # @PRE: Valid source_env_id, target_env_id # @PURPOSE: Validate database mapping suggestions are returned for valid source and target environments. @@ -479,13 +478,13 @@ def test_get_database_mappings_success(mock_deps): assert data["mappings"][0]["confidence"] == 0.95 -# [/DEF:test_get_database_mappings_success:Function] +# #endregion test_get_database_mappings_success -# [DEF:test_get_database_mappings_env_not_found:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate database mapping suggestions return 404 when either environment is missing. -# @PRE: source_env_id and target_env_id are valid environment IDs +# #region test_get_database_mappings_env_not_found [TYPE Function] +# @BRIEF Validate database mapping suggestions return 404 when either environment is missing. +# @PRE source_env_id and target_env_id are valid environment IDs +# @RELATION BINDS_TO -> [DashboardsApiTests] def test_get_database_mappings_env_not_found(mock_deps): """@PRE: source_env_id must be a valid environment.""" mock_deps["config"].get_environments.return_value = [] @@ -495,12 +494,12 @@ def test_get_database_mappings_env_not_found(mock_deps): assert response.status_code == 404 -# [/DEF:test_get_database_mappings_env_not_found:Function] +# #endregion test_get_database_mappings_env_not_found -# [DEF:test_get_dashboard_tasks_history_filters_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboard task history returns only related backup and LLM tasks. +# #region test_get_dashboard_tasks_history_filters_success [TYPE Function] +# @BRIEF Validate dashboard task history returns only related backup and LLM tasks. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard def test_get_dashboard_tasks_history_filters_success(mock_deps): now = datetime.now(timezone.utc) @@ -546,12 +545,12 @@ def test_get_dashboard_tasks_history_filters_success(mock_deps): } -# [/DEF:test_get_dashboard_tasks_history_filters_success:Function] +# #endregion test_get_dashboard_tasks_history_filters_success -# [DEF:test_get_dashboard_thumbnail_success:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset. +# #region test_get_dashboard_thumbnail_success [TYPE Function] +# @BRIEF Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset. +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset def test_get_dashboard_thumbnail_success(mock_deps): with patch("src.api.routes.dashboards._detail_routes.SupersetClient") as mock_client_cls: @@ -580,14 +579,14 @@ def test_get_dashboard_thumbnail_success(mock_deps): assert response.headers["content-type"].startswith("image/png") -# [/DEF:test_get_dashboard_thumbnail_success:Function] +# #endregion test_get_dashboard_thumbnail_success -# [DEF:_build_profile_preference_stub:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Creates profile preference payload stub for dashboards filter contract tests. -# @PRE: username can be empty; enabled indicates profile-default toggle state. -# @POST: Returns object compatible with ProfileService.get_my_preference contract. +# #region _build_profile_preference_stub [TYPE Function] +# @BRIEF Creates profile preference payload stub for dashboards filter contract tests. +# @PRE username can be empty; enabled indicates profile-default toggle state. +# @POST Returns object compatible with ProfileService.get_my_preference contract. +# @RELATION BINDS_TO -> [DashboardsApiTests] def _build_profile_preference_stub(username: str, enabled: bool): preference = MagicMock() preference.superset_username = username @@ -601,14 +600,14 @@ def _build_profile_preference_stub(username: str, enabled: bool): return payload -# [/DEF:_build_profile_preference_stub:Function] +# #endregion _build_profile_preference_stub -# [DEF:_matches_actor_case_insensitive:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests -# @PURPOSE: Applies trim + case-insensitive owners OR modified_by matching used by route contract tests. -# @PRE: owners can be None or list-like values. -# @POST: Returns True when bound username matches any owner or modified_by. +# #region _matches_actor_case_insensitive [TYPE Function] +# @BRIEF Applies trim + case-insensitive owners OR modified_by matching used by route contract tests. +# @PRE owners can be None or list-like values. +# @POST Returns True when bound username matches any owner or modified_by. +# @RELATION BINDS_TO -> [DashboardsApiTests] def _matches_actor_case_insensitive(bound_username, owners, modified_by): normalized_bound = str(bound_username or "").strip().lower() if not normalized_bound: @@ -626,11 +625,11 @@ def _matches_actor_case_insensitive(bound_username, owners, modified_by): ) -# [/DEF:_matches_actor_case_insensitive:Function] +# #endregion _matches_actor_case_insensitive -# [DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_profile_filter_contract_owners_or_modified_by [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics. # @PURPOSE: Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values. # @PRE: Current user has enabled profile-default preference and bound username. @@ -693,11 +692,11 @@ def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps) assert payload["effective_profile_filter"]["match_logic"] == "owners_or_modified_by" -# [/DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function] +# #endregion test_get_dashboards_profile_filter_contract_owners_or_modified_by -# [DEF:test_get_dashboards_override_show_all_contract:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_override_show_all_contract [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page. # @PURPOSE: Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics. # @PRE: Profile-default preference exists but override_show_all=true query is provided. @@ -754,11 +753,11 @@ def test_get_dashboards_override_show_all_contract(mock_deps): profile_service.matches_dashboard_actor.assert_not_called() -# [/DEF:test_get_dashboards_override_show_all_contract:Function] +# #endregion test_get_dashboards_override_show_all_contract -# [DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_profile_filter_no_match_results_contract [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match. # @PURPOSE: Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user. # @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching. @@ -817,11 +816,11 @@ def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps): assert payload["effective_profile_filter"]["match_logic"] == "owners_or_modified_by" -# [/DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function] +# #endregion test_get_dashboards_profile_filter_no_match_results_contract -# [DEF:test_get_dashboards_page_context_other_disables_profile_default:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_page_context_other_disables_profile_default [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context. # @PURPOSE: Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results. # @PRE: Profile-default preference exists but page_context=other query is provided. @@ -878,11 +877,11 @@ def test_get_dashboards_page_context_other_disables_profile_default(mock_deps): profile_service.matches_dashboard_actor.assert_not_called() -# [/DEF:test_get_dashboards_page_context_other_disables_profile_default:Function] +# #endregion test_get_dashboards_page_context_other_disables_profile_default -# [DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls. # @PURPOSE: Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout. # @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels. @@ -963,11 +962,11 @@ def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fano superset_client.get_dashboard.assert_not_called() -# [/DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function] +# #endregion test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout -# [DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function] -# @RELATION: BINDS_TO -> DashboardsApiTests +# #region test_get_dashboards_profile_filter_matches_owner_object_payload_contract [TYPE Function] +# @RELATION BINDS_TO -> [DashboardsApiTests] # @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads. # @PURPOSE: Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username. # @PRE: Profile-default preference is enabled and owners list contains dict payloads. @@ -1045,7 +1044,7 @@ def test_get_dashboards_profile_filter_matches_owner_object_payload_contract(moc assert payload["dashboards"][0]["title"] == "Featured Charts" -# [/DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function] +# #endregion test_get_dashboards_profile_filter_matches_owner_object_payload_contract -# [/DEF:DashboardsApiTests:Module] +# #endregion DashboardsApiTests diff --git a/backend/src/api/routes/__tests__/test_dataset_review_api.py b/backend/src/api/routes/__tests__/test_dataset_review_api.py index d86456d4..4c52982d 100644 --- a/backend/src/api/routes/__tests__/test_dataset_review_api.py +++ b/backend/src/api/routes/__tests__/test_dataset_review_api.py @@ -1,10 +1,8 @@ -# [DEF:DatasetReviewApiTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: dataset_review, api, tests, lifecycle, exports, orchestration -# @PURPOSE: Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts. -# @LAYER: API -# @RELATION: [BINDS_TO] ->[DatasetReviewApi] -# @RELATION: [BINDS_TO] ->[DatasetReviewOrchestrator] +# #region DatasetReviewApiTests [C:3] [TYPE Module] [SEMANTICS dataset_review, api, tests, lifecycle, exports, orchestration] +# @BRIEF Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts. +# @LAYER API +# @RELATION BINDS_TO -> [DatasetReviewApi] +# @RELATION BINDS_TO -> [DatasetReviewOrchestrator] from datetime import datetime, timezone import json @@ -73,8 +71,8 @@ from src.services.dataset_review.repositories.session_repository import ( client = TestClient(app) -# [DEF:_make_user:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_user [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_user(): permissions = [ SimpleNamespace(resource="dataset:session", action="READ"), @@ -87,11 +85,11 @@ def _make_user(): return SimpleNamespace(id="user-1", username="tester", roles=[dataset_review_role]) -# [/DEF:_make_user:Function] +# #endregion _make_user -# [DEF:_make_config_manager:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_config_manager [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_config_manager(): env = Environment( id="env-1", @@ -109,11 +107,11 @@ def _make_config_manager(): return manager -# [/DEF:_make_config_manager:Function] +# #endregion _make_config_manager -# [DEF:_make_session:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_session [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_session(): now = datetime.now(timezone.utc) return DatasetReviewSession( @@ -136,11 +134,11 @@ def _make_session(): ) -# [/DEF:_make_session:Function] +# #endregion _make_session -# [DEF:_make_us2_session:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_us2_session [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_us2_session(): now = datetime.now(timezone.utc) session = _make_session() @@ -254,11 +252,11 @@ def _make_us2_session(): return session -# [/DEF:_make_us2_session:Function] +# #endregion _make_us2_session -# [DEF:_make_us3_session:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_us3_session [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_us3_session(): """Fake session factory for US3 flow tests. @@ -324,11 +322,11 @@ def _make_us3_session(): return session -# [/DEF:_make_us3_session:Function] +# #endregion _make_us3_session -# [DEF:_make_preview_ready_session:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region _make_preview_ready_session [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def _make_preview_ready_session(): session = _make_us3_session() session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY @@ -337,11 +335,11 @@ def _make_preview_ready_session(): return session -# [/DEF:_make_preview_ready_session:Function] +# #endregion _make_preview_ready_session -# [DEF:dataset_review_api_dependencies:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests +# #region dataset_review_api_dependencies [TYPE Function] +# @RELATION BINDS_TO -> [DatasetReviewApiTests] @pytest.fixture(autouse=True) def dataset_review_api_dependencies(): mock_user = _make_user() @@ -360,12 +358,12 @@ def dataset_review_api_dependencies(): app.dependency_overrides.clear() -# [/DEF:dataset_review_api_dependencies:Function] +# #endregion dataset_review_api_dependencies -# [DEF:test_parse_superset_link_dashboard_partial_recovery:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify dashboard links recover dataset context and preserve explicit partial-recovery markers. +# #region test_parse_superset_link_dashboard_partial_recovery [TYPE Function] +# @BRIEF Verify dashboard links recover dataset context and preserve explicit partial-recovery markers. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_parse_superset_link_dashboard_partial_recovery(): env = Environment( id="env-1", @@ -397,12 +395,12 @@ def test_parse_superset_link_dashboard_partial_recovery(): assert result.imported_filters[0]["filter_name"] == "country" -# [/DEF:test_parse_superset_link_dashboard_partial_recovery:Function] +# #endregion test_parse_superset_link_dashboard_partial_recovery -# [DEF:test_parse_superset_link_dashboard_slug_recovery:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify dashboard slug links resolve through dashboard detail endpoints and recover dataset context. +# #region test_parse_superset_link_dashboard_slug_recovery [TYPE Function] +# @BRIEF Verify dashboard slug links resolve through dashboard detail endpoints and recover dataset context. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_parse_superset_link_dashboard_slug_recovery(): env = Environment( id="env-1", @@ -434,12 +432,12 @@ def test_parse_superset_link_dashboard_slug_recovery(): fake_client.get_dashboard_detail.assert_called_once_with("slack") -# [/DEF:test_parse_superset_link_dashboard_slug_recovery:Function] +# #endregion test_parse_superset_link_dashboard_slug_recovery -# [DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify dashboard permalink links no longer fail parsing and preserve permalink filter state for partial recovery. +# #region test_parse_superset_link_dashboard_permalink_partial_recovery [TYPE Function] +# @BRIEF Verify dashboard permalink links no longer fail parsing and preserve permalink filter state for partial recovery. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_parse_superset_link_dashboard_permalink_partial_recovery(): env = Environment( id="env-1", @@ -484,9 +482,9 @@ def test_parse_superset_link_dashboard_permalink_partial_recovery(): fake_client.get_dashboard_permalink_state.assert_called_once_with("QabXy6wG30Z") -# [DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters. +# #region test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state [TYPE Function] +# @BRIEF Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state(): env = Environment( id="env-1", @@ -528,13 +526,13 @@ def test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_da assert result.imported_filters[0]["filter_name"] == "country" -# [/DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function] -# [/DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function] +# #endregion test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state +# #endregion test_parse_superset_link_dashboard_permalink_partial_recovery -# [DEF:test_resolve_from_dictionary_prefers_exact_match:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify trusted dictionary exact matches outrank fuzzy candidates and unresolved fields stay explicit. +# #region test_resolve_from_dictionary_prefers_exact_match [TYPE Function] +# @BRIEF Verify trusted dictionary exact matches outrank fuzzy candidates and unresolved fields stay explicit. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_resolve_from_dictionary_prefers_exact_match(): resolver = SemanticSourceResolver() result = resolver.resolve_from_dictionary( @@ -574,12 +572,12 @@ def test_resolve_from_dictionary_prefers_exact_match(): assert result.partial_recovery is True -# [/DEF:test_resolve_from_dictionary_prefers_exact_match:Function] +# #endregion test_resolve_from_dictionary_prefers_exact_match -# [DEF:test_orchestrator_start_session_preserves_partial_recovery:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify session start persists usable recovery-required state when Superset intake is partial. +# #region test_orchestrator_start_session_preserves_partial_recovery [TYPE Function] +# @BRIEF Verify session start persists usable recovery-required state when Superset intake is partial. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_orchestrator_start_session_preserves_partial_recovery( dataset_review_api_dependencies, ): @@ -642,12 +640,12 @@ def test_orchestrator_start_session_preserves_partial_recovery( repository.save_profile_and_findings.assert_called_once() -# [/DEF:test_orchestrator_start_session_preserves_partial_recovery:Function] +# #endregion test_orchestrator_start_session_preserves_partial_recovery -# [DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify session start persists recovered filters, template variables, and initial execution mappings for review workspace bootstrap. +# #region test_orchestrator_start_session_bootstraps_recovery_state [TYPE Function] +# @BRIEF Verify session start persists recovered filters, template variables, and initial execution mappings for review workspace bootstrap. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_orchestrator_start_session_bootstraps_recovery_state( dataset_review_api_dependencies, ): @@ -750,12 +748,12 @@ def test_orchestrator_start_session_bootstraps_recovery_state( assert saved_mappings[0].raw_input_value == ["DE"] -# [/DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function] +# #endregion test_orchestrator_start_session_bootstraps_recovery_state -# [DEF:test_start_session_endpoint_returns_created_summary:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify POST session lifecycle endpoint returns a persisted ownership-scoped summary. +# #region test_start_session_endpoint_returns_created_summary [TYPE Function] +# @BRIEF Verify POST session lifecycle endpoint returns a persisted ownership-scoped summary. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_start_session_endpoint_returns_created_summary( dataset_review_api_dependencies, ): @@ -783,12 +781,12 @@ def test_start_session_endpoint_returns_created_summary( assert payload["environment_id"] == "env-1" -# [/DEF:test_start_session_endpoint_returns_created_summary:Function] +# #endregion test_start_session_endpoint_returns_created_summary -# [DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Verify lifecycle get/patch/delete plus documentation and validation exports remain ownership-scoped and usable. +# #region test_get_session_detail_export_and_lifecycle_endpoints [TYPE Function] +# @BRIEF Verify lifecycle get/patch/delete plus documentation and validation exports remain ownership-scoped and usable. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_get_session_detail_export_and_lifecycle_endpoints( dataset_review_api_dependencies, ): @@ -901,11 +899,11 @@ def test_get_session_detail_export_and_lifecycle_endpoints( assert delete_response.status_code == 204 -# [/DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function] +# #endregion test_get_session_detail_export_and_lifecycle_endpoints -# [DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function] -# @PURPOSE: Clarification state endpoint should return a non-blocking empty payload when the session has no clarification aggregate yet. +# #region test_get_clarification_state_returns_empty_payload_when_session_has_no_record [TYPE Function] +# @BRIEF Clarification state endpoint should return a non-blocking empty payload when the session has no clarification aggregate yet. def test_get_clarification_state_returns_empty_payload_when_session_has_no_record( dataset_review_api_dependencies, ): @@ -924,12 +922,12 @@ def test_get_clarification_state_returns_empty_payload_when_session_has_no_recor } -# [/DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function] +# #endregion test_get_clarification_state_returns_empty_payload_when_session_has_no_record -# [DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Clarification endpoints should expose one current question, persist the answer before advancement, and store feedback on the answer audit record. +# #region test_us2_clarification_endpoints_persist_answer_and_feedback [TYPE Function] +# @BRIEF Clarification endpoints should expose one current question, persist the answer before advancement, and store feedback on the answer audit record. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us2_clarification_endpoints_persist_answer_and_feedback( dataset_review_api_dependencies, ): @@ -991,12 +989,12 @@ def test_us2_clarification_endpoints_persist_answer_and_feedback( assert session.clarification_sessions[0].questions[0].answer.user_feedback == "up" -# [/DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function] +# #endregion test_us2_clarification_endpoints_persist_answer_and_feedback -# [DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Semantic field endpoints should apply manual overrides with lock/provenance invariants and persist feedback independently. +# #region test_us2_field_semantic_override_lock_unlock_and_feedback [TYPE Function] +# @BRIEF Semantic field endpoints should apply manual overrides with lock/provenance invariants and persist feedback independently. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us2_field_semantic_override_lock_unlock_and_feedback( dataset_review_api_dependencies, ): @@ -1073,12 +1071,12 @@ def test_us2_field_semantic_override_lock_unlock_and_feedback( assert session.semantic_fields[0].user_feedback == "down" -# [/DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function] +# #endregion test_us2_field_semantic_override_lock_unlock_and_feedback -# [DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: US3 execution endpoints should persist manual overrides, preserve explicit approval semantics, return contract-shaped preview truth, and expose audited launch handoff. +# #region test_us3_mapping_patch_approval_preview_and_launch_endpoints [TYPE Function] +# @BRIEF US3 execution endpoints should persist manual overrides, preserve explicit approval semantics, return contract-shaped preview truth, and expose audited launch handoff. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us3_mapping_patch_approval_preview_and_launch_endpoints( dataset_review_api_dependencies, ): @@ -1281,12 +1279,12 @@ def test_us3_mapping_patch_approval_preview_and_launch_endpoints( ) -# [/DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function] +# #endregion test_us3_mapping_patch_approval_preview_and_launch_endpoints -# [DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Preview response should expose the refreshed session version so the normal preview-then-launch UI flow can satisfy optimistic locking without a forced full reload. +# #region test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up [TYPE Function] +# @BRIEF Preview response should expose the refreshed session version so the normal preview-then-launch UI flow can satisfy optimistic locking without a forced full reload. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up( dataset_review_api_dependencies, ): @@ -1380,12 +1378,12 @@ def test_us3_preview_response_propagates_refreshed_session_version_for_launch_fo assert launch_response.json()["run_context"]["run_context_id"] == "run-2" -# [/DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function] +# #endregion test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up -# [DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Preview endpoint should preserve API contract and surface generic upstream preview failures without fabricating dashboard-not-found semantics for non-dashboard 404s. +# #region test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift [TYPE Function] +# @BRIEF Preview endpoint should preserve API contract and surface generic upstream preview failures without fabricating dashboard-not-found semantics for non-dashboard 404s. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift( dataset_review_api_dependencies, ): @@ -1434,12 +1432,12 @@ def test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not assert "Dashboard not found" not in payload["error_details"] -# [/DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function] +# #endregion test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift -# [DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Dataset review mutation endpoints should return deterministic 409 conflict semantics when optimistic-lock versions are stale. +# #region test_mutation_endpoints_surface_session_version_conflict_payload [TYPE Function] +# @BRIEF Dataset review mutation endpoints should return deterministic 409 conflict semantics when optimistic-lock versions are stale. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_mutation_endpoints_surface_session_version_conflict_payload( dataset_review_api_dependencies, ): @@ -1474,12 +1472,12 @@ def test_mutation_endpoints_surface_session_version_conflict_payload( assert payload["actual_version"] == 5 -# [/DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function] +# #endregion test_mutation_endpoints_surface_session_version_conflict_payload -# [DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Session lifecycle mutation should return deterministic 409 conflict semantics when commit-time optimistic locking rejects a stale write. +# #region test_update_session_surfaces_commit_time_session_version_conflict_payload [TYPE Function] +# @BRIEF Session lifecycle mutation should return deterministic 409 conflict semantics when commit-time optimistic locking rejects a stale write. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_update_session_surfaces_commit_time_session_version_conflict_payload( dataset_review_api_dependencies, ): @@ -1512,12 +1510,12 @@ def test_update_session_surfaces_commit_time_session_version_conflict_payload( assert payload["actual_version"] == 1 -# [/DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function] +# #endregion test_update_session_surfaces_commit_time_session_version_conflict_payload -# [DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Recovered imported filters with values should flow into preview filter context even when no template variable mapping exists. +# #region test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping [TYPE Function] +# @BRIEF Recovered imported filters with values should flow into preview filter context even when no template variable mapping exists. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping( dataset_review_api_dependencies, ): @@ -1585,12 +1583,12 @@ def test_execution_snapshot_includes_recovered_imported_filters_without_template ] -# [/DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function] +# #endregion test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping -# [DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Mapped template variables should still populate template params while contributing their effective filter context. +# #region test_execution_snapshot_preserves_mapped_template_variables_and_filter_context [TYPE Function] +# @BRIEF Mapped template variables should still populate template params while contributing their effective filter context. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_execution_snapshot_preserves_mapped_template_variables_and_filter_context( dataset_review_api_dependencies, ): @@ -1624,12 +1622,12 @@ def test_execution_snapshot_preserves_mapped_template_variables_and_filter_conte assert snapshot["open_warning_refs"] == ["map-1"] -# [/DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function] +# #endregion test_execution_snapshot_preserves_mapped_template_variables_and_filter_context -# [DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Partial imported filters without raw or normalized values must not emit bogus active preview filters. +# #region test_execution_snapshot_skips_partial_imported_filters_without_values [TYPE Function] +# @BRIEF Partial imported filters without raw or normalized values must not emit bogus active preview filters. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_execution_snapshot_skips_partial_imported_filters_without_values( dataset_review_api_dependencies, ): @@ -1666,12 +1664,12 @@ def test_execution_snapshot_skips_partial_imported_filters_without_values( assert snapshot["preview_blockers"] == [] -# [/DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function] +# #endregion test_execution_snapshot_skips_partial_imported_filters_without_values -# [DEF:test_us3_launch_endpoint_requires_launch_permission:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Launch endpoint should enforce the contract RBAC permission instead of the generic session-manage permission. +# #region test_us3_launch_endpoint_requires_launch_permission [TYPE Function] +# @BRIEF Launch endpoint should enforce the contract RBAC permission instead of the generic session-manage permission. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_us3_launch_endpoint_requires_launch_permission( dataset_review_api_dependencies, ): @@ -1722,12 +1720,12 @@ def test_us3_launch_endpoint_requires_launch_permission( ) -# [/DEF:test_us3_launch_endpoint_requires_launch_permission:Function] +# #endregion test_us3_launch_endpoint_requires_launch_permission -# [DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function] -# @RELATION: BINDS_TO -> DatasetReviewApiTests -# @PURPOSE: Updated semantic source versions should mark unlocked fields reviewable while preserving locked manual values. +# #region test_semantic_source_version_propagation_preserves_locked_fields [TYPE Function] +# @BRIEF Updated semantic source versions should mark unlocked fields reviewable while preserving locked manual values. +# @RELATION BINDS_TO -> [DatasetReviewApiTests] def test_semantic_source_version_propagation_preserves_locked_fields(): resolver = SemanticSourceResolver() source = SimpleNamespace(source_id="src-1", source_version="2026.04") @@ -1761,6 +1759,6 @@ def test_semantic_source_version_propagation_preserves_locked_fields(): assert locked_field.needs_review is False -# [/DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function] +# #endregion test_semantic_source_version_propagation_preserves_locked_fields -# [/DEF:DatasetReviewApiTests:Module] +# #endregion DatasetReviewApiTests diff --git a/backend/src/api/routes/__tests__/test_datasets.py b/backend/src/api/routes/__tests__/test_datasets.py index a2098901..9269d60a 100644 --- a/backend/src/api/routes/__tests__/test_datasets.py +++ b/backend/src/api/routes/__tests__/test_datasets.py @@ -1,10 +1,8 @@ -# [DEF:DatasetsApiTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: datasets, api, tests, pagination, mapping, docs -# @PURPOSE: Unit tests for datasets API endpoints. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DatasetsApi] -# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths. +# #region DatasetsApiTests [C:3] [TYPE Module] [SEMANTICS datasets, api, tests, pagination, mapping, docs] +# @BRIEF Unit tests for datasets API endpoints. +# @LAYER API +# @INVARIANT Endpoint contracts remain stable for success and validation failure paths. +# @RELATION DEPENDS_ON -> [DatasetsApi] import pytest from unittest.mock import MagicMock, patch, AsyncMock @@ -72,9 +70,9 @@ def mock_deps(): client = TestClient(app) -# [DEF:test_get_datasets_success:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate successful datasets listing contract for an existing environment. +# #region test_get_datasets_success [TYPE Function] +# @BRIEF Validate successful datasets listing contract for an existing environment. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: GET /api/datasets returns 200 and valid schema # @PRE: env_id exists # @POST: Response matches DatasetsResponse schema @@ -108,12 +106,12 @@ def test_get_datasets_success(mock_deps): DatasetsResponse(**data) -# [/DEF:test_get_datasets_success:Function] +# #endregion test_get_datasets_success -# [DEF:test_get_datasets_env_not_found:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate datasets listing returns 404 when the requested environment does not exist. +# #region test_get_datasets_env_not_found [TYPE Function] +# @BRIEF Validate datasets listing returns 404 when the requested environment does not exist. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: GET /api/datasets returns 404 if env_id missing # @PRE: env_id does not exist # @POST: Returns 404 error @@ -126,12 +124,12 @@ def test_get_datasets_env_not_found(mock_deps): assert "Environment not found" in response.json()["detail"] -# [/DEF:test_get_datasets_env_not_found:Function] +# #endregion test_get_datasets_env_not_found -# [DEF:test_get_datasets_invalid_pagination:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate datasets listing rejects invalid pagination parameters with 400 responses. +# #region test_get_datasets_invalid_pagination [TYPE Function] +# @BRIEF Validate datasets listing rejects invalid pagination parameters with 400 responses. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: GET /api/datasets returns 400 for invalid page/page_size # @PRE: page < 1 or page_size > 100 # @POST: Returns 400 error @@ -156,12 +154,12 @@ def test_get_datasets_invalid_pagination(mock_deps): assert "Page size must be between 1 and 100" in response.json()["detail"] -# [/DEF:test_get_datasets_invalid_pagination:Function] +# #endregion test_get_datasets_invalid_pagination -# [DEF:test_map_columns_success:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate map-columns request creates an async mapping task and returns its identifier. +# #region test_map_columns_success [TYPE Function] +# @BRIEF Validate map-columns request creates an async mapping task and returns its identifier. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/map-columns creates mapping task # @PRE: Valid env_id, dataset_ids, source_type # @POST: Returns task_id @@ -188,12 +186,12 @@ def test_map_columns_success(mock_deps): mock_deps["task"].create_task.assert_called_once() -# [/DEF:test_map_columns_success:Function] +# #endregion test_map_columns_success -# [DEF:test_map_columns_invalid_source_type:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate map-columns rejects unsupported source types with a 400 contract response. +# #region test_map_columns_invalid_source_type [TYPE Function] +# @BRIEF Validate map-columns rejects unsupported source types with a 400 contract response. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type # @PRE: source_type is not 'postgresql' or 'xlsx' # @POST: Returns 400 error @@ -207,11 +205,11 @@ def test_map_columns_invalid_source_type(mock_deps): assert "Source type must be 'postgresql' or 'xlsx'" in response.json()["detail"] -# [/DEF:test_map_columns_invalid_source_type:Function] +# #endregion test_map_columns_invalid_source_type -# [DEF:test_generate_docs_success:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] +# #region test_generate_docs_success [TYPE Function] +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/generate-docs creates doc generation task # @PRE: Valid env_id, dataset_ids, llm_provider # @PURPOSE: Validate generate-docs request creates an async documentation task and returns its identifier. @@ -239,12 +237,12 @@ def test_generate_docs_success(mock_deps): mock_deps["task"].create_task.assert_called_once() -# [/DEF:test_generate_docs_success:Function] +# #endregion test_generate_docs_success -# [DEF:test_map_columns_empty_ids:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate map-columns rejects empty dataset identifier lists. +# #region test_map_columns_empty_ids [TYPE Function] +# @BRIEF Validate map-columns rejects empty dataset identifier lists. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids # @PRE: dataset_ids is empty # @POST: Returns 400 error @@ -258,12 +256,12 @@ def test_map_columns_empty_ids(mock_deps): assert "At least one dataset ID must be provided" in response.json()["detail"] -# [/DEF:test_map_columns_empty_ids:Function] +# #endregion test_map_columns_empty_ids -# [DEF:test_generate_docs_empty_ids:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate generate-docs rejects empty dataset identifier lists. +# #region test_generate_docs_empty_ids [TYPE Function] +# @BRIEF Validate generate-docs rejects empty dataset identifier lists. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids # @PRE: dataset_ids is empty # @POST: Returns 400 error @@ -277,11 +275,11 @@ def test_generate_docs_empty_ids(mock_deps): assert "At least one dataset ID must be provided" in response.json()["detail"] -# [/DEF:test_generate_docs_empty_ids:Function] +# #endregion test_generate_docs_empty_ids -# [DEF:test_generate_docs_env_not_found:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] +# #region test_generate_docs_env_not_found [TYPE Function] +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST: POST /api/datasets/generate-docs returns 404 for missing env # @PRE: env_id does not exist # @PURPOSE: Validate generate-docs returns 404 when the requested environment cannot be resolved. @@ -297,12 +295,12 @@ def test_generate_docs_env_not_found(mock_deps): assert "Environment not found" in response.json()["detail"] -# [/DEF:test_generate_docs_env_not_found:Function] +# #endregion test_generate_docs_env_not_found -# [DEF:test_get_datasets_superset_failure:Function] -# @RELATION: BINDS_TO -> [DatasetsApiTests:Module] -# @PURPOSE: Validate datasets listing surfaces a 503 contract when Superset access fails. +# #region test_get_datasets_superset_failure [TYPE Function] +# @BRIEF Validate datasets listing surfaces a 503 contract when Superset access fails. +# @RELATION BINDS_TO -> [DatasetsApiTests:Module] # @TEST_EDGE: external_superset_failure -> {status: 503} # @POST: Returns 503 with stable error detail when upstream dataset fetch fails. def test_get_datasets_superset_failure(mock_deps): @@ -320,7 +318,7 @@ def test_get_datasets_superset_failure(mock_deps): assert "Failed to fetch datasets" in response.json()["detail"] -# [/DEF:test_get_datasets_superset_failure:Function] +# #endregion test_get_datasets_superset_failure -# [/DEF:DatasetsApiTests:Module] +# #endregion DatasetsApiTests diff --git a/backend/src/api/routes/__tests__/test_git_api.py b/backend/src/api/routes/__tests__/test_git_api.py index e097ab1a..d9249b02 100644 --- a/backend/src/api/routes/__tests__/test_git_api.py +++ b/backend/src/api/routes/__tests__/test_git_api.py @@ -1,7 +1,6 @@ -# [DEF:TestGitApi:Module] -# @COMPLEXITY: 3 -# @RELATION: VERIFIES -> [GitApi] -# @PURPOSE: API tests for Git configurations and repository operations. +# #region TestGitApi [C:3] [TYPE Module] +# @BRIEF API tests for Git configurations and repository operations. +# @RELATION VERIFIES -> [GitApi] import pytest import asyncio @@ -11,11 +10,10 @@ from src.api.routes import git as git_routes from src.models.git import GitServerConfig, GitProvider, GitStatus, GitRepository -# [DEF:DbMock:Class] -# @RELATION: BINDS_TO -> [TestGitApi] -# @COMPLEXITY: 2 -# @PURPOSE: In-memory session double for git route tests with minimal query/filter persistence semantics. -# @INVARIANT: Supports only the SQLAlchemy-like operations exercised by this test module. +# #region DbMock [C:2] [TYPE Class] +# @BRIEF In-memory session double for git route tests with minimal query/filter persistence semantics. +# @INVARIANT Supports only the SQLAlchemy-like operations exercised by this test module. +# @RELATION BINDS_TO -> [TestGitApi] class DbMock: def __init__(self, data=None): self._data = data or [] @@ -84,12 +82,12 @@ class DbMock: item.last_validated = "2026-03-08T00:00:00Z" -# [/DEF:DbMock:Class] +# #endregion DbMock -# [DEF:test_get_git_configs_masks_pat:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate listing git configs masks stored PAT values in API-facing responses. +# #region test_get_git_configs_masks_pat [TYPE Function] +# @BRIEF Validate listing git configs masks stored PAT values in API-facing responses. +# @RELATION BINDS_TO -> [TestGitApi] def test_get_git_configs_masks_pat(): """ @PRE: Database session `db` is available. @@ -116,12 +114,12 @@ def test_get_git_configs_masks_pat(): assert result[0].name == "Test Server" -# [/DEF:test_get_git_configs_masks_pat:Function] +# #endregion test_get_git_configs_masks_pat -# [DEF:test_create_git_config_persists_config:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate creating git config persists supplied server attributes in backing session. +# #region test_create_git_config_persists_config [TYPE Function] +# @BRIEF Validate creating git config persists supplied server attributes in backing session. +# @RELATION BINDS_TO -> [TestGitApi] def test_create_git_config_persists_config(): """ @PRE: `config` contains valid GitServerConfigCreate data. @@ -149,14 +147,14 @@ def test_create_git_config_persists_config(): ) # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not. -# [/DEF:test_create_git_config_persists_config:Function] +# #endregion test_create_git_config_persists_config from src.api.routes.git_schemas import GitServerConfigUpdate -# [DEF:test_update_git_config_modifies_record:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate updating git config modifies mutable fields while preserving masked PAT semantics. +# #region test_update_git_config_modifies_record [TYPE Function] +# @BRIEF Validate updating git config modifies mutable fields while preserving masked PAT semantics. +# @RELATION BINDS_TO -> [TestGitApi] def test_update_git_config_modifies_record(): """ @PRE: `config_id` corresponds to an existing configuration. @@ -206,12 +204,12 @@ def test_update_git_config_modifies_record(): assert result.pat == "********" -# [/DEF:test_update_git_config_modifies_record:Function] +# #endregion test_update_git_config_modifies_record -# [DEF:test_update_git_config_raises_404_if_not_found:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate updating non-existent git config raises HTTP 404 contract response. +# #region test_update_git_config_raises_404_if_not_found [TYPE Function] +# @BRIEF Validate updating non-existent git config raises HTTP 404 contract response. +# @RELATION BINDS_TO -> [TestGitApi] def test_update_git_config_raises_404_if_not_found(): """ @PRE: `config_id` corresponds to a missing configuration. @@ -231,12 +229,12 @@ def test_update_git_config_raises_404_if_not_found(): assert exc_info.value.detail == "Configuration not found" -# [/DEF:test_update_git_config_raises_404_if_not_found:Function] +# #endregion test_update_git_config_raises_404_if_not_found -# [DEF:test_delete_git_config_removes_record:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate deleting existing git config removes record and returns success payload. +# #region test_delete_git_config_removes_record [TYPE Function] +# @BRIEF Validate deleting existing git config removes record and returns success payload. +# @RELATION BINDS_TO -> [TestGitApi] def test_delete_git_config_removes_record(): """ @PRE: `config_id` corresponds to an existing configuration. @@ -269,12 +267,12 @@ def test_delete_git_config_removes_record(): assert result["status"] == "success" -# [/DEF:test_delete_git_config_removes_record:Function] +# #endregion test_delete_git_config_removes_record -# [DEF:test_test_git_config_validates_connection_successfully:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate test-connection endpoint returns success when provider connectivity check passes. +# #region test_test_git_config_validates_connection_successfully [TYPE Function] +# @BRIEF Validate test-connection endpoint returns success when provider connectivity check passes. +# @RELATION BINDS_TO -> [TestGitApi] def test_test_git_config_validates_connection_successfully(monkeypatch): """ @PRE: `config` contains provider, url, and pat. @@ -302,12 +300,12 @@ def test_test_git_config_validates_connection_successfully(monkeypatch): assert result["status"] == "success" -# [/DEF:test_test_git_config_validates_connection_successfully:Function] +# #endregion test_test_git_config_validates_connection_successfully -# [DEF:test_test_git_config_fails_validation:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate test-connection endpoint raises HTTP 400 when provider connectivity check fails. +# #region test_test_git_config_fails_validation [TYPE Function] +# @BRIEF Validate test-connection endpoint raises HTTP 400 when provider connectivity check fails. +# @RELATION BINDS_TO -> [TestGitApi] def test_test_git_config_fails_validation(monkeypatch): """ @PRE: `config` contains provider, url, and pat BUT connection fails. @@ -337,12 +335,12 @@ def test_test_git_config_fails_validation(monkeypatch): assert exc_info.value.detail == "Connection failed" -# [/DEF:test_test_git_config_fails_validation:Function] +# #endregion test_test_git_config_fails_validation -# [DEF:test_list_gitea_repositories_returns_payload:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate gitea repositories endpoint returns normalized list for GITEA provider configs. +# #region test_list_gitea_repositories_returns_payload [TYPE Function] +# @BRIEF Validate gitea repositories endpoint returns normalized list for GITEA provider configs. +# @RELATION BINDS_TO -> [TestGitApi] def test_list_gitea_repositories_returns_payload(monkeypatch): """ @PRE: config_id exists and provider is GITEA. @@ -375,12 +373,12 @@ def test_list_gitea_repositories_returns_payload(monkeypatch): assert result[0].private is True -# [/DEF:test_list_gitea_repositories_returns_payload:Function] +# #endregion test_list_gitea_repositories_returns_payload -# [DEF:test_list_gitea_repositories_rejects_non_gitea:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate gitea repositories endpoint rejects non-GITEA providers with HTTP 400. +# #region test_list_gitea_repositories_rejects_non_gitea [TYPE Function] +# @BRIEF Validate gitea repositories endpoint rejects non-GITEA providers with HTTP 400. +# @RELATION BINDS_TO -> [TestGitApi] def test_list_gitea_repositories_rejects_non_gitea(monkeypatch): """ @PRE: config_id exists and provider is NOT GITEA. @@ -402,12 +400,12 @@ def test_list_gitea_repositories_rejects_non_gitea(monkeypatch): assert "GITEA provider only" in exc_info.value.detail -# [/DEF:test_list_gitea_repositories_rejects_non_gitea:Function] +# #endregion test_list_gitea_repositories_rejects_non_gitea -# [DEF:test_create_remote_repository_creates_provider_repo:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate remote repository creation endpoint maps provider response into normalized payload. +# #region test_create_remote_repository_creates_provider_repo [TYPE Function] +# @BRIEF Validate remote repository creation endpoint maps provider response into normalized payload. +# @RELATION BINDS_TO -> [TestGitApi] def test_create_remote_repository_creates_provider_repo(monkeypatch): """ @PRE: config_id exists and PAT has creation permissions. @@ -450,12 +448,12 @@ def test_create_remote_repository_creates_provider_repo(monkeypatch): assert result.full_name == "user/new-repo" -# [/DEF:test_create_remote_repository_creates_provider_repo:Function] +# #endregion test_create_remote_repository_creates_provider_repo -# [DEF:test_init_repository_initializes_and_saves_binding:Function] -# @RELATION: BINDS_TO -> [TestGitApi] -# @PURPOSE: Validate repository initialization endpoint creates local repo and persists dashboard binding. +# #region test_init_repository_initializes_and_saves_binding [TYPE Function] +# @BRIEF Validate repository initialization endpoint creates local repo and persists dashboard binding. +# @RELATION BINDS_TO -> [TestGitApi] def test_init_repository_initializes_and_saves_binding(monkeypatch): """ @PRE: `dashboard_ref` exists and `init_data` contains valid config_id and remote_url. @@ -509,5 +507,5 @@ def test_init_repository_initializes_and_saves_binding(monkeypatch): assert db._added[0].dashboard_id == 123 -# [/DEF:test_init_repository_initializes_and_saves_binding:Function] -# [/DEF:TestGitApi:Module] +# #endregion test_init_repository_initializes_and_saves_binding +# #endregion TestGitApi diff --git a/backend/src/api/routes/__tests__/test_git_status_route.py b/backend/src/api/routes/__tests__/test_git_status_route.py index 867fd706..7940d0dd 100644 --- a/backend/src/api/routes/__tests__/test_git_status_route.py +++ b/backend/src/api/routes/__tests__/test_git_status_route.py @@ -1,9 +1,7 @@ -# [DEF:TestGitStatusRoute:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, git, api, status, no_repo -# @PURPOSE: Validate status endpoint behavior for missing and error repository states. -# @LAYER: Domain (Tests) -# @RELATION: VERIFIES -> [GitApi] +# #region TestGitStatusRoute [C:3] [TYPE Module] [SEMANTICS tests, git, api, status, no_repo] +# @BRIEF Validate status endpoint behavior for missing and error repository states. +# @LAYER Domain (Tests) +# @RELATION VERIFIES -> [GitApi] from fastapi import HTTPException import pytest @@ -13,11 +11,11 @@ from unittest.mock import MagicMock from src.api.routes import git as git_routes -# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error. -# @PRE: GitService.get_status raises HTTPException(404). -# @POST: Route returns a deterministic NO_REPO status payload. +# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function] +# @BRIEF Ensure missing local repository is represented as NO_REPO payload instead of an API error. +# @PRE GitService.get_status raises HTTPException(404). +# @POST Route returns a deterministic NO_REPO status payload. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch): class MissingRepoGitService: def _get_repo_path(self, dashboard_id: int) -> str: @@ -34,14 +32,14 @@ def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypa assert response["sync_state"] == "NO_REPO" assert response["has_repo"] is False assert response["current_branch"] is None -# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function] +# #endregion test_get_repository_status_returns_no_repo_payload_for_missing_repo -# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked. -# @PRE: GitService.get_status raises HTTPException with non-404 status. -# @POST: Raised exception preserves original status and detail. +# #region test_get_repository_status_propagates_non_404_http_exception [TYPE Function] +# @BRIEF Ensure HTTP exceptions other than 404 are not masked. +# @PRE GitService.get_status raises HTTPException with non-404 status. +# @POST Raised exception preserves original status and detail. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_status_propagates_non_404_http_exception(monkeypatch): class ConflictGitService: def _get_repo_path(self, dashboard_id: int) -> str: @@ -58,14 +56,14 @@ def test_get_repository_status_propagates_non_404_http_exception(monkeypatch): assert exc_info.value.status_code == 409 assert exc_info.value.detail == "Conflict" -# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function] +# #endregion test_get_repository_status_propagates_non_404_http_exception -# [DEF:test_get_repository_diff_propagates_http_exception:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService. -# @PRE: GitService.get_diff raises HTTPException. -# @POST: Endpoint raises same HTTPException values. +# #region test_get_repository_diff_propagates_http_exception [TYPE Function] +# @BRIEF Ensure diff endpoint preserves domain HTTP errors from GitService. +# @PRE GitService.get_diff raises HTTPException. +# @POST Endpoint raises same HTTPException values. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_diff_propagates_http_exception(monkeypatch): class DiffGitService: def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str: @@ -78,14 +76,14 @@ def test_get_repository_diff_propagates_http_exception(monkeypatch): assert exc_info.value.status_code == 404 assert exc_info.value.detail == "Repository missing" -# [/DEF:test_get_repository_diff_propagates_http_exception:Function] +# #endregion test_get_repository_diff_propagates_http_exception -# [DEF:test_get_history_wraps_unexpected_error_as_500:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors. -# @PRE: GitService.get_commit_history raises ValueError. -# @POST: Endpoint returns HTTPException with status 500 and route context. +# #region test_get_history_wraps_unexpected_error_as_500 [TYPE Function] +# @BRIEF Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors. +# @PRE GitService.get_commit_history raises ValueError. +# @POST Endpoint returns HTTPException with status 500 and route context. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_history_wraps_unexpected_error_as_500(monkeypatch): class HistoryGitService: def get_commit_history(self, dashboard_id: int, limit: int = 50): @@ -98,14 +96,14 @@ def test_get_history_wraps_unexpected_error_as_500(monkeypatch): assert exc_info.value.status_code == 500 assert exc_info.value.detail == "get_history failed: broken parser" -# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function] +# #endregion test_get_history_wraps_unexpected_error_as_500 -# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400. -# @PRE: GitService.commit_changes raises RuntimeError. -# @POST: Endpoint raises HTTPException(500) with route context. +# #region test_commit_changes_wraps_unexpected_error_as_500 [TYPE Function] +# @BRIEF Ensure commit endpoint does not leak unexpected errors as 400. +# @PRE GitService.commit_changes raises RuntimeError. +# @POST Endpoint raises HTTPException(500) with route context. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch): class CommitGitService: def commit_changes(self, dashboard_id: int, message: str, files): @@ -122,14 +120,14 @@ def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch): assert exc_info.value.status_code == 500 assert exc_info.value.detail == "commit_changes failed: index lock" -# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function] +# #endregion test_commit_changes_wraps_unexpected_error_as_500 -# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response. -# @PRE: Some repositories are missing and some are initialized. -# @POST: Returned map includes resolved status for each requested dashboard ID. +# #region test_get_repository_status_batch_returns_mixed_statuses [TYPE Function] +# @BRIEF Ensure batch endpoint returns per-dashboard statuses in one response. +# @PRE Some repositories are missing and some are initialized. +# @POST Returned map includes resolved status for each requested dashboard ID. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch): class BatchGitService: def _get_repo_path(self, dashboard_id: int) -> str: @@ -150,14 +148,14 @@ def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch): assert response.statuses["1"]["sync_status"] == "NO_REPO" assert response.statuses["2"]["sync_state"] == "SYNCED" -# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function] +# #endregion test_get_repository_status_batch_returns_mixed_statuses -# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request. -# @PRE: GitService raises non-HTTP exception for one dashboard. -# @POST: Failed dashboard status is marked as ERROR. +# #region test_get_repository_status_batch_marks_item_as_error_on_service_failure [TYPE Function] +# @BRIEF Ensure batch endpoint marks failed items as ERROR without failing entire request. +# @PRE GitService raises non-HTTP exception for one dashboard. +# @POST Failed dashboard status is marked as ERROR. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch): class BatchErrorGitService: def _get_repo_path(self, dashboard_id: int) -> str: @@ -176,14 +174,14 @@ def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monk assert response.statuses["9"]["sync_status"] == "ERROR" assert response.statuses["9"]["sync_state"] == "ERROR" -# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function] +# #endregion test_get_repository_status_batch_marks_item_as_error_on_service_failure -# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure batch endpoint protects server from oversized payloads. -# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries. -# @POST: Result contains unique IDs up to configured cap. +# #region test_get_repository_status_batch_deduplicates_and_truncates_ids [TYPE Function] +# @BRIEF Ensure batch endpoint protects server from oversized payloads. +# @PRE request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries. +# @POST Result contains unique IDs up to configured cap. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch): class SafeBatchGitService: def _get_repo_path(self, dashboard_id: int) -> str: @@ -202,14 +200,14 @@ def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch) assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH assert "1" in response.statuses -# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function] +# #endregion test_get_repository_status_batch_deduplicates_and_truncates_ids -# [DEF:test_commit_changes_applies_profile_identity_before_commit:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure commit route configures repository identity from profile preferences before commit call. -# @PRE: Profile preference contains git_username/git_email for current user. -# @POST: git_service.configure_identity receives resolved identity and commit proceeds. +# #region test_commit_changes_applies_profile_identity_before_commit [TYPE Function] +# @BRIEF Ensure commit route configures repository identity from profile preferences before commit call. +# @PRE Profile preference contains git_username/git_email for current user. +# @POST git_service.configure_identity receives resolved identity and commit proceeds. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_commit_changes_applies_profile_identity_before_commit(monkeypatch): class IdentityGitService: def __init__(self): @@ -264,14 +262,14 @@ def test_commit_changes_applies_profile_identity_before_commit(monkeypatch): assert identity_service.configured_identity == (12, "user_1", "user1@mail.ru") assert identity_service.commit_payload == (12, "test", ["dashboards/a.yaml"]) -# [/DEF:test_commit_changes_applies_profile_identity_before_commit:Function] +# #endregion test_commit_changes_applies_profile_identity_before_commit -# [DEF:test_pull_changes_applies_profile_identity_before_pull:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure pull route configures repository identity from profile preferences before pull call. -# @PRE: Profile preference contains git_username/git_email for current user. -# @POST: git_service.configure_identity receives resolved identity and pull proceeds. +# #region test_pull_changes_applies_profile_identity_before_pull [TYPE Function] +# @BRIEF Ensure pull route configures repository identity from profile preferences before pull call. +# @PRE Profile preference contains git_username/git_email for current user. +# @POST git_service.configure_identity receives resolved identity and pull proceeds. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_pull_changes_applies_profile_identity_before_pull(monkeypatch): class IdentityGitService: def __init__(self): @@ -321,14 +319,14 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch): assert identity_service.configured_identity == (12, "user_1", "user1@mail.ru") assert identity_service.pulled_dashboard_id == 12 -# [/DEF:test_pull_changes_applies_profile_identity_before_pull:Function] +# #endregion test_pull_changes_applies_profile_identity_before_pull -# [DEF:test_get_merge_status_returns_service_payload:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure merge status route returns service payload as-is. -# @PRE: git_service.get_merge_status returns unfinished merge payload. -# @POST: Route response contains has_unfinished_merge=True. +# #region test_get_merge_status_returns_service_payload [TYPE Function] +# @BRIEF Ensure merge status route returns service payload as-is. +# @PRE git_service.get_merge_status returns unfinished merge payload. +# @POST Route response contains has_unfinished_merge=True. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_get_merge_status_returns_service_payload(monkeypatch): class MergeStatusGitService: def get_merge_status(self, dashboard_id: int) -> dict: @@ -354,14 +352,14 @@ def test_get_merge_status_returns_service_payload(monkeypatch): assert response["has_unfinished_merge"] is True assert response["conflicts_count"] == 2 -# [/DEF:test_get_merge_status_returns_service_payload:Function] +# #endregion test_get_merge_status_returns_service_payload -# [DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure merge resolve route forwards parsed resolutions to service. -# @PRE: resolve_data has one file strategy. -# @POST: Service receives normalized list and route returns resolved files. +# #region test_resolve_merge_conflicts_passes_resolution_items_to_service [TYPE Function] +# @BRIEF Ensure merge resolve route forwards parsed resolutions to service. +# @PRE resolve_data has one file strategy. +# @POST Service receives normalized list and route returns resolved files. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch): captured = {} @@ -392,14 +390,14 @@ def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch) assert captured["dashboard_id"] == 12 assert captured["resolutions"][0]["resolution"] == "mine" assert response["resolved_files"] == ["dashboards/a.yaml"] -# [/DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function] +# #endregion test_resolve_merge_conflicts_passes_resolution_items_to_service -# [DEF:test_abort_merge_calls_service_and_returns_result:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure abort route delegates to service. -# @PRE: Service abort_merge returns aborted status. -# @POST: Route returns aborted status. +# #region test_abort_merge_calls_service_and_returns_result [TYPE Function] +# @BRIEF Ensure abort route delegates to service. +# @PRE Service abort_merge returns aborted status. +# @POST Route returns aborted status. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_abort_merge_calls_service_and_returns_result(monkeypatch): class AbortGitService: def abort_merge(self, dashboard_id: int): @@ -417,14 +415,14 @@ def test_abort_merge_calls_service_and_returns_result(monkeypatch): ) assert response["status"] == "aborted" -# [/DEF:test_abort_merge_calls_service_and_returns_result:Function] +# #endregion test_abort_merge_calls_service_and_returns_result -# [DEF:test_continue_merge_passes_message_and_returns_commit:Function] -# @RELATION: BINDS_TO -> TestGitStatusRoute -# @PURPOSE: Ensure continue route passes commit message to service. -# @PRE: continue_data.message is provided. -# @POST: Route returns committed status and hash. +# #region test_continue_merge_passes_message_and_returns_commit [TYPE Function] +# @BRIEF Ensure continue route passes commit message to service. +# @PRE continue_data.message is provided. +# @POST Route returns committed status and hash. +# @RELATION BINDS_TO -> [TestGitStatusRoute] def test_continue_merge_passes_message_and_returns_commit(monkeypatch): class ContinueGitService: def continue_merge(self, dashboard_id: int, message: str): @@ -448,7 +446,7 @@ def test_continue_merge_passes_message_and_returns_commit(monkeypatch): assert response["status"] == "committed" assert response["commit_hash"] == "abc123" -# [/DEF:test_continue_merge_passes_message_and_returns_commit:Function] +# #endregion test_continue_merge_passes_message_and_returns_commit -# [/DEF:TestGitStatusRoute:Module] +# #endregion TestGitStatusRoute diff --git a/backend/src/api/routes/__tests__/test_migration_routes.py b/backend/src/api/routes/__tests__/test_migration_routes.py index 37473204..e20709a6 100644 --- a/backend/src/api/routes/__tests__/test_migration_routes.py +++ b/backend/src/api/routes/__tests__/test_migration_routes.py @@ -1,9 +1,8 @@ -# [DEF:TestMigrationRoutes:Module] +# #region TestMigrationRoutes [C:3] [TYPE Module] +# @BRIEF Unit tests for migration API route handlers. +# @LAYER API +# @RELATION VERIFIES -> [backend.src.api.routes.migration] # -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for migration API route handlers. -# @LAYER: API -# @RELATION: VERIFIES -> backend.src.api.routes.migration # import pytest import sys @@ -60,8 +59,8 @@ def db_session(): session.close() -# [DEF:_make_config_manager:Function] -# @RELATION: BINDS_TO -> TestMigrationRoutes +# #region _make_config_manager [TYPE Function] +# @RELATION BINDS_TO -> [TestMigrationRoutes] def _make_config_manager(cron="0 2 * * *"): """Creates a mock config manager with a realistic AppConfig-like object.""" settings = MagicMock() @@ -76,7 +75,7 @@ def _make_config_manager(cron="0 2 * * *"): # --- get_migration_settings tests --- -# [/DEF:_make_config_manager:Function] +# #endregion _make_config_manager @pytest.mark.asyncio @@ -329,8 +328,8 @@ async def test_get_resource_mappings_filter_by_type(db_session): @pytest.fixture -# [DEF:_mock_env:Function] -# @RELATION: BINDS_TO -> TestMigrationRoutes +# #region _mock_env [TYPE Function] +# @RELATION BINDS_TO -> [TestMigrationRoutes] def _mock_env(): """Creates a mock config environment object.""" env = MagicMock() @@ -344,11 +343,11 @@ def _mock_env(): return env -# [/DEF:_mock_env:Function] +# #endregion _mock_env -# [DEF:_make_sync_config_manager:Function] -# @RELATION: BINDS_TO -> TestMigrationRoutes +# #region _make_sync_config_manager [TYPE Function] +# @RELATION BINDS_TO -> [TestMigrationRoutes] def _make_sync_config_manager(environments): """Creates a mock config manager with environments list.""" settings = MagicMock() @@ -362,7 +361,7 @@ def _make_sync_config_manager(environments): return cm -# [/DEF:_make_sync_config_manager:Function] +# #endregion _make_sync_config_manager @pytest.mark.asyncio @@ -653,4 +652,4 @@ async def test_dry_run_migration_rejects_same_environment(db_session): assert exc.value.status_code == 400 -# [/DEF:TestMigrationRoutes:Module] +# #endregion TestMigrationRoutes diff --git a/backend/src/api/routes/__tests__/test_profile_api.py b/backend/src/api/routes/__tests__/test_profile_api.py index 4a3500de..4c8bdd35 100644 --- a/backend/src/api/routes/__tests__/test_profile_api.py +++ b/backend/src/api/routes/__tests__/test_profile_api.py @@ -1,9 +1,7 @@ -# [DEF:TestProfileApi:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, profile, api, preferences, lookup, contract -# @PURPOSE: Verifies profile API route contracts for preference read/update and Superset account lookup. -# @LAYER: API +# #region TestProfileApi [C:3] [TYPE Module] [SEMANTICS tests, profile, api, preferences, lookup, contract] +# @BRIEF Verifies profile API route contracts for preference read/update and Superset account lookup. +# @LAYER API +# @RELATION BELONGS_TO -> [SrcRoot] # [SECTION: IMPORTS] from datetime import datetime, timezone @@ -33,11 +31,11 @@ from src.services.profile_service import ( client = TestClient(app) -# [DEF:mock_profile_route_dependencies:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Provides deterministic dependency overrides for profile route tests. -# @PRE: App instance is initialized. -# @POST: Dependencies are overridden for current test and restored afterward. +# #region mock_profile_route_dependencies [TYPE Function] +# @BRIEF Provides deterministic dependency overrides for profile route tests. +# @PRE App instance is initialized. +# @POST Dependencies are overridden for current test and restored afterward. +# @RELATION BINDS_TO -> [TestProfileApi] def mock_profile_route_dependencies(): mock_user = MagicMock() mock_user.id = "u-1" @@ -51,14 +49,14 @@ def mock_profile_route_dependencies(): app.dependency_overrides[get_config_manager] = lambda: mock_config_manager return mock_user, mock_db, mock_config_manager -# [/DEF:mock_profile_route_dependencies:Function] +# #endregion mock_profile_route_dependencies -# [DEF:profile_route_deps_fixture:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Pytest fixture wrapper for profile route dependency overrides. -# @PRE: None. -# @POST: Yields overridden dependencies and clears overrides after test. +# #region profile_route_deps_fixture [TYPE Function] +# @BRIEF Pytest fixture wrapper for profile route dependency overrides. +# @PRE None. +# @POST Yields overridden dependencies and clears overrides after test. +# @RELATION BINDS_TO -> [TestProfileApi] import pytest @@ -67,14 +65,14 @@ def profile_route_deps_fixture(): yielded = mock_profile_route_dependencies() yield yielded app.dependency_overrides.clear() -# [/DEF:profile_route_deps_fixture:Function] +# #endregion profile_route_deps_fixture -# [DEF:_build_preference_response:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Builds stable profile preference response payload for route tests. -# @PRE: user_id is provided. -# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps. +# #region _build_preference_response [TYPE Function] +# @BRIEF Builds stable profile preference response payload for route tests. +# @PRE user_id is provided. +# @POST Returns ProfilePreferenceResponse object with deterministic timestamps. +# @RELATION BINDS_TO -> [TestProfileApi] def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceResponse: now = datetime.now(timezone.utc) return ProfilePreferenceResponse( @@ -108,14 +106,14 @@ def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceRespons ], ), ) -# [/DEF:_build_preference_response:Function] +# #endregion _build_preference_response -# [DEF:test_get_profile_preferences_returns_self_payload:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies GET /api/profile/preferences returns stable self-scoped payload. -# @PRE: Authenticated user context is available. -# @POST: Response status is 200 and payload contains current user preference. +# #region test_get_profile_preferences_returns_self_payload [TYPE Function] +# @BRIEF Verifies GET /api/profile/preferences returns stable self-scoped payload. +# @PRE Authenticated user context is available. +# @POST Response status is 200 and payload contains current user preference. +# @RELATION BINDS_TO -> [TestProfileApi] def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture): mock_user, _, _ = profile_route_deps_fixture service = MagicMock() @@ -141,14 +139,14 @@ def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture assert payload["security"]["current_role"] == "Data Engineer" assert payload["security"]["permissions"][0]["key"] == "migration:run" service.get_my_preference.assert_called_once_with(mock_user) -# [/DEF:test_get_profile_preferences_returns_self_payload:Function] +# #endregion test_get_profile_preferences_returns_self_payload -# [DEF:test_patch_profile_preferences_success:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies PATCH /api/profile/preferences persists valid payload through route mapping. -# @PRE: Valid request payload and authenticated user. -# @POST: Response status is 200 with saved preference payload. +# #region test_patch_profile_preferences_success [TYPE Function] +# @BRIEF Verifies PATCH /api/profile/preferences persists valid payload through route mapping. +# @PRE Valid request payload and authenticated user. +# @POST Response status is 200 with saved preference payload. +# @RELATION BINDS_TO -> [TestProfileApi] def test_patch_profile_preferences_success(profile_route_deps_fixture): mock_user, _, _ = profile_route_deps_fixture service = MagicMock() @@ -192,14 +190,14 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture): assert called_kwargs["payload"].start_page == "reports-logs" assert called_kwargs["payload"].auto_open_task_drawer is False assert called_kwargs["payload"].dashboards_table_density == "free" -# [/DEF:test_patch_profile_preferences_success:Function] +# #endregion test_patch_profile_preferences_success -# [DEF:test_patch_profile_preferences_validation_error:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies route maps domain validation failure to HTTP 422 with actionable details. -# @PRE: Service raises ProfileValidationError. -# @POST: Response status is 422 and includes validation messages. +# #region test_patch_profile_preferences_validation_error [TYPE Function] +# @BRIEF Verifies route maps domain validation failure to HTTP 422 with actionable details. +# @PRE Service raises ProfileValidationError. +# @POST Response status is 422 and includes validation messages. +# @RELATION BINDS_TO -> [TestProfileApi] def test_patch_profile_preferences_validation_error(profile_route_deps_fixture): service = MagicMock() service.update_my_preference.side_effect = ProfileValidationError( @@ -219,14 +217,14 @@ def test_patch_profile_preferences_validation_error(profile_route_deps_fixture): payload = response.json() assert "detail" in payload assert "Superset username is required when default filter is enabled." in payload["detail"] -# [/DEF:test_patch_profile_preferences_validation_error:Function] +# #endregion test_patch_profile_preferences_validation_error -# [DEF:test_patch_profile_preferences_cross_user_denied:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies route maps domain authorization guard failure to HTTP 403. -# @PRE: Service raises ProfileAuthorizationError. -# @POST: Response status is 403 with denial message. +# #region test_patch_profile_preferences_cross_user_denied [TYPE Function] +# @BRIEF Verifies route maps domain authorization guard failure to HTTP 403. +# @PRE Service raises ProfileAuthorizationError. +# @POST Response status is 403 with denial message. +# @RELATION BINDS_TO -> [TestProfileApi] def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture): service = MagicMock() service.update_my_preference.side_effect = ProfileAuthorizationError( @@ -245,14 +243,14 @@ def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture) assert response.status_code == 403 payload = response.json() assert payload["detail"] == "Cross-user preference mutation is forbidden" -# [/DEF:test_patch_profile_preferences_cross_user_denied:Function] +# #endregion test_patch_profile_preferences_cross_user_denied -# [DEF:test_lookup_superset_accounts_success:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies lookup route returns success payload with normalized candidates. -# @PRE: Valid environment_id and service success response. -# @POST: Response status is 200 and items list is returned. +# #region test_lookup_superset_accounts_success [TYPE Function] +# @BRIEF Verifies lookup route returns success payload with normalized candidates. +# @PRE Valid environment_id and service success response. +# @POST Response status is 200 and items list is returned. +# @RELATION BINDS_TO -> [TestProfileApi] def test_lookup_superset_accounts_success(profile_route_deps_fixture): service = MagicMock() service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse( @@ -282,14 +280,14 @@ def test_lookup_superset_accounts_success(profile_route_deps_fixture): assert payload["environment_id"] == "dev" assert payload["total"] == 1 assert payload["items"][0]["username"] == "john_doe" -# [/DEF:test_lookup_superset_accounts_success:Function] +# #endregion test_lookup_superset_accounts_success -# [DEF:test_lookup_superset_accounts_env_not_found:Function] -# @RELATION: BINDS_TO -> TestProfileApi -# @PURPOSE: Verifies lookup route maps missing environment to HTTP 404. -# @PRE: Service raises EnvironmentNotFoundError. -# @POST: Response status is 404 with explicit message. +# #region test_lookup_superset_accounts_env_not_found [TYPE Function] +# @BRIEF Verifies lookup route maps missing environment to HTTP 404. +# @PRE Service raises EnvironmentNotFoundError. +# @POST Response status is 404 with explicit message. +# @RELATION BINDS_TO -> [TestProfileApi] def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture): service = MagicMock() service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError( @@ -302,6 +300,6 @@ def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture): assert response.status_code == 404 payload = response.json() assert payload["detail"] == "Environment 'missing-env' not found" -# [/DEF:test_lookup_superset_accounts_env_not_found:Function] +# #endregion test_lookup_superset_accounts_env_not_found -# [/DEF:TestProfileApi:Module] +# #endregion TestProfileApi diff --git a/backend/src/api/routes/__tests__/test_reports_api.py b/backend/src/api/routes/__tests__/test_reports_api.py index d2ebe139..512421d0 100644 --- a/backend/src/api/routes/__tests__/test_reports_api.py +++ b/backend/src/api/routes/__tests__/test_reports_api.py @@ -1,10 +1,8 @@ -# [DEF:TestReportsApi:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, reports, api, contract, pagination, filtering -# @PURPOSE: Contract tests for GET /api/reports defaults, pagination, and filtering behavior. -# @LAYER: Domain (Tests) -# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}. +# #region TestReportsApi [C:3] [TYPE Module] [SEMANTICS tests, reports, api, contract, pagination, filtering] +# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior. +# @LAYER Domain (Tests) +# @INVARIANT API response contract contains {items,total,page,page_size,has_next,applied_filters}. +# @RELATION BELONGS_TO -> [SrcRoot] from datetime import datetime, timedelta, timezone from types import SimpleNamespace @@ -17,11 +15,10 @@ from src.dependencies import get_current_user, get_task_manager # @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks(). -# [DEF:_FakeTaskManager:Class] -# @RELATION: BINDS_TO -> [TestReportsApi] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal task-manager double exposing only get_all_tasks used by reports route tests. -# @INVARIANT: Returns pre-seeded tasks without mutation or side effects. +# #region _FakeTaskManager [C:1] [TYPE Class] +# @BRIEF Minimal task-manager double exposing only get_all_tasks used by reports route tests. +# @INVARIANT Returns pre-seeded tasks without mutation or side effects. +# @RELATION BINDS_TO -> [TestReportsApi] class _FakeTaskManager: def __init__(self, tasks): self._tasks = tasks @@ -30,23 +27,23 @@ class _FakeTaskManager: return self._tasks -# [/DEF:_FakeTaskManager:Class] +# #endregion _FakeTaskManager -# [DEF:_admin_user:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Build deterministic admin principal accepted by reports authorization guard. +# #region _admin_user [TYPE Function] +# @BRIEF Build deterministic admin principal accepted by reports authorization guard. +# @RELATION BINDS_TO -> [TestReportsApi] def _admin_user(): admin_role = SimpleNamespace(name="Admin", permissions=[]) return SimpleNamespace(username="test-admin", roles=[admin_role]) -# [/DEF:_admin_user:Function] +# #endregion _admin_user -# [DEF:_make_task:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Build Task fixture with controlled timestamps/status for reports list/detail normalization. +# #region _make_task [TYPE Function] +# @BRIEF Build Task fixture with controlled timestamps/status for reports list/detail normalization. +# @RELATION BINDS_TO -> [TestReportsApi] def _make_task( task_id: str, plugin_id: str, @@ -66,12 +63,12 @@ def _make_task( ) -# [/DEF:_make_task:Function] +# #endregion _make_task -# [DEF:test_get_reports_default_pagination_contract:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Validate reports list endpoint default pagination and contract keys for mixed task statuses. +# #region test_get_reports_default_pagination_contract [TYPE Function] +# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses. +# @RELATION BINDS_TO -> [TestReportsApi] def test_get_reports_default_pagination_contract(): now = datetime.utcnow() tasks = [ @@ -120,12 +117,12 @@ def test_get_reports_default_pagination_contract(): app.dependency_overrides.clear() -# [/DEF:test_get_reports_default_pagination_contract:Function] +# #endregion test_get_reports_default_pagination_contract -# [DEF:test_get_reports_filter_and_pagination:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Validate reports list endpoint applies task-type/status filters and pagination boundaries. +# #region test_get_reports_filter_and_pagination [TYPE Function] +# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries. +# @RELATION BINDS_TO -> [TestReportsApi] def test_get_reports_filter_and_pagination(): now = datetime.utcnow() tasks = [ @@ -174,12 +171,12 @@ def test_get_reports_filter_and_pagination(): app.dependency_overrides.clear() -# [/DEF:test_get_reports_filter_and_pagination:Function] +# #endregion test_get_reports_filter_and_pagination -# [DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes. +# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function] +# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes. +# @RELATION BINDS_TO -> [TestReportsApi] def test_get_reports_handles_mixed_naive_and_aware_datetimes(): naive_now = datetime.utcnow() aware_now = datetime.now(timezone.utc) @@ -214,12 +211,12 @@ def test_get_reports_handles_mixed_naive_and_aware_datetimes(): app.dependency_overrides.clear() -# [/DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function] +# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes -# [DEF:test_get_reports_invalid_filter_returns_400:Function] -# @RELATION: BINDS_TO -> TestReportsApi -# @PURPOSE: Validate reports list endpoint rejects unsupported task type filters with HTTP 400. +# #region test_get_reports_invalid_filter_returns_400 [TYPE Function] +# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400. +# @RELATION BINDS_TO -> [TestReportsApi] def test_get_reports_invalid_filter_returns_400(): now = datetime.utcnow() tasks = [ @@ -245,5 +242,5 @@ def test_get_reports_invalid_filter_returns_400(): app.dependency_overrides.clear() -# [/DEF:test_get_reports_invalid_filter_returns_400:Function] -# [/DEF:TestReportsApi:Module] +# #endregion test_get_reports_invalid_filter_returns_400 +# #endregion TestReportsApi diff --git a/backend/src/api/routes/__tests__/test_reports_detail_api.py b/backend/src/api/routes/__tests__/test_reports_detail_api.py index 5606de4c..ad8b1a17 100644 --- a/backend/src/api/routes/__tests__/test_reports_detail_api.py +++ b/backend/src/api/routes/__tests__/test_reports_detail_api.py @@ -1,10 +1,8 @@ -# [DEF:TestReportsDetailApi:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, reports, api, detail, diagnostics -# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior. -# @LAYER: Domain (Tests) -# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts. +# #region TestReportsDetailApi [C:3] [TYPE Module] [SEMANTICS tests, reports, api, detail, diagnostics] +# @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior. +# @LAYER Domain (Tests) +# @INVARIANT Detail endpoint tests must keep deterministic assertions for success and not-found contracts. +# @RELATION BELONGS_TO -> [SrcRoot] from datetime import datetime, timedelta from types import SimpleNamespace @@ -17,11 +15,10 @@ from src.dependencies import get_current_user, get_task_manager # @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks(). -# [DEF:_FakeTaskManager:Class] -# @RELATION: BINDS_TO -> [TestReportsDetailApi] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test. -# @INVARIANT: get_all_tasks returns exactly seeded tasks list. +# #region _FakeTaskManager [C:1] [TYPE Class] +# @BRIEF Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test. +# @INVARIANT get_all_tasks returns exactly seeded tasks list. +# @RELATION BINDS_TO -> [TestReportsDetailApi] class _FakeTaskManager: def __init__(self, tasks): self._tasks = tasks @@ -30,23 +27,23 @@ class _FakeTaskManager: return self._tasks -# [/DEF:_FakeTaskManager:Class] +# #endregion _FakeTaskManager -# [DEF:_admin_user:Function] -# @RELATION: BINDS_TO -> TestReportsDetailApi -# @PURPOSE: Provide admin principal fixture accepted by reports detail authorization policy. +# #region _admin_user [TYPE Function] +# @BRIEF Provide admin principal fixture accepted by reports detail authorization policy. +# @RELATION BINDS_TO -> [TestReportsDetailApi] def _admin_user(): role = SimpleNamespace(name="Admin", permissions=[]) return SimpleNamespace(username="test-admin", roles=[role]) -# [/DEF:_admin_user:Function] +# #endregion _admin_user -# [DEF:_make_task:Function] -# @RELATION: BINDS_TO -> TestReportsDetailApi -# @PURPOSE: Build deterministic Task payload for reports detail endpoint contract assertions. +# #region _make_task [TYPE Function] +# @BRIEF Build deterministic Task payload for reports detail endpoint contract assertions. +# @RELATION BINDS_TO -> [TestReportsDetailApi] def _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None): now = datetime.utcnow() return Task( @@ -62,12 +59,12 @@ def _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None): ) -# [/DEF:_make_task:Function] +# #endregion _make_task -# [DEF:test_get_report_detail_success:Function] -# @RELATION: BINDS_TO -> TestReportsDetailApi -# @PURPOSE: Validate report detail endpoint returns report body with diagnostics and next actions for existing task. +# #region test_get_report_detail_success [TYPE Function] +# @BRIEF Validate report detail endpoint returns report body with diagnostics and next actions for existing task. +# @RELATION BINDS_TO -> [TestReportsDetailApi] def test_get_report_detail_success(): task = _make_task( "detail-1", @@ -98,12 +95,12 @@ def test_get_report_detail_success(): app.dependency_overrides.clear() -# [/DEF:test_get_report_detail_success:Function] +# #endregion test_get_report_detail_success -# [DEF:test_get_report_detail_not_found:Function] -# @RELATION: BINDS_TO -> TestReportsDetailApi -# @PURPOSE: Validate report detail endpoint returns 404 when requested report identifier is absent. +# #region test_get_report_detail_not_found [TYPE Function] +# @BRIEF Validate report detail endpoint returns 404 when requested report identifier is absent. +# @RELATION BINDS_TO -> [TestReportsDetailApi] def test_get_report_detail_not_found(): task = _make_task("detail-2", "superset-backup", TaskStatus.SUCCESS) @@ -118,5 +115,5 @@ def test_get_report_detail_not_found(): app.dependency_overrides.clear() -# [/DEF:test_get_report_detail_not_found:Function] -# [/DEF:TestReportsDetailApi:Module] +# #endregion test_get_report_detail_not_found +# #endregion TestReportsDetailApi diff --git a/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py b/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py index 0b425c0a..63f36598 100644 --- a/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py +++ b/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py @@ -1,10 +1,8 @@ -# [DEF:TestReportsOpenapiConformance:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, reports, openapi, conformance -# @PURPOSE: Validate implemented reports payload shape against OpenAPI-required top-level contract fields. -# @LAYER: Domain (Tests) -# @INVARIANT: List and detail payloads include required contract keys. +# #region TestReportsOpenapiConformance [C:3] [TYPE Module] [SEMANTICS tests, reports, openapi, conformance] +# @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields. +# @LAYER Domain (Tests) +# @INVARIANT List and detail payloads include required contract keys. +# @RELATION BELONGS_TO -> [SrcRoot] from datetime import datetime from types import SimpleNamespace @@ -16,11 +14,10 @@ from src.core.task_manager.models import Task, TaskStatus from src.dependencies import get_current_user, get_task_manager -# [DEF:_FakeTaskManager:Class] -# @RELATION: BINDS_TO -> [TestReportsOpenapiConformance] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal task-manager fake exposing static task list for OpenAPI conformance checks. -# @INVARIANT: get_all_tasks returns seeded tasks unchanged. +# #region _FakeTaskManager [C:1] [TYPE Class] +# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks. +# @INVARIANT get_all_tasks returns seeded tasks unchanged. +# @RELATION BINDS_TO -> [TestReportsOpenapiConformance] class _FakeTaskManager: def __init__(self, tasks): self._tasks = tasks @@ -29,23 +26,23 @@ class _FakeTaskManager: return self._tasks -# [/DEF:_FakeTaskManager:Class] +# #endregion _FakeTaskManager -# [DEF:_admin_user:Function] -# @RELATION: BINDS_TO -> TestReportsOpenapiConformance -# @PURPOSE: Provide admin principal fixture required by reports routes in conformance tests. +# #region _admin_user [TYPE Function] +# @BRIEF Provide admin principal fixture required by reports routes in conformance tests. +# @RELATION BINDS_TO -> [TestReportsOpenapiConformance] def _admin_user(): role = SimpleNamespace(name="Admin", permissions=[]) return SimpleNamespace(username="test-admin", roles=[role]) -# [/DEF:_admin_user:Function] +# #endregion _admin_user -# [DEF:_task:Function] -# @RELATION: BINDS_TO -> TestReportsOpenapiConformance -# @PURPOSE: Construct deterministic task fixture consumed by reports list/detail payload assertions. +# #region _task [TYPE Function] +# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions. +# @RELATION BINDS_TO -> [TestReportsOpenapiConformance] def _task(task_id: str, plugin_id: str, status: TaskStatus): now = datetime.utcnow() return Task( @@ -59,12 +56,12 @@ def _task(task_id: str, plugin_id: str, status: TaskStatus): ) -# [/DEF:_task:Function] +# #endregion _task -# [DEF:test_reports_list_openapi_required_keys:Function] -# @RELATION: BINDS_TO -> TestReportsOpenapiConformance -# @PURPOSE: Verify reports list endpoint includes all required OpenAPI top-level keys. +# #region test_reports_list_openapi_required_keys [TYPE Function] +# @BRIEF Verify reports list endpoint includes all required OpenAPI top-level keys. +# @RELATION BINDS_TO -> [TestReportsOpenapiConformance] def test_reports_list_openapi_required_keys(): tasks = [ _task("r-1", "superset-backup", TaskStatus.SUCCESS), @@ -92,12 +89,12 @@ def test_reports_list_openapi_required_keys(): app.dependency_overrides.clear() -# [/DEF:test_reports_list_openapi_required_keys:Function] +# #endregion test_reports_list_openapi_required_keys -# [DEF:test_reports_detail_openapi_required_keys:Function] -# @RELATION: BINDS_TO -> TestReportsOpenapiConformance -# @PURPOSE: Verify reports detail endpoint returns payload containing the report object key. +# #region test_reports_detail_openapi_required_keys [TYPE Function] +# @BRIEF Verify reports detail endpoint returns payload containing the report object key. +# @RELATION BINDS_TO -> [TestReportsOpenapiConformance] def test_reports_detail_openapi_required_keys(): tasks = [_task("r-3", "llm_dashboard_validation", TaskStatus.SUCCESS)] app.dependency_overrides[get_current_user] = lambda: _admin_user() @@ -114,5 +111,5 @@ def test_reports_detail_openapi_required_keys(): app.dependency_overrides.clear() -# [/DEF:test_reports_detail_openapi_required_keys:Function] -# [/DEF:TestReportsOpenapiConformance:Module] +# #endregion test_reports_detail_openapi_required_keys +# #endregion TestReportsOpenapiConformance diff --git a/backend/src/api/routes/__tests__/test_tasks_logs.py b/backend/src/api/routes/__tests__/test_tasks_logs.py index dd99fda0..827ba181 100644 --- a/backend/src/api/routes/__tests__/test_tasks_logs.py +++ b/backend/src/api/routes/__tests__/test_tasks_logs.py @@ -1,9 +1,7 @@ -# [DEF:test_tasks_logs_module:Module] -# @RELATION: VERIFIES -> [src.api.routes.tasks:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: tests, tasks, logs, api, contract, validation -# @PURPOSE: Contract testing for task logs API endpoints. -# @LAYER: Domain (Tests) +# #region test_tasks_logs_module [C:2] [TYPE Module] [SEMANTICS tests, tasks, logs, api, contract, validation] +# @BRIEF Contract testing for task logs API endpoints. +# @LAYER Domain (Tests) +# @RELATION VERIFIES -> [src.api.routes.tasks:Module] import pytest from fastapi import FastAPI @@ -32,9 +30,9 @@ def client(): # @TEST_CONTRACT: get_task_logs_api -> Invariants # @TEST_FIXTURE: valid_task_logs_request -# [DEF:test_get_task_logs_success:Function] -# @RELATION: BINDS_TO -> test_tasks_logs_module -# @PURPOSE: Validate task logs endpoint returns filtered logs for an existing task. +# #region test_get_task_logs_success [TYPE Function] +# @BRIEF Validate task logs endpoint returns filtered logs for an existing task. +# @RELATION BINDS_TO -> [test_tasks_logs_module] def test_get_task_logs_success(client): tc, tm = client @@ -55,12 +53,12 @@ def test_get_task_logs_success(client): # @TEST_EDGE: task_not_found -# [/DEF:test_get_task_logs_success:Function] +# #endregion test_get_task_logs_success -# [DEF:test_get_task_logs_not_found:Function] -# @RELATION: BINDS_TO -> test_tasks_logs_module -# @PURPOSE: Validate task logs endpoint returns 404 when the task identifier is missing. +# #region test_get_task_logs_not_found [TYPE Function] +# @BRIEF Validate task logs endpoint returns 404 when the task identifier is missing. +# @RELATION BINDS_TO -> [test_tasks_logs_module] def test_get_task_logs_not_found(client): tc, tm = client tm.get_task.return_value = None @@ -71,12 +69,12 @@ def test_get_task_logs_not_found(client): # @TEST_EDGE: invalid_limit -# [/DEF:test_get_task_logs_not_found:Function] +# #endregion test_get_task_logs_not_found -# [DEF:test_get_task_logs_invalid_limit:Function] -# @RELATION: BINDS_TO -> test_tasks_logs_module -# @PURPOSE: Validate task logs endpoint enforces query validation for limit lower bound. +# #region test_get_task_logs_invalid_limit [TYPE Function] +# @BRIEF Validate task logs endpoint enforces query validation for limit lower bound. +# @RELATION BINDS_TO -> [test_tasks_logs_module] def test_get_task_logs_invalid_limit(client): tc, tm = client # limit=0 is ge=1 in Query @@ -85,12 +83,12 @@ def test_get_task_logs_invalid_limit(client): # @TEST_INVARIANT: response_purity -# [/DEF:test_get_task_logs_invalid_limit:Function] +# #endregion test_get_task_logs_invalid_limit -# [DEF:test_get_task_log_stats_success:Function] -# @RELATION: BINDS_TO -> test_tasks_logs_module -# @PURPOSE: Validate log stats endpoint returns success payload for an existing task. +# #region test_get_task_log_stats_success [TYPE Function] +# @BRIEF Validate log stats endpoint returns success payload for an existing task. +# @RELATION BINDS_TO -> [test_tasks_logs_module] def test_get_task_log_stats_success(client): tc, tm = client tm.get_task.return_value = MagicMock() @@ -104,5 +102,5 @@ def test_get_task_log_stats_success(client): # assuming tm.get_task_log_stats returns something compatible with LogStats -# [/DEF:test_get_task_log_stats_success:Function] -# [/DEF:test_tasks_logs_module:Module] +# #endregion test_get_task_log_stats_success +# #endregion test_tasks_logs_module diff --git a/backend/src/api/routes/admin.py b/backend/src/api/routes/admin.py index aeaf5c3b..0d95ceee 100644 --- a/backend/src/api/routes/admin.py +++ b/backend/src/api/routes/admin.py @@ -1,14 +1,12 @@ -# [DEF:AdminApi:Module] +# #region AdminApi [C:3] [TYPE Module] [SEMANTICS api, admin, users, roles, permissions] +# @BRIEF Admin API endpoints for user and role management. +# @LAYER API +# @INVARIANT All endpoints in this module require 'Admin' role or 'admin' scope. +# @RELATION DEPENDS_ON -> [AuthRepository:Class] +# @RELATION DEPENDS_ON -> [get_auth_db:Function] +# @RELATION DEPENDS_ON -> [has_permission:Function] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, admin, users, roles, permissions -# @PURPOSE: Admin API endpoints for user and role management. -# @LAYER: API -# @RELATION: [DEPENDS_ON] ->[AuthRepository:Class] -# @RELATION: [DEPENDS_ON] ->[get_auth_db:Function] -# @RELATION: [DEPENDS_ON] ->[has_permission:Function] # -# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope. # [SECTION: IMPORTS] from typing import List @@ -40,18 +38,17 @@ from ...services.rbac_permission_catalog import ( ) # [/SECTION] -# [DEF:router:Variable] -# @RELATION: DEPENDS_ON -> fastapi.APIRouter -# @PURPOSE: APIRouter instance for admin routes. +# #region router [TYPE Variable] +# @BRIEF APIRouter instance for admin routes. +# @RELATION DEPENDS_ON -> [fastapi.APIRouter] router = APIRouter(prefix="/api/admin", tags=["admin"]) -# [/DEF:router:Variable] +# #endregion router -# [DEF:list_users:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lists all registered users. -# @PRE: Current user has 'Admin' role. -# @POST: Returns a list of UserSchema objects. +# #region list_users [C:3] [TYPE Function] +# @BRIEF Lists all registered users. +# @PRE Current user has 'Admin' role. +# @POST Returns a list of UserSchema objects. # @PARAM: db (Session) - Auth database session. # @RETURN: List[UserSchema] - List of users. # @RELATION: CALLS -> User @@ -64,14 +61,13 @@ async def list_users( return users -# [/DEF:list_users:Function] +# #endregion list_users -# [DEF:create_user:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Creates a new local user. -# @PRE: Current user has 'Admin' role. -# @POST: New user is created in the database. +# #region create_user [C:3] [TYPE Function] +# @BRIEF Creates a new local user. +# @PRE Current user has 'Admin' role. +# @POST New user is created in the database. # @PARAM: user_in (UserCreate) - New user data. # @PARAM: db (Session) - Auth database session. # @RETURN: UserSchema - The created user. @@ -106,14 +102,13 @@ async def create_user( return new_user -# [/DEF:create_user:Function] +# #endregion create_user -# [DEF:update_user:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Updates an existing user. -# @PRE: Current user has 'Admin' role. -# @POST: User record is updated in the database. +# #region update_user [C:3] [TYPE Function] +# @BRIEF Updates an existing user. +# @PRE Current user has 'Admin' role. +# @POST User record is updated in the database. # @PARAM: user_id (str) - Target user UUID. # @PARAM: user_in (UserUpdate) - Updated user data. # @PARAM: db (Session) - Auth database session. @@ -151,14 +146,13 @@ async def update_user( return user -# [/DEF:update_user:Function] +# #endregion update_user -# [DEF:delete_user:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Deletes a user. -# @PRE: Current user has 'Admin' role. -# @POST: User record is removed from the database. +# #region delete_user [C:3] [TYPE Function] +# @BRIEF Deletes a user. +# @PRE Current user has 'Admin' role. +# @POST User record is removed from the database. # @PARAM: user_id (str) - Target user UUID. # @PARAM: db (Session) - Auth database session. # @RETURN: None @@ -184,14 +178,13 @@ async def delete_user( return None -# [/DEF:delete_user:Function] +# #endregion delete_user -# [DEF:list_roles:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lists all available roles. -# @RETURN: List[RoleSchema] - List of roles. -# @RELATION: [CALLS] ->[Role:Class] +# #region list_roles [C:3] [TYPE Function] +# @BRIEF Lists all available roles. +# @RETURN List[RoleSchema] - List of roles. +# @RELATION CALLS -> [Role:Class] @router.get("/roles", response_model=List[RoleSchema]) async def list_roles( db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "READ")) @@ -200,14 +193,13 @@ async def list_roles( return db.query(Role).all() -# [/DEF:list_roles:Function] +# #endregion list_roles -# [DEF:create_role:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Creates a new system role with associated permissions. -# @PRE: Role name must be unique. -# @POST: New Role record is created in auth.db. +# #region create_role [C:3] [TYPE Function] +# @BRIEF Creates a new system role with associated permissions. +# @PRE Role name must be unique. +# @POST New Role record is created in auth.db. # @PARAM: role_in (RoleCreate) - New role data. # @PARAM: db (Session) - Auth database session. # @RETURN: RoleSchema - The created role. @@ -241,14 +233,13 @@ async def create_role( return new_role -# [/DEF:create_role:Function] +# #endregion create_role -# [DEF:update_role:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Updates an existing role's metadata and permissions. -# @PRE: role_id must be a valid existing role UUID. -# @POST: Role record is updated in auth.db. +# #region update_role [C:3] [TYPE Function] +# @BRIEF Updates an existing role's metadata and permissions. +# @PRE role_id must be a valid existing role UUID. +# @POST Role record is updated in auth.db. # @PARAM: role_id (str) - Target role identifier. # @PARAM: role_in (RoleUpdate) - Updated role data. # @PARAM: db (Session) - Auth database session. @@ -289,14 +280,13 @@ async def update_role( return role -# [/DEF:update_role:Function] +# #endregion update_role -# [DEF:delete_role:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Removes a role from the system. -# @PRE: role_id must be a valid existing role UUID. -# @POST: Role record is removed from auth.db. +# #region delete_role [C:3] [TYPE Function] +# @BRIEF Removes a role from the system. +# @PRE role_id must be a valid existing role UUID. +# @POST Role record is removed from auth.db. # @PARAM: role_id (str) - Target role identifier. # @PARAM: db (Session) - Auth database session. # @RETURN: None @@ -319,13 +309,12 @@ async def delete_role( return None -# [/DEF:delete_role:Function] +# #endregion delete_role -# [DEF:list_permissions:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lists all available system permissions for assignment. -# @POST: Returns a list of all PermissionSchema objects. +# #region list_permissions [C:3] [TYPE Function] +# @BRIEF Lists all available system permissions for assignment. +# @POST Returns a list of all PermissionSchema objects. # @PARAM: db (Session) - Auth database session. # @RETURN: List[PermissionSchema] - List of permissions. # @RELATION: CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions @@ -349,13 +338,12 @@ async def list_permissions( return repo.list_permissions() -# [/DEF:list_permissions:Function] +# #endregion list_permissions -# [DEF:list_ad_mappings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lists all AD Group to Role mappings. -# @RELATION: CALLS -> ADGroupMapping +# #region list_ad_mappings [C:3] [TYPE Function] +# @BRIEF Lists all AD Group to Role mappings. +# @RELATION CALLS -> [ADGroupMapping] @router.get("/ad-mappings", response_model=List[ADGroupMappingSchema]) async def list_ad_mappings( db: Session = Depends(get_auth_db), @@ -365,15 +353,14 @@ async def list_ad_mappings( return db.query(ADGroupMapping).all() -# [/DEF:list_ad_mappings:Function] +# #endregion list_ad_mappings -# [DEF:create_ad_mapping:Function] -# @RELATION: [DEPENDS_ON] ->[ADGroupMapping:Class] -# @RELATION: [DEPENDS_ON] ->[get_auth_db:Function] -# @RELATION: [DEPENDS_ON] ->[has_permission:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Creates a new AD Group mapping. +# #region create_ad_mapping [C:2] [TYPE Function] +# @BRIEF Creates a new AD Group mapping. +# @RELATION DEPENDS_ON -> [ADGroupMapping:Class] +# @RELATION DEPENDS_ON -> [get_auth_db:Function] +# @RELATION DEPENDS_ON -> [has_permission:Function] @router.post("/ad-mappings", response_model=ADGroupMappingSchema) async def create_ad_mapping( mapping_in: ADGroupMappingCreate, @@ -390,6 +377,6 @@ async def create_ad_mapping( return new_mapping -# [/DEF:create_ad_mapping:Function] +# #endregion create_ad_mapping -# [/DEF:AdminApi:Module] +# #endregion AdminApi diff --git a/backend/src/api/routes/assistant/__init__.py b/backend/src/api/routes/assistant/__init__.py index 34aa145c..a2cc545b 100644 --- a/backend/src/api/routes/assistant/__init__.py +++ b/backend/src/api/routes/assistant/__init__.py @@ -1,13 +1,11 @@ -# [DEF:AssistantApi:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: api, assistant, chat, command, confirmation -# @PURPOSE: API routes for LLM assistant command parsing and safe execution orchestration. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [TaskManager] -# @RELATION: DEPENDS_ON -> [AssistantMessageRecord] -# @RELATION: DEPENDS_ON -> [AssistantConfirmationRecord] -# @RELATION: DEPENDS_ON -> [AssistantAuditRecord] -# @INVARIANT: Risky operations are never executed without valid confirmation token. +# #region AssistantApi [C:5] [TYPE Module] [SEMANTICS api, assistant, chat, command, confirmation] +# @BRIEF API routes for LLM assistant command parsing and safe execution orchestration. +# @LAYER API +# @INVARIANT Risky operations are never executed without valid confirmation token. +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [AssistantMessageRecord] +# @RELATION DEPENDS_ON -> [AssistantConfirmationRecord] +# @RELATION DEPENDS_ON -> [AssistantAuditRecord] # Re-export public API for backward compatibility. from ._routes import router, send_message, confirm_operation, cancel_operation @@ -113,4 +111,4 @@ __all__ = [ "_build_task_observability_summary", ] -# [/DEF:AssistantApi:Module] +# #endregion AssistantApi diff --git a/backend/src/api/routes/assistant/_admin_routes.py b/backend/src/api/routes/assistant/_admin_routes.py index 54beb23e..8b859f93 100644 --- a/backend/src/api/routes/assistant/_admin_routes.py +++ b/backend/src/api/routes/assistant/_admin_routes.py @@ -1,11 +1,10 @@ -# [DEF:AssistantAdminRoutes:Module] -# @COMPLEXITY: 5 -# @PURPOSE: FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantRoutes] -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @RELATION: DEPENDS_ON -> [AssistantHistory] -# @INVARIANT: Audit endpoint requires tasks:READ permission. +# #region AssistantAdminRoutes [C:5] [TYPE Module] +# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit. +# @LAYER API +# @INVARIANT Audit endpoint requires tasks:READ permission. +# @RELATION DEPENDS_ON -> [AssistantRoutes] +# @RELATION DEPENDS_ON -> [AssistantSchemas] +# @RELATION DEPENDS_ON -> [AssistantHistory] from __future__ import annotations @@ -41,12 +40,11 @@ from ._history import ( from ._routes import router -# [DEF:list_conversations:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return paginated conversation list for current user with archived flag and last message preview. -# @PRE: Authenticated user context and valid pagination params. -# @POST: Conversations are grouped by conversation_id sorted by latest activity descending. -# @RETURN: Dict with items, paging metadata, and archive segmentation counts. +# #region list_conversations [C:2] [TYPE Function] +# @BRIEF Return paginated conversation list for current user with archived flag and last message preview. +# @PRE Authenticated user context and valid pagination params. +# @POST Conversations are grouped by conversation_id sorted by latest activity descending. +# @RETURN Dict with items, paging metadata, and archive segmentation counts. @router.get("/conversations") async def list_conversations( page: int = Query(1, ge=1), @@ -136,14 +134,13 @@ async def list_conversations( } -# [/DEF:list_conversations:Function] +# #endregion list_conversations -# [DEF:delete_conversation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Soft-delete or hard-delete a conversation and clear its in-memory trace. -# @PRE: conversation_id belongs to current_user. -# @POST: Conversation records are removed from DB and CONVERSATIONS cache. +# #region delete_conversation [C:2] [TYPE Function] +# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace. +# @PRE conversation_id belongs to current_user. +# @POST Conversation records are removed from DB and CONVERSATIONS cache. @router.delete("/conversations/{conversation_id}") async def delete_conversation( conversation_id: str, @@ -182,15 +179,15 @@ async def delete_conversation( } -# [/DEF:delete_conversation:Function] +# #endregion delete_conversation @router.get("/history") -# [DEF:get_history:Function] -# @PURPOSE: Retrieve paginated assistant conversation history for current user. -# @PRE: Authenticated user is available and page params are valid. -# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics. -# @RETURN: Dict with items, paging metadata, and resolved conversation_id. +# #region get_history [TYPE Function] +# @BRIEF Retrieve paginated assistant conversation history for current user. +# @PRE Authenticated user is available and page params are valid. +# @POST Returns persistent messages and mirrored in-memory snapshot for diagnostics. +# @RETURN Dict with items, paging metadata, and resolved conversation_id. async def get_history( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), @@ -254,15 +251,15 @@ async def get_history( } -# [/DEF:get_history:Function] +# #endregion get_history @router.get("/audit") -# [DEF:get_assistant_audit:Function] -# @PURPOSE: Return assistant audit decisions for current user from persistent and in-memory stores. -# @PRE: User has tasks:READ permission. -# @POST: Audit payload is returned in reverse chronological order from DB. -# @RETURN: Dict with persistent and memory audit slices. +# #region get_assistant_audit [TYPE Function] +# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores. +# @PRE User has tasks:READ permission. +# @POST Audit payload is returned in reverse chronological order from DB. +# @RETURN Dict with persistent and memory audit slices. async def get_assistant_audit( limit: int = Query(50, ge=1, le=500), current_user: User = Depends(get_current_user), @@ -299,7 +296,7 @@ async def get_assistant_audit( } -# [/DEF:get_assistant_audit:Function] +# #endregion get_assistant_audit -# [/DEF:AssistantAdminRoutes:Module] +# #endregion AssistantAdminRoutes diff --git a/backend/src/api/routes/assistant/_command_parser.py b/backend/src/api/routes/assistant/_command_parser.py index b5c98a87..8d044485 100644 --- a/backend/src/api/routes/assistant/_command_parser.py +++ b/backend/src/api/routes/assistant/_command_parser.py @@ -1,9 +1,8 @@ -# [DEF:AssistantCommandParser:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Deterministic RU/EN command text parser that converts user messages into intent payloads. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantResolvers] -# @INVARIANT: Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation. +# #region AssistantCommandParser [C:4] [TYPE Module] +# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads. +# @LAYER API +# @INVARIANT Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation. +# @RELATION DEPENDS_ON -> [AssistantResolvers] from __future__ import annotations @@ -15,16 +14,15 @@ from src.core.config_manager import ConfigManager from ._resolvers import _extract_id, _is_production_env -# [DEF:_parse_command:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Deterministically parse RU/EN command text into intent payload. -# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}] -# @RELATION: DEPENDS_ON -> [_extract_id] -# @RELATION: DEPENDS_ON -> [_is_production_env] -# @SIDE_EFFECT: None (pure parsing logic). -# @PRE: message contains raw user text and config manager resolves environments. -# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields. -# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation. +# #region _parse_command [C:4] [TYPE Function] +# @BRIEF Deterministically parse RU/EN command text into intent payload. +# @PRE message contains raw user text and config manager resolves environments. +# @POST Returns intent dict with domain/operation/entities/confidence/risk fields. +# @SIDE_EFFECT None (pure parsing logic). +# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}] +# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation. +# @RELATION DEPENDS_ON -> [_extract_id] +# @RELATION DEPENDS_ON -> [_is_production_env] def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any]: with belief_scope('_parse_command'): logger.reason('Belief protocol reasoning checkpoint for _parse_command') @@ -85,7 +83,7 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False} -# [/DEF:_parse_command:Function] +# #endregion _parse_command -# [/DEF:AssistantCommandParser:Module] +# #endregion AssistantCommandParser diff --git a/backend/src/api/routes/assistant/_dataset_review.py b/backend/src/api/routes/assistant/_dataset_review.py index 87b103fa..5c4738dd 100644 --- a/backend/src/api/routes/assistant/_dataset_review.py +++ b/backend/src/api/routes/assistant/_dataset_review.py @@ -1,11 +1,10 @@ -# [DEF:AssistantDatasetReview:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Dataset review context loading and intent planning for the assistant API. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DatasetReviewOrchestrator] -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @RELATION: DISPATCHES -> [AssistantDatasetReviewDispatch] -# @INVARIANT: Dataset review operations are always scoped to the owner's session. +# #region AssistantDatasetReview [C:4] [TYPE Module] +# @BRIEF Dataset review context loading and intent planning for the assistant API. +# @LAYER API +# @INVARIANT Dataset review operations are always scoped to the owner's session. +# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator] +# @RELATION DEPENDS_ON -> [AssistantSchemas] +# @RELATION DISPATCHES -> [AssistantDatasetReviewDispatch] from __future__ import annotations @@ -36,13 +35,12 @@ from ._schemas import ( ) -# [DEF:_serialize_dataset_review_context:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] -# @PRE: session_id is a valid active review session identifier. -# @POST: Returns a serializable dictionary containing the complete review context. -# @SIDE_EFFECT: Reads session data from the database. +# #region _serialize_dataset_review_context [C:4] [TYPE Function] +# @BRIEF Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing. +# @PRE session_id is a valid active review session identifier. +# @POST Returns a serializable dictionary containing the complete review context. +# @SIDE_EFFECT Reads session data from the database. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] def _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str, Any]: with belief_scope('_serialize_dataset_review_context'): logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context') @@ -54,16 +52,15 @@ def _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str return {'session_id': session.session_id, 'version': int(getattr(session, 'version', 0) or 0), 'dataset_ref': session.dataset_ref, 'dataset_id': session.dataset_id, 'environment_id': session.environment_id, 'readiness_state': session.readiness_state.value, 'recommended_action': session.recommended_action.value, 'status': session.status.value, 'current_phase': session.current_phase.value, 'findings': [{'finding_id': item.finding_id, 'code': item.code, 'severity': item.severity.value, 'message': item.message, 'resolution_state': item.resolution_state.value} for item in getattr(session, 'findings', [])], 'imported_filters': [sanitize_imported_filter_for_assistant({'filter_id': item.filter_id, 'filter_name': item.filter_name, 'display_name': item.display_name, 'raw_value': item.raw_value, 'raw_value_masked': bool(getattr(item, 'raw_value_masked', False)), 'normalized_value': item.normalized_value, 'source': getattr(item.source, 'value', item.source), 'confidence_state': getattr(item.confidence_state, 'value', item.confidence_state), 'requires_confirmation': bool(item.requires_confirmation), 'recovery_status': getattr(item.recovery_status, 'value', item.recovery_status), 'notes': item.notes}) for item in getattr(session, 'imported_filters', [])], 'mappings': [{'mapping_id': item.mapping_id, 'filter_id': item.filter_id, 'variable_id': item.variable_id, 'mapping_method': getattr(item.mapping_method, 'value', item.mapping_method), 'effective_value': item.effective_value, 'approval_state': getattr(item.approval_state, 'value', item.approval_state), 'requires_explicit_approval': bool(item.requires_explicit_approval)} for item in getattr(session, 'execution_mappings', [])], 'semantic_fields': [{'field_id': item.field_id, 'field_name': item.field_name, 'verbose_name': item.verbose_name, 'description': item.description, 'display_format': item.display_format, 'provenance': getattr(item.provenance, 'value', item.provenance), 'is_locked': bool(item.is_locked), 'needs_review': bool(getattr(item, 'needs_review', False)), 'candidates': [{'candidate_id': c.candidate_id, 'field_id': c.field_id, 'verbose_name': c.proposed_verbose_name, 'description': c.proposed_description, 'display_format': c.proposed_display_format, 'status': getattr(c.status, 'value', c.status), 'source': c.source_id, 'score': c.confidence_score, 'created_at': c.created_at.isoformat() if c.created_at else None} for c in getattr(item, 'candidates', [])]} for item in getattr(session, 'semantic_fields', [])]} -# [/DEF:_serialize_dataset_review_context:Function] +# #endregion _serialize_dataset_review_context -# [DEF:_load_dataset_review_context:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Load owner-scoped dataset-review context for assistant planning and grounded response generation. -# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository] -# @PRE: session_id is a valid active review session identifier. -# @POST: Returns a loaded context object with session data and findings. -# @SIDE_EFFECT: Reads session data from the database. +# #region _load_dataset_review_context [C:4] [TYPE Function] +# @BRIEF Load owner-scoped dataset-review context for assistant planning and grounded response generation. +# @PRE session_id is a valid active review session identifier. +# @POST Returns a loaded context object with session data and findings. +# @SIDE_EFFECT Reads session data from the database. +# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository] def _load_dataset_review_context(dataset_review_session_id: Optional[str], current_user: User, db: Session) -> Optional[Dict[str, Any]]: with belief_scope('_load_dataset_review_context'): if not dataset_review_session_id: @@ -77,12 +74,11 @@ def _load_dataset_review_context(dataset_review_session_id: Optional[str], curre return _serialize_dataset_review_context(session) -# [/DEF:_load_dataset_review_context:Function] +# #endregion _load_dataset_review_context -# [DEF:_extract_dataset_review_target:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Extract structured dataset-review focus target hints embedded in assistant prompts. +# #region _extract_dataset_review_target [C:2] [TYPE Function] +# @BRIEF Extract structured dataset-review focus target hints embedded in assistant prompts. def _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optional[str]]: match = re.search( r"(?:target|focus)\s*[:=]\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)", @@ -94,12 +90,11 @@ def _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optiona return match.group(1).lower(), match.group(2) -# [/DEF:_extract_dataset_review_target:Function] +# #endregion _extract_dataset_review_target -# [DEF:_match_dataset_review_field:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve one semantic field from assistant-visible context by id or user-visible label. +# #region _match_dataset_review_field [C:2] [TYPE Function] +# @BRIEF Resolve one semantic field from assistant-visible context by id or user-visible label. def _match_dataset_review_field( dataset_context: Dict[str, Any], message: str, @@ -125,25 +120,23 @@ def _match_dataset_review_field( return None -# [/DEF:_match_dataset_review_field:Function] +# #endregion _match_dataset_review_field -# [DEF:_extract_quoted_segment:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Extract one quoted assistant command segment after a label token. +# #region _extract_quoted_segment [C:2] [TYPE Function] +# @BRIEF Extract one quoted assistant command segment after a label token. def _extract_quoted_segment(message: str, label: str) -> Optional[str]: pattern = rf"{label}\s*[=:]?\s*[\"']([^\"']+)[\"']" match = re.search(pattern, str(message or ""), re.IGNORECASE) return match.group(1).strip() if match else None -# [/DEF:_extract_quoted_segment:Function] +# #endregion _extract_quoted_segment -# [DEF:_plan_dataset_review_intent:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing. -# @RELATION: CALLS -> DatasetReviewOrchestrator +# #region _plan_dataset_review_intent [C:3] [TYPE Function] +# @BRIEF Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing. +# @RELATION CALLS -> [DatasetReviewOrchestrator] def _plan_dataset_review_intent( message: str, dataset_context: Dict[str, Any], @@ -291,7 +284,7 @@ def _plan_dataset_review_intent( return None -# [/DEF:_plan_dataset_review_intent:Function] +# #endregion _plan_dataset_review_intent -# [/DEF:AssistantDatasetReview:Module] +# #endregion AssistantDatasetReview diff --git a/backend/src/api/routes/assistant/_dataset_review_dispatch.py b/backend/src/api/routes/assistant/_dataset_review_dispatch.py index 31f4a286..279fd3ee 100644 --- a/backend/src/api/routes/assistant/_dataset_review_dispatch.py +++ b/backend/src/api/routes/assistant/_dataset_review_dispatch.py @@ -1,11 +1,10 @@ -# [DEF:AssistantDatasetReviewDispatch:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Dispatch and confirmation handling for dataset-review assistant intents. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantDatasetReview] -# @RELATION: DEPENDS_ON -> [DatasetReviewOrchestrator] -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @INVARIANT: Dataset review dispatch requires valid session version for write operations. +# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] +# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents. +# @LAYER API +# @INVARIANT Dataset review dispatch requires valid session version for write operations. +# @RELATION DEPENDS_ON -> [AssistantDatasetReview] +# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator] +# @RELATION DEPENDS_ON -> [AssistantSchemas] from __future__ import annotations @@ -37,9 +36,8 @@ from ._schemas import ( ) -# [DEF:_dataset_review_conflict_http_exception:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics. +# #region _dataset_review_conflict_http_exception [C:2] [TYPE Function] +# @BRIEF Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics. def _dataset_review_conflict_http_exception( exc: DatasetReviewSessionVersionConflictError, ) -> HTTPException: @@ -55,16 +53,15 @@ def _dataset_review_conflict_http_exception( ) -# [/DEF:_dataset_review_conflict_http_exception:Function] +# #endregion _dataset_review_conflict_http_exception -# [DEF:_dispatch_dataset_review_intent:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries. -# @RELATION: CALLS -> DatasetReviewOrchestrator -# @PRE: context contains valid session data and user intent. -# @POST: Returns a structured response with planned actions and confirmations. -# @SIDE_EFFECT: May update session state and enqueue tasks. +# #region _dispatch_dataset_review_intent [C:4] [TYPE Function] +# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries. +# @PRE context contains valid session data and user intent. +# @POST Returns a structured response with planned actions and confirmations. +# @SIDE_EFFECT May update session state and enqueue tasks. +# @RELATION CALLS -> [DatasetReviewOrchestrator] async def _dispatch_dataset_review_intent( intent: Dict[str, Any], current_user: User, @@ -284,7 +281,7 @@ async def _dispatch_dataset_review_intent( ) -# [/DEF:_dispatch_dataset_review_intent:Function] +# #endregion _dispatch_dataset_review_intent -# [/DEF:AssistantDatasetReviewDispatch:Module] +# #endregion AssistantDatasetReviewDispatch diff --git a/backend/src/api/routes/assistant/_dispatch.py b/backend/src/api/routes/assistant/_dispatch.py index 1b377520..559e4a56 100644 --- a/backend/src/api/routes/assistant/_dispatch.py +++ b/backend/src/api/routes/assistant/_dispatch.py @@ -1,12 +1,11 @@ -# [DEF:AssistantDispatch:Module] -# @COMPLEXITY: 5 -# @PURPOSE: Intent dispatch engine, confirmation summary, and clarification text for the assistant API. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @RELATION: DEPENDS_ON -> [AssistantResolvers] -# @RELATION: DEPENDS_ON -> [AssistantLlmPlanner] -# @RELATION: DEPENDS_ON -> [AssistantDatasetReview] -# @INVARIANT: Unsupported operations are rejected via HTTPException(400). +# #region AssistantDispatch [C:5] [TYPE Module] +# @BRIEF Intent dispatch engine, confirmation summary, and clarification text for the assistant API. +# @LAYER API +# @INVARIANT Unsupported operations are rejected via HTTPException(400). +# @RELATION DEPENDS_ON -> [AssistantSchemas] +# @RELATION DEPENDS_ON -> [AssistantResolvers] +# @RELATION DEPENDS_ON -> [AssistantLlmPlanner] +# @RELATION DEPENDS_ON -> [AssistantDatasetReview] from __future__ import annotations @@ -41,11 +40,10 @@ from ._dataset_review_dispatch import _dispatch_dataset_review_intent git_service = GitService() -# [DEF:_clarification_text_for_intent:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Convert technical missing-parameter errors into user-facing clarification prompts. -# @PRE: state was classified as needs_clarification for current intent/error combination. -# @POST: Returned text is human-readable and actionable for target operation. +# #region _clarification_text_for_intent [C:2] [TYPE Function] +# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts. +# @PRE state was classified as needs_clarification for current intent/error combination. +# @POST Returned text is human-readable and actionable for target operation. def _clarification_text_for_intent( intent: Optional[Dict[str, Any]], detail_text: str ) -> str: @@ -66,15 +64,14 @@ def _clarification_text_for_intent( return guidance_by_operation.get(operation, detail_text) -# [/DEF:_clarification_text_for_intent:Function] +# #endregion _clarification_text_for_intent -# [DEF:_async_confirmation_summary:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Build human-readable confirmation prompt for an intent before execution. -# @PRE: actions is a non-empty list of planned review actions. -# @POST: Returns a formatted summary string suitable for display to the user. -# @SIDE_EFFECT: None - pure formatting function. +# #region _async_confirmation_summary [C:4] [TYPE Function] +# @BRIEF Build human-readable confirmation prompt for an intent before execution. +# @PRE actions is a non-empty list of planned review actions. +# @POST Returns a formatted summary string suitable for display to the user. +# @SIDE_EFFECT None - pure formatting function. async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str: with belief_scope('_confirmation_summary'): logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary') @@ -137,21 +134,20 @@ async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: Co return f'Выполнить: {text}. Подтвердите или отмените.' -# [/DEF:_async_confirmation_summary:Function] +# #endregion _async_confirmation_summary -# [DEF:_dispatch_intent:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services. -# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]] -# @RELATION: DEPENDS_ON -> [_check_any_permission] -# @RELATION: DEPENDS_ON -> [_resolve_dashboard_id_entity] -# @RELATION: DEPENDS_ON -> [TaskManager] -# @RELATION: DEPENDS_ON -> [GitService] -# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state. -# @PRE: intent operation is known and actor permissions are validated per operation. -# @POST: Returns response text, optional task id, and UI actions for follow-up. -# @INVARIANT: unsupported operations are rejected via HTTPException(400). +# #region _dispatch_intent [C:5] [TYPE Function] +# @BRIEF Execute parsed assistant intent via existing task/plugin/git services. +# @PRE intent operation is known and actor permissions are validated per operation. +# @POST Returns response text, optional task id, and UI actions for follow-up. +# @SIDE_EFFECT May enqueue tasks, invoke git operations, and query/update external service state. +# @DATA_CONTRACT Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]] +# @INVARIANT unsupported operations are rejected via HTTPException(400). +# @RELATION DEPENDS_ON -> [_check_any_permission] +# @RELATION DEPENDS_ON -> [_resolve_dashboard_id_entity] +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [GitService] async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]: with belief_scope('_dispatch_intent'): logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent') @@ -303,7 +299,7 @@ async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_mana raise HTTPException(status_code=400, detail='Unsupported operation') -# [/DEF:_dispatch_intent:Function] +# #endregion _dispatch_intent -# [/DEF:AssistantDispatch:Module] +# #endregion AssistantDispatch diff --git a/backend/src/api/routes/assistant/_history.py b/backend/src/api/routes/assistant/_history.py index 374ed8df..fcac8923 100644 --- a/backend/src/api/routes/assistant/_history.py +++ b/backend/src/api/routes/assistant/_history.py @@ -1,9 +1,8 @@ -# [DEF:AssistantHistory:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Conversation history, audit trail, and confirmation persistence helpers for the assistant API. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @INVARIANT: Failed persistence attempts always rollback before returning. +# #region AssistantHistory [C:2] [TYPE Module] +# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API. +# @LAYER API +# @INVARIANT Failed persistence attempts always rollback before returning. +# @RELATION DEPENDS_ON -> [AssistantSchemas] from __future__ import annotations @@ -31,15 +30,14 @@ from ._schemas import ( logger = logger -# [DEF:_append_history:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Append conversation message to in-memory history buffer. -# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None] -# @RELATION: UPDATES -> [CONVERSATIONS] -# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history. -# @PRE: user_id and conversation_id identify target conversation bucket. -# @POST: Message entry is appended to CONVERSATIONS key list. -# @INVARIANT: every appended entry includes generated message_id and created_at timestamp. +# #region _append_history [C:2] [TYPE Function] +# @BRIEF Append conversation message to in-memory history buffer. +# @PRE user_id and conversation_id identify target conversation bucket. +# @POST Message entry is appended to CONVERSATIONS key list. +# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history. +# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None] +# @INVARIANT every appended entry includes generated message_id and created_at timestamp. +# @RELATION UPDATES -> [CONVERSATIONS] def _append_history( user_id: str, conversation_id: str, @@ -66,18 +64,17 @@ def _append_history( ) -# [/DEF:_append_history:Function] +# #endregion _append_history -# [DEF:_persist_message:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Persist assistant/user message record to database. -# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None] -# @RELATION: DEPENDS_ON -> [AssistantMessageRecord] -# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session. -# @PRE: db session is writable and message payload is serializable. -# @POST: Message row is committed or persistence failure is logged. -# @INVARIANT: failed persistence attempts always rollback before returning. +# #region _persist_message [C:2] [TYPE Function] +# @BRIEF Persist assistant/user message record to database. +# @PRE db session is writable and message payload is serializable. +# @POST Message row is committed or persistence failure is logged. +# @SIDE_EFFECT Writes AssistantMessageRecord rows and commits or rollbacks the DB session. +# @DATA_CONTRACT Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None] +# @INVARIANT failed persistence attempts always rollback before returning. +# @RELATION DEPENDS_ON -> [AssistantMessageRecord] def _persist_message( db: Session, user_id: str, @@ -108,18 +105,17 @@ def _persist_message( logger.warning(f"[assistant.message][persist_failed] {exc}") -# [/DEF:_persist_message:Function] +# #endregion _persist_message -# [DEF:_audit:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Append in-memory audit record for assistant decision trace. -# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None] -# @RELATION: UPDATES -> [ASSISTANT_AUDIT] -# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event. -# @PRE: payload describes decision/outcome fields. -# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry. -# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format. +# #region _audit [C:2] [TYPE Function] +# @BRIEF Append in-memory audit record for assistant decision trace. +# @PRE payload describes decision/outcome fields. +# @POST ASSISTANT_AUDIT list for user contains new timestamped entry. +# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event. +# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None] +# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format. +# @RELATION UPDATES -> [ASSISTANT_AUDIT] def _audit(user_id: str, payload: Dict[str, Any]): if user_id not in ASSISTANT_AUDIT: ASSISTANT_AUDIT[user_id] = [] @@ -129,14 +125,13 @@ def _audit(user_id: str, payload: Dict[str, Any]): logger.info(f"[assistant.audit] {payload}") -# [/DEF:_audit:Function] +# #endregion _audit -# [DEF:_persist_audit:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Persist structured assistant audit payload in database. -# @PRE: db session is writable and payload is JSON-serializable. -# @POST: Audit row is committed or failure is logged with rollback. +# #region _persist_audit [C:2] [TYPE Function] +# @BRIEF Persist structured assistant audit payload in database. +# @PRE db session is writable and payload is JSON-serializable. +# @POST Audit row is committed or failure is logged with rollback. def _persist_audit( db: Session, user_id: str, payload: Dict[str, Any], conversation_id: Optional[str] ): @@ -157,14 +152,13 @@ def _persist_audit( logger.warning(f"[assistant.audit][persist_failed] {exc}") -# [/DEF:_persist_audit:Function] +# #endregion _persist_audit -# [DEF:_persist_confirmation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Persist confirmation token record to database. -# @PRE: record contains id/user/intent/dispatch/expiry fields. -# @POST: Confirmation row exists in persistent storage. +# #region _persist_confirmation [C:2] [TYPE Function] +# @BRIEF Persist confirmation token record to database. +# @PRE record contains id/user/intent/dispatch/expiry fields. +# @POST Confirmation row exists in persistent storage. def _persist_confirmation(db: Session, record: ConfirmationRecord): try: row = AssistantConfirmationRecord( @@ -185,14 +179,13 @@ def _persist_confirmation(db: Session, record: ConfirmationRecord): logger.warning(f"[assistant.confirmation][persist_failed] {exc}") -# [/DEF:_persist_confirmation:Function] +# #endregion _persist_confirmation -# [DEF:_update_confirmation_state:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Update persistent confirmation token lifecycle state. -# @PRE: confirmation_id references existing row. -# @POST: State and consumed_at fields are updated when applicable. +# #region _update_confirmation_state [C:2] [TYPE Function] +# @BRIEF Update persistent confirmation token lifecycle state. +# @PRE confirmation_id references existing row. +# @POST State and consumed_at fields are updated when applicable. def _update_confirmation_state(db: Session, confirmation_id: str, state: str): try: row = ( @@ -211,14 +204,13 @@ def _update_confirmation_state(db: Session, confirmation_id: str, state: str): logger.warning(f"[assistant.confirmation][update_failed] {exc}") -# [/DEF:_update_confirmation_state:Function] +# #endregion _update_confirmation_state -# [DEF:_load_confirmation_from_db:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Load confirmation token from database into in-memory model. -# @PRE: confirmation_id may or may not exist in storage. -# @POST: Returns ConfirmationRecord when found, otherwise None. +# #region _load_confirmation_from_db [C:2] [TYPE Function] +# @BRIEF Load confirmation token from database into in-memory model. +# @PRE confirmation_id may or may not exist in storage. +# @POST Returns ConfirmationRecord when found, otherwise None. def _load_confirmation_from_db( db: Session, confirmation_id: str ) -> Optional[ConfirmationRecord]: @@ -241,14 +233,13 @@ def _load_confirmation_from_db( ) -# [/DEF:_load_confirmation_from_db:Function] +# #endregion _load_confirmation_from_db -# [DEF:_ensure_conversation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve active conversation id in memory or create a new one. -# @PRE: user_id identifies current actor. -# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION. +# #region _ensure_conversation [C:2] [TYPE Function] +# @BRIEF Resolve active conversation id in memory or create a new one. +# @PRE user_id identifies current actor. +# @POST Returns stable conversation id and updates USER_ACTIVE_CONVERSATION. def _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str: if conversation_id: from ._schemas import USER_ACTIVE_CONVERSATION @@ -265,14 +256,13 @@ def _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str: return new_id -# [/DEF:_ensure_conversation:Function] +# #endregion _ensure_conversation -# [DEF:_resolve_or_create_conversation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve active conversation using explicit id, memory cache, or persisted history. -# @PRE: user_id and db session are available. -# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache. +# #region _resolve_or_create_conversation [C:2] [TYPE Function] +# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history. +# @PRE user_id and db session are available. +# @POST Returns conversation id and updates USER_ACTIVE_CONVERSATION cache. def _resolve_or_create_conversation( user_id: str, conversation_id: Optional[str], db: Session ) -> str: @@ -303,14 +293,13 @@ def _resolve_or_create_conversation( return new_id -# [/DEF:_resolve_or_create_conversation:Function] +# #endregion _resolve_or_create_conversation -# [DEF:_cleanup_history_ttl:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Enforce assistant message retention window by deleting expired rows and in-memory records. -# @PRE: db session is available and user_id references current actor scope. -# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors. +# #region _cleanup_history_ttl [C:2] [TYPE Function] +# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records. +# @PRE db session is available and user_id references current actor scope. +# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors. def _cleanup_history_ttl(db: Session, user_id: str): cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS) try: @@ -345,14 +334,13 @@ def _cleanup_history_ttl(db: Session, user_id: str): CONVERSATIONS.pop(key, None) -# [/DEF:_cleanup_history_ttl:Function] +# #endregion _cleanup_history_ttl -# [DEF:_is_conversation_archived:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Determine archived state for a conversation based on last update timestamp. -# @PRE: updated_at can be null for empty conversations. -# @POST: Returns True when conversation inactivity exceeds archive threshold. +# #region _is_conversation_archived [C:2] [TYPE Function] +# @BRIEF Determine archived state for a conversation based on last update timestamp. +# @PRE updated_at can be null for empty conversations. +# @POST Returns True when conversation inactivity exceeds archive threshold. def _is_conversation_archived(updated_at: Optional[datetime]) -> bool: if not updated_at: return False @@ -360,14 +348,13 @@ def _is_conversation_archived(updated_at: Optional[datetime]) -> bool: return updated_at < cutoff -# [/DEF:_is_conversation_archived:Function] +# #endregion _is_conversation_archived -# [DEF:_coerce_query_bool:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize bool-like query values for compatibility in direct handler invocations/tests. -# @PRE: value may be bool, string, or FastAPI Query metadata object. -# @POST: Returns deterministic boolean flag. +# #region _coerce_query_bool [C:2] [TYPE Function] +# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests. +# @PRE value may be bool, string, or FastAPI Query metadata object. +# @POST Returns deterministic boolean flag. def _coerce_query_bool(value: Any) -> bool: if isinstance(value, bool): return value @@ -376,7 +363,7 @@ def _coerce_query_bool(value: Any) -> bool: return False -# [/DEF:_coerce_query_bool:Function] +# #endregion _coerce_query_bool -# [/DEF:AssistantHistory:Module] +# #endregion AssistantHistory diff --git a/backend/src/api/routes/assistant/_llm_planner.py b/backend/src/api/routes/assistant/_llm_planner.py index fb334b89..4af20aa4 100644 --- a/backend/src/api/routes/assistant/_llm_planner.py +++ b/backend/src/api/routes/assistant/_llm_planner.py @@ -1,11 +1,10 @@ -# [DEF:AssistantLlmPlanner:Module] -# @COMPLEXITY: 3 -# @PURPOSE: LLM-based intent planning, tool catalog construction, and authorization for the assistant API. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @RELATION: DEPENDS_ON -> [AssistantResolvers] -# @RELATION: DISPATCHES -> [AssistantLlmPlannerIntent] -# @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM. +# #region AssistantLlmPlanner [C:3] [TYPE Module] +# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API. +# @LAYER API +# @INVARIANT Tool catalog is filtered by user permissions before being sent to LLM. +# @RELATION DEPENDS_ON -> [AssistantSchemas] +# @RELATION DEPENDS_ON -> [AssistantResolvers] +# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent] from __future__ import annotations @@ -29,11 +28,10 @@ from ._resolvers import ( ) -# [DEF:_check_any_permission:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Validate user against alternative permission checks (logical OR). -# @PRE: checks list contains resource-action tuples. -# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise. +# #region _check_any_permission [C:2] [TYPE Function] +# @BRIEF Validate user against alternative permission checks (logical OR). +# @PRE checks list contains resource-action tuples. +# @POST Returns on first successful permission; raises 403-like HTTPException otherwise. def _check_any_permission(current_user: User, checks: List[Tuple[str, str]]): errors: List[HTTPException] = [] for resource, action in checks: @@ -50,14 +48,13 @@ def _check_any_permission(current_user: User, checks: List[Tuple[str, str]]): ) -# [/DEF:_check_any_permission:Function] +# #endregion _check_any_permission -# [DEF:_has_any_permission:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Check whether user has at least one permission tuple from the provided list. -# @PRE: current_user and checks list are valid. -# @POST: Returns True when at least one permission check passes. +# #region _has_any_permission [C:2] [TYPE Function] +# @BRIEF Check whether user has at least one permission tuple from the provided list. +# @PRE current_user and checks list are valid. +# @POST Returns True when at least one permission check passes. def _has_any_permission(current_user: User, checks: List[Tuple[str, str]]) -> bool: try: _check_any_permission(current_user, checks) @@ -66,15 +63,14 @@ def _has_any_permission(current_user: User, checks: List[Tuple[str, str]]) -> bo return False -# [/DEF:_has_any_permission:Function] +# #endregion _has_any_permission -# [DEF:_build_tool_catalog:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Build current-user tool catalog for LLM planner with operation contracts and defaults. -# @PRE: current_user is authenticated; config/db are available. -# @POST: Returns list of executable tools filtered by permission and runtime availability. -# @RELATION: CALLS -> LLMProviderService +# #region _build_tool_catalog [C:3] [TYPE Function] +# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults. +# @PRE current_user is authenticated; config/db are available. +# @POST Returns list of executable tools filtered by permission and runtime availability. +# @RELATION CALLS -> [LLMProviderService] def _build_tool_catalog( current_user: User, config_manager: ConfigManager, @@ -269,14 +265,13 @@ def _build_tool_catalog( return available -# [/DEF:_build_tool_catalog:Function] +# #endregion _build_tool_catalog -# [DEF:_coerce_intent_entities:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize intent entity value types from LLM output to route-compatible values. -# @PRE: intent contains entities dict or missing entities. -# @POST: Returned intent has numeric ids coerced where possible and string values stripped. +# #region _coerce_intent_entities [C:2] [TYPE Function] +# @BRIEF Normalize intent entity value types from LLM output to route-compatible values. +# @PRE intent contains entities dict or missing entities. +# @POST Returned intent has numeric ids coerced where possible and string values stripped. def _coerce_intent_entities(intent: Dict[str, Any]) -> Dict[str, Any]: entities = intent.get("entities") if not isinstance(entities, dict): @@ -292,7 +287,7 @@ def _coerce_intent_entities(intent: Dict[str, Any]) -> Dict[str, Any]: return intent -# [/DEF:_coerce_intent_entities:Function] +# #endregion _coerce_intent_entities -# [/DEF:AssistantLlmPlanner:Module] +# #endregion AssistantLlmPlanner diff --git a/backend/src/api/routes/assistant/_llm_planner_intent.py b/backend/src/api/routes/assistant/_llm_planner_intent.py index 6661a86e..a59799e0 100644 --- a/backend/src/api/routes/assistant/_llm_planner_intent.py +++ b/backend/src/api/routes/assistant/_llm_planner_intent.py @@ -1,10 +1,9 @@ -# [DEF:AssistantLlmPlannerIntent:Module] -# @COMPLEXITY: 3 -# @PURPOSE: LLM-based intent planning and authorization for the assistant API — separated from tool catalog. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantLlmPlanner] -# @RELATION: DEPENDS_ON -> [AssistantResolvers] -# @INVARIANT: Production deployments always require confirmation. +# #region AssistantLlmPlannerIntent [C:3] [TYPE Module] +# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog. +# @LAYER API +# @INVARIANT Production deployments always require confirmation. +# @RELATION DEPENDS_ON -> [AssistantLlmPlanner] +# @RELATION DEPENDS_ON -> [AssistantResolvers] from __future__ import annotations @@ -34,11 +33,10 @@ from ._llm_planner import ( ) -# [DEF:_plan_intent_with_llm:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Use active LLM provider to select best tool/operation from dynamic catalog. -# @PRE: tools list contains allowed operations for current user. -# @POST: Returns normalized intent dict when planning succeeds; otherwise None. +# #region _plan_intent_with_llm [C:2] [TYPE Function] +# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog. +# @PRE tools list contains allowed operations for current user. +# @POST Returns normalized intent dict when planning succeeds; otherwise None. async def _plan_intent_with_llm( message: str, tools: List[Dict[str, Any]], @@ -153,21 +151,20 @@ async def _plan_intent_with_llm( return intent -# [/DEF:_plan_intent_with_llm:Function] +# #endregion _plan_intent_with_llm -# [DEF:_authorize_intent:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Validate user permissions for parsed intent before confirmation/dispatch. -# @PRE: intent.operation is present for known assistant command domains. -# @POST: Returns if authorized; raises HTTPException(403) when denied. +# #region _authorize_intent [C:2] [TYPE Function] +# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch. +# @PRE intent.operation is present for known assistant command domains. +# @POST Returns if authorized; raises HTTPException(403) when denied. def _authorize_intent(intent: Dict[str, Any], current_user: User): operation = intent.get("operation") if operation in INTENT_PERMISSION_CHECKS: _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation]) -# [/DEF:_authorize_intent:Function] +# #endregion _authorize_intent -# [/DEF:AssistantLlmPlannerIntent:Module] +# #endregion AssistantLlmPlannerIntent diff --git a/backend/src/api/routes/assistant/_resolvers.py b/backend/src/api/routes/assistant/_resolvers.py index 8996b61b..a9f24ee9 100644 --- a/backend/src/api/routes/assistant/_resolvers.py +++ b/backend/src/api/routes/assistant/_resolvers.py @@ -1,10 +1,9 @@ -# [DEF:AssistantResolvers:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Environment, dashboard, provider, and task resolution utilities for the assistant API. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [ConfigManager] -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @INVARIANT: Resolution functions never raise; they return None on failure. +# #region AssistantResolvers [C:2] [TYPE Module] +# @BRIEF Environment, dashboard, provider, and task resolution utilities for the assistant API. +# @LAYER API +# @INVARIANT Resolution functions never raise; they return None on failure. +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [SupersetClient] from __future__ import annotations @@ -23,11 +22,10 @@ from src.schemas.auth import User logger = cast(Any, logger) -# [DEF:_extract_id:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Extract first regex match group from text by ordered pattern list. -# @PRE: patterns contain at least one capture group. -# @POST: Returns first matched token or None. +# #region _extract_id [C:2] [TYPE Function] +# @BRIEF Extract first regex match group from text by ordered pattern list. +# @PRE patterns contain at least one capture group. +# @POST Returns first matched token or None. def _extract_id(text: str, patterns: List[str]) -> Optional[str]: for p in patterns: m = re.search(p, text, flags=re.IGNORECASE) @@ -36,13 +34,12 @@ def _extract_id(text: str, patterns: List[str]) -> Optional[str]: return None -# [/DEF:_extract_id:Function] +# #endregion _extract_id -# [DEF:_resolve_env_id:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve environment identifier/name token to canonical environment id. -# @PRE: config_manager provides environment list. -# @POST: Returns matched environment id or None. +# #region _resolve_env_id [C:2] [TYPE Function] +# @BRIEF Resolve environment identifier/name token to canonical environment id. +# @PRE config_manager provides environment list. +# @POST Returns matched environment id or None. def _resolve_env_id( token: Optional[str], config_manager: ConfigManager ) -> Optional[str]: @@ -57,13 +54,12 @@ def _resolve_env_id( return None -# [/DEF:_resolve_env_id:Function] +# #endregion _resolve_env_id -# [DEF:_is_production_env:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Determine whether environment token resolves to production-like target. -# @PRE: config_manager provides environments or token text is provided. -# @POST: Returns True for production/prod synonyms, else False. +# #region _is_production_env [C:2] [TYPE Function] +# @BRIEF Determine whether environment token resolves to production-like target. +# @PRE config_manager provides environments or token text is provided. +# @POST Returns True for production/prod synonyms, else False. def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> bool: env_id = _resolve_env_id(token, config_manager) if not env_id: @@ -76,13 +72,12 @@ def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> b return "prod" in target or "production" in target or "прод" in target -# [/DEF:_is_production_env:Function] +# #endregion _is_production_env -# [DEF:_resolve_provider_id:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve provider token to provider id with active/default fallback. -# @PRE: db session can load provider list through LLMProviderService. -# @POST: Returns provider id or None when no providers configured. +# #region _resolve_provider_id [C:2] [TYPE Function] +# @BRIEF Resolve provider token to provider id with active/default fallback. +# @PRE db session can load provider list through LLMProviderService. +# @POST Returns provider id or None when no providers configured. def _resolve_provider_id( provider_token: Optional[str], db: Session, @@ -113,13 +108,12 @@ def _resolve_provider_id( return active.id if active else providers[0].id -# [/DEF:_resolve_provider_id:Function] +# #endregion _resolve_provider_id -# [DEF:_get_default_environment_id:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve default environment id from settings or first configured environment. -# @PRE: config_manager returns environments list. -# @POST: Returns default environment id or None when environment list is empty. +# #region _get_default_environment_id [C:2] [TYPE Function] +# @BRIEF Resolve default environment id from settings or first configured environment. +# @PRE config_manager returns environments list. +# @POST Returns default environment id or None when environment list is empty. def _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]: configured = config_manager.get_environments() if not configured: @@ -138,13 +132,12 @@ def _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]: return explicit_default or configured[0].id -# [/DEF:_get_default_environment_id:Function] +# #endregion _get_default_environment_id -# [DEF:_resolve_dashboard_id_by_ref:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard id by title or slug reference in selected environment. -# @PRE: dashboard_ref is a non-empty string-like token. -# @POST: Returns dashboard id when uniquely matched, otherwise None. +# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function] +# @BRIEF Resolve dashboard id by title or slug reference in selected environment. +# @PRE dashboard_ref is a non-empty string-like token. +# @POST Returns dashboard id when uniquely matched, otherwise None. def _resolve_dashboard_id_by_ref( dashboard_ref: Optional[str], env_id: Optional[str], @@ -191,13 +184,12 @@ def _resolve_dashboard_id_by_ref( return None -# [/DEF:_resolve_dashboard_id_by_ref:Function] +# #endregion _resolve_dashboard_id_by_ref -# [DEF:_resolve_dashboard_id_entity:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback. -# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref. -# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable. +# #region _resolve_dashboard_id_entity [C:2] [TYPE Function] +# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback. +# @PRE entities may contain dashboard_id as int/str and optional dashboard_ref. +# @POST Returns resolved dashboard id or None when ambiguous/unresolvable. def _resolve_dashboard_id_entity( entities: Dict[str, Any], config_manager: ConfigManager, @@ -233,13 +225,12 @@ def _resolve_dashboard_id_entity( return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager) -# [/DEF:_resolve_dashboard_id_entity:Function] +# #endregion _resolve_dashboard_id_entity -# [DEF:_get_environment_name_by_id:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve human-readable environment name by id. -# @PRE: environment id may be None. -# @POST: Returns matching environment name or fallback id. +# #region _get_environment_name_by_id [C:2] [TYPE Function] +# @BRIEF Resolve human-readable environment name by id. +# @PRE environment id may be None. +# @POST Returns matching environment name or fallback id. def _get_environment_name_by_id( env_id: Optional[str], config_manager: ConfigManager ) -> str: @@ -251,13 +242,12 @@ def _get_environment_name_by_id( return env.name if env else env_id -# [/DEF:_get_environment_name_by_id:Function] +# #endregion _get_environment_name_by_id -# [DEF:_extract_result_deep_links:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Build deep-link actions to verify task result from assistant chat. -# @PRE: task object is available. -# @POST: Returns zero or more assistant actions for dashboard open/diff. +# #region _extract_result_deep_links [C:2] [TYPE Function] +# @BRIEF Build deep-link actions to verify task result from assistant chat. +# @PRE task object is available. +# @POST Returns zero or more assistant actions for dashboard open/diff. def _extract_result_deep_links( task: Any, config_manager: ConfigManager ) -> List: @@ -325,13 +315,12 @@ def _extract_result_deep_links( return actions -# [/DEF:_extract_result_deep_links:Function] +# #endregion _extract_result_deep_links -# [DEF:_build_task_observability_summary:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Build compact textual summary for completed tasks to reduce "black box" effect. -# @PRE: task may contain plugin-specific result payload. -# @POST: Returns non-empty summary line for known task types or empty string fallback. +# #region _build_task_observability_summary [C:2] [TYPE Function] +# @BRIEF Build compact textual summary for completed tasks to reduce "black box" effect. +# @PRE task may contain plugin-specific result payload. +# @POST Returns non-empty summary line for known task types or empty string fallback. def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str: plugin_id = getattr(task, "plugin_id", None) status = str(getattr(task, "status", "")).upper() @@ -392,6 +381,6 @@ def _build_task_observability_summary(task: Any, config_manager: ConfigManager) return "" -# [/DEF:_build_task_observability_summary:Function] +# #endregion _build_task_observability_summary -# [/DEF:AssistantResolvers:Module] +# #endregion AssistantResolvers diff --git a/backend/src/api/routes/assistant/_routes.py b/backend/src/api/routes/assistant/_routes.py index b197dad3..a4a7947d 100644 --- a/backend/src/api/routes/assistant/_routes.py +++ b/backend/src/api/routes/assistant/_routes.py @@ -1,15 +1,14 @@ -# [DEF:AssistantRoutes:Module] -# @COMPLEXITY: 5 -# @PURPOSE: FastAPI route handlers for the assistant API — message sending, confirmation, conversation management. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AssistantSchemas] -# @RELATION: DEPENDS_ON -> [AssistantHistory] -# @RELATION: DEPENDS_ON -> [AssistantCommandParser] -# @RELATION: DEPENDS_ON -> [AssistantLlmPlanner] -# @RELATION: DEPENDS_ON -> [AssistantDatasetReview] -# @RELATION: DEPENDS_ON -> [AssistantDispatch] -# @RELATION: DISPATCHES -> [AssistantAdminRoutes] -# @INVARIANT: Risky operations are never executed without valid confirmation token. +# #region AssistantRoutes [C:5] [TYPE Module] +# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management. +# @LAYER API +# @INVARIANT Risky operations are never executed without valid confirmation token. +# @RELATION DEPENDS_ON -> [AssistantSchemas] +# @RELATION DEPENDS_ON -> [AssistantHistory] +# @RELATION DEPENDS_ON -> [AssistantCommandParser] +# @RELATION DEPENDS_ON -> [AssistantLlmPlanner] +# @RELATION DEPENDS_ON -> [AssistantDatasetReview] +# @RELATION DEPENDS_ON -> [AssistantDispatch] +# @RELATION DISPATCHES -> [AssistantAdminRoutes] from __future__ import annotations @@ -77,21 +76,20 @@ router = APIRouter(tags=["Assistant"]) @router.post("/messages", response_model=AssistantMessageResponse) -# [DEF:send_message:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Parse assistant command, enforce safety gates, and dispatch executable intent. -# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse] -# @RELATION: DEPENDS_ON -> [_plan_intent_with_llm] -# @RELATION: DEPENDS_ON -> [_parse_command] -# @RELATION: DEPENDS_ON -> [_dispatch_intent] -# @RELATION: DEPENDS_ON -> [_append_history] -# @RELATION: DEPENDS_ON -> [_persist_message] -# @RELATION: DEPENDS_ON -> [_audit] -# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records. -# @PRE: Authenticated user is available and message text is non-empty. -# @POST: Response state is one of clarification/confirmation/started/success/denied/failed. -# @RETURN: AssistantMessageResponse with operation feedback and optional actions. -# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint. +# #region send_message [C:5] [TYPE Function] +# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent. +# @PRE Authenticated user is available and message text is non-empty. +# @POST Response state is one of clarification/confirmation/started/success/denied/failed. +# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records. +# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse] +# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint. +# @RETURN AssistantMessageResponse with operation feedback and optional actions. +# @RELATION DEPENDS_ON -> [_plan_intent_with_llm] +# @RELATION DEPENDS_ON -> [_parse_command] +# @RELATION DEPENDS_ON -> [_dispatch_intent] +# @RELATION DEPENDS_ON -> [_append_history] +# @RELATION DEPENDS_ON -> [_persist_message] +# @RELATION DEPENDS_ON -> [_audit] async def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)): with belief_scope('send_message'): logger.reason('Belief protocol reasoning checkpoint for send_message') @@ -166,18 +164,17 @@ async def send_message(request: AssistantMessageRequest, current_user: User=Depe return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.utcnow()) -# [/DEF:send_message:Function] +# #endregion send_message @router.post( "/confirmations/{confirmation_id}/confirm", response_model=AssistantMessageResponse ) -# [DEF:confirm_operation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Execute previously requested risky operation after explicit user confirmation. -# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired. -# @POST: Confirmation state becomes consumed and operation result is persisted in history. -# @RETURN: AssistantMessageResponse with task details when async execution starts. +# #region confirm_operation [C:2] [TYPE Function] +# @BRIEF Execute previously requested risky operation after explicit user confirmation. +# @PRE confirmation_id exists, belongs to current user, is pending, and not expired. +# @POST Confirmation state becomes consumed and operation result is persisted in history. +# @RETURN AssistantMessageResponse with task details when async execution starts. async def confirm_operation( confirmation_id: str, current_user: User = Depends(get_current_user), @@ -255,18 +252,17 @@ async def confirm_operation( ) -# [/DEF:confirm_operation:Function] +# #endregion confirm_operation @router.post( "/confirmations/{confirmation_id}/cancel", response_model=AssistantMessageResponse ) -# [DEF:cancel_operation:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Cancel pending risky operation and mark confirmation token as cancelled. -# @PRE: confirmation_id exists, belongs to current user, and is still pending. -# @POST: Confirmation becomes cancelled and cannot be executed anymore. -# @RETURN: AssistantMessageResponse confirming cancellation. +# #region cancel_operation [C:2] [TYPE Function] +# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled. +# @PRE confirmation_id exists, belongs to current user, and is still pending. +# @POST Confirmation becomes cancelled and cannot be executed anymore. +# @RETURN AssistantMessageResponse confirming cancellation. async def cancel_operation( confirmation_id: str, current_user: User = Depends(get_current_user), @@ -332,7 +328,7 @@ async def cancel_operation( ) -# [/DEF:cancel_operation:Function] +# #endregion cancel_operation -# [/DEF:AssistantRoutes:Module] +# #endregion AssistantRoutes diff --git a/backend/src/api/routes/assistant/_schemas.py b/backend/src/api/routes/assistant/_schemas.py index 7087f47b..a27b37af 100644 --- a/backend/src/api/routes/assistant/_schemas.py +++ b/backend/src/api/routes/assistant/_schemas.py @@ -1,10 +1,9 @@ -# [DEF:AssistantSchemas:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Pydantic models, in-memory stores, and permission mappings for the assistant API. -# @LAYER: API -# @RELATION: USED_BY -> [AssistantRoutes] -# @RELATION: USED_BY -> [AssistantHistory] -# @INVARIANT: In-memory stores are module-level singletons shared across the assistant package. +# #region AssistantSchemas [C:2] [TYPE Module] +# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API. +# @LAYER API +# @INVARIANT In-memory stores are module-level singletons shared across the assistant package. +# @RELATION USED_BY -> [AssistantRoutes] +# @RELATION USED_BY -> [AssistantHistory] from __future__ import annotations @@ -16,53 +15,50 @@ from pydantic import BaseModel, Field from src.schemas.auth import User -# [DEF:AssistantMessageRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Input payload for assistant message endpoint. -# @DATA_CONTRACT: Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest] -# @RELATION: USED_BY -> [send_message] -# @SIDE_EFFECT: None (schema declaration only). -# @PRE: message length is within accepted bounds. -# @POST: Request object provides message text and optional conversation binding. -# @INVARIANT: message is always non-empty and no longer than 4000 characters. +# #region AssistantMessageRequest [C:1] [TYPE Class] +# @BRIEF Input payload for assistant message endpoint. +# @PRE message length is within accepted bounds. +# @POST Request object provides message text and optional conversation binding. +# @SIDE_EFFECT None (schema declaration only). +# @DATA_CONTRACT Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest] +# @INVARIANT message is always non-empty and no longer than 4000 characters. +# @RELATION USED_BY -> [send_message] class AssistantMessageRequest(BaseModel): conversation_id: Optional[str] = None message: str = Field(..., min_length=1, max_length=4000) dataset_review_session_id: Optional[str] = None -# [/DEF:AssistantMessageRequest:Class] +# #endregion AssistantMessageRequest -# [DEF:AssistantAction:Class] -# @COMPLEXITY: 1 -# @PURPOSE: UI action descriptor returned with assistant responses. -# @DATA_CONTRACT: Input[type:str, label:str, target?:str] -> Output[AssistantAction] -# @RELATION: USED_BY -> [AssistantMessageResponse] -# @SIDE_EFFECT: None (schema declaration only). -# @PRE: type and label are provided by orchestration logic. -# @POST: Action can be rendered as button on frontend. -# @INVARIANT: type and label are required for every UI action. +# #region AssistantAction [C:1] [TYPE Class] +# @BRIEF UI action descriptor returned with assistant responses. +# @PRE type and label are provided by orchestration logic. +# @POST Action can be rendered as button on frontend. +# @SIDE_EFFECT None (schema declaration only). +# @DATA_CONTRACT Input[type:str, label:str, target?:str] -> Output[AssistantAction] +# @INVARIANT type and label are required for every UI action. +# @RELATION USED_BY -> [AssistantMessageResponse] class AssistantAction(BaseModel): type: str label: str target: Optional[str] = None -# [/DEF:AssistantAction:Class] +# #endregion AssistantAction -# [DEF:AssistantMessageResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Output payload contract for assistant interaction endpoints. -# @DATA_CONTRACT: Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse] -# @RELATION: RETURNED_BY -> [send_message] -# @RELATION: RETURNED_BY -> [confirm_operation] -# @RELATION: RETURNED_BY -> [cancel_operation] -# @SIDE_EFFECT: None (schema declaration only). -# @PRE: Response includes deterministic state and text. -# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up. -# @INVARIANT: created_at and state are always present in endpoint responses. +# #region AssistantMessageResponse [C:1] [TYPE Class] +# @BRIEF Output payload contract for assistant interaction endpoints. +# @PRE Response includes deterministic state and text. +# @POST Payload may include task_id/confirmation_id/actions for UI follow-up. +# @SIDE_EFFECT None (schema declaration only). +# @DATA_CONTRACT Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse] +# @INVARIANT created_at and state are always present in endpoint responses. +# @RELATION RETURNED_BY -> [send_message] +# @RELATION RETURNED_BY -> [confirm_operation] +# @RELATION RETURNED_BY -> [cancel_operation] class AssistantMessageResponse(BaseModel): conversation_id: str response_id: str @@ -75,20 +71,19 @@ class AssistantMessageResponse(BaseModel): created_at: datetime -# [/DEF:AssistantMessageResponse:Class] +# #endregion AssistantMessageResponse -# [DEF:ConfirmationRecord:Class] -# @COMPLEXITY: 1 -# @PURPOSE: In-memory confirmation token model for risky operation dispatch. -# @DATA_CONTRACT: Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord] -# @RELATION: USED_BY -> [send_message] -# @RELATION: USED_BY -> [confirm_operation] -# @RELATION: USED_BY -> [cancel_operation] -# @SIDE_EFFECT: None (schema declaration only). -# @PRE: intent/dispatch/user_id are populated at confirmation request time. -# @POST: Record tracks lifecycle state and expiry timestamp. -# @INVARIANT: state defaults to "pending" and expires_at bounds confirmation validity. +# #region ConfirmationRecord [C:1] [TYPE Class] +# @BRIEF In-memory confirmation token model for risky operation dispatch. +# @PRE intent/dispatch/user_id are populated at confirmation request time. +# @POST Record tracks lifecycle state and expiry timestamp. +# @SIDE_EFFECT None (schema declaration only). +# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord] +# @INVARIANT state defaults to "pending" and expires_at bounds confirmation validity. +# @RELATION USED_BY -> [send_message] +# @RELATION USED_BY -> [confirm_operation] +# @RELATION USED_BY -> [cancel_operation] class ConfirmationRecord(BaseModel): id: str user_id: str @@ -100,7 +95,7 @@ class ConfirmationRecord(BaseModel): created_at: datetime -# [/DEF:ConfirmationRecord:Class] +# #endregion ConfirmationRecord # --- In-memory stores --- @@ -146,4 +141,4 @@ INTENT_PERMISSION_CHECKS: Dict[str, List[Tuple[str, str]]] = { } -# [/DEF:AssistantSchemas:Module] +# #endregion AssistantSchemas diff --git a/backend/src/api/routes/clean_release.py b/backend/src/api/routes/clean_release.py index dee53be8..9659abf4 100644 --- a/backend/src/api/routes/clean_release.py +++ b/backend/src/api/routes/clean_release.py @@ -1,14 +1,12 @@ -# [DEF:CleanReleaseApi:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: api, clean-release, candidate-preparation, compliance -# @PURPOSE: Expose clean release endpoints for candidate preparation and subsequent compliance flow. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [get_clean_release_repository] -# @RELATION: DEPENDS_ON -> [PreparationService] -# @PRE: Clean release repository and preparation service dependencies are configured for the current request scope. -# @POST: Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation. -# @SIDE_EFFECT: Persists candidate/compliance lifecycle state and triggers clean-release orchestration services. -# @INVARIANT: API never reports prepared status if preparation errors are present. +# #region CleanReleaseApi [C:4] [TYPE Module] [SEMANTICS api, clean-release, candidate-preparation, compliance] +# @BRIEF Expose clean release endpoints for candidate preparation and subsequent compliance flow. +# @LAYER API +# @PRE Clean release repository and preparation service dependencies are configured for the current request scope. +# @POST Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation. +# @SIDE_EFFECT Persists candidate/compliance lifecycle state and triggers clean-release orchestration services. +# @INVARIANT API never reports prepared status if preparation errors are present. +# @RELATION DEPENDS_ON -> [get_clean_release_repository] +# @RELATION DEPENDS_ON -> [PreparationService] from __future__ import annotations @@ -55,8 +53,8 @@ from ...models.clean_release import ( router = APIRouter(prefix="/api/clean-release", tags=["Clean Release"]) -# [DEF:PrepareCandidateRequest:Class] -# @PURPOSE: Request schema for candidate preparation endpoint. +# #region PrepareCandidateRequest [TYPE Class] +# @BRIEF Request schema for candidate preparation endpoint. class PrepareCandidateRequest(BaseModel): candidate_id: str = Field(min_length=1) artifacts: List[Dict[str, Any]] = Field(default_factory=list) @@ -64,11 +62,11 @@ class PrepareCandidateRequest(BaseModel): operator_id: str = Field(min_length=1) -# [/DEF:PrepareCandidateRequest:Class] +# #endregion PrepareCandidateRequest -# [DEF:StartCheckRequest:Class] -# @PURPOSE: Request schema for clean compliance check run startup. +# #region StartCheckRequest [TYPE Class] +# @BRIEF Request schema for clean compliance check run startup. class StartCheckRequest(BaseModel): candidate_id: str = Field(min_length=1) profile: str = Field(default="enterprise-clean") @@ -76,11 +74,11 @@ class StartCheckRequest(BaseModel): triggered_by: str = Field(default="system") -# [/DEF:StartCheckRequest:Class] +# #endregion StartCheckRequest -# [DEF:RegisterCandidateRequest:Class] -# @PURPOSE: Request schema for candidate registration endpoint. +# #region RegisterCandidateRequest [TYPE Class] +# @BRIEF Request schema for candidate registration endpoint. class RegisterCandidateRequest(BaseModel): id: str = Field(min_length=1) version: str = Field(min_length=1) @@ -88,41 +86,41 @@ class RegisterCandidateRequest(BaseModel): created_by: str = Field(min_length=1) -# [/DEF:RegisterCandidateRequest:Class] +# #endregion RegisterCandidateRequest -# [DEF:ImportArtifactsRequest:Class] -# @PURPOSE: Request schema for candidate artifact import endpoint. +# #region ImportArtifactsRequest [TYPE Class] +# @BRIEF Request schema for candidate artifact import endpoint. class ImportArtifactsRequest(BaseModel): artifacts: List[Dict[str, Any]] = Field(default_factory=list) -# [/DEF:ImportArtifactsRequest:Class] +# #endregion ImportArtifactsRequest -# [DEF:BuildManifestRequest:Class] -# @PURPOSE: Request schema for manifest build endpoint. +# #region BuildManifestRequest [TYPE Class] +# @BRIEF Request schema for manifest build endpoint. class BuildManifestRequest(BaseModel): created_by: str = Field(default="system") -# [/DEF:BuildManifestRequest:Class] +# #endregion BuildManifestRequest -# [DEF:CreateComplianceRunRequest:Class] -# @PURPOSE: Request schema for compliance run creation with optional manifest pinning. +# #region CreateComplianceRunRequest [TYPE Class] +# @BRIEF Request schema for compliance run creation with optional manifest pinning. class CreateComplianceRunRequest(BaseModel): requested_by: str = Field(min_length=1) manifest_id: str | None = None -# [/DEF:CreateComplianceRunRequest:Class] +# #endregion CreateComplianceRunRequest -# [DEF:register_candidate_v2_endpoint:Function] -# @PURPOSE: Register a clean-release candidate for headless lifecycle. -# @PRE: Candidate identifier is unique. -# @POST: Candidate is persisted in DRAFT status. +# #region register_candidate_v2_endpoint [TYPE Function] +# @BRIEF Register a clean-release candidate for headless lifecycle. +# @PRE Candidate identifier is unique. +# @POST Candidate is persisted in DRAFT status. @router.post( "/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED ) @@ -157,13 +155,13 @@ async def register_candidate_v2_endpoint( ) -# [/DEF:register_candidate_v2_endpoint:Function] +# #endregion register_candidate_v2_endpoint -# [DEF:import_candidate_artifacts_v2_endpoint:Function] -# @PURPOSE: Import candidate artifacts in headless flow. -# @PRE: Candidate exists and artifacts array is non-empty. -# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT. +# #region import_candidate_artifacts_v2_endpoint [TYPE Function] +# @BRIEF Import candidate artifacts in headless flow. +# @PRE Candidate exists and artifacts array is non-empty. +# @POST Artifacts are persisted and candidate advances to PREPARED if it was DRAFT. @router.post("/candidates/{candidate_id}/artifacts") async def import_candidate_artifacts_v2_endpoint( candidate_id: str, @@ -215,13 +213,13 @@ async def import_candidate_artifacts_v2_endpoint( return {"status": "success"} -# [/DEF:import_candidate_artifacts_v2_endpoint:Function] +# #endregion import_candidate_artifacts_v2_endpoint -# [DEF:build_candidate_manifest_v2_endpoint:Function] -# @PURPOSE: Build immutable manifest snapshot for prepared candidate. -# @PRE: Candidate exists and has imported artifacts. -# @POST: Returns created ManifestDTO with incremented version. +# #region build_candidate_manifest_v2_endpoint [TYPE Function] +# @BRIEF Build immutable manifest snapshot for prepared candidate. +# @PRE Candidate exists and has imported artifacts. +# @POST Returns created ManifestDTO with incremented version. @router.post( "/candidates/{candidate_id}/manifests", response_model=ManifestDTO, @@ -259,13 +257,13 @@ async def build_candidate_manifest_v2_endpoint( ) -# [/DEF:build_candidate_manifest_v2_endpoint:Function] +# #endregion build_candidate_manifest_v2_endpoint -# [DEF:get_candidate_overview_v2_endpoint:Function] -# @PURPOSE: Return expanded candidate overview DTO for headless lifecycle visibility. -# @PRE: Candidate exists. -# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints. +# #region get_candidate_overview_v2_endpoint [TYPE Function] +# @BRIEF Return expanded candidate overview DTO for headless lifecycle visibility. +# @PRE Candidate exists. +# @POST Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints. @router.get("/candidates/{candidate_id}/overview", response_model=CandidateOverviewDTO) async def get_candidate_overview_v2_endpoint( candidate_id: str, @@ -375,13 +373,13 @@ async def get_candidate_overview_v2_endpoint( ) -# [/DEF:get_candidate_overview_v2_endpoint:Function] +# #endregion get_candidate_overview_v2_endpoint -# [DEF:prepare_candidate_endpoint:Function] -# @PURPOSE: Prepare candidate with policy evaluation and deterministic manifest generation. -# @PRE: Candidate and active policy exist in repository. -# @POST: Returns preparation result including manifest reference and violations. +# #region prepare_candidate_endpoint [TYPE Function] +# @BRIEF Prepare candidate with policy evaluation and deterministic manifest generation. +# @PRE Candidate and active policy exist in repository. +# @POST Returns preparation result including manifest reference and violations. @router.post("/candidates/prepare") async def prepare_candidate_endpoint( payload: PrepareCandidateRequest, @@ -409,13 +407,13 @@ async def prepare_candidate_endpoint( ) -# [/DEF:prepare_candidate_endpoint:Function] +# #endregion prepare_candidate_endpoint -# [DEF:start_check:Function] -# @PURPOSE: Start and finalize a clean compliance check run and persist report artifacts. -# @PRE: Active policy and candidate exist. -# @POST: Returns accepted payload with check_run_id and started_at. +# #region start_check [TYPE Function] +# @BRIEF Start and finalize a clean compliance check run and persist report artifacts. +# @PRE Active policy and candidate exist. +# @POST Returns accepted payload with check_run_id and started_at. @router.post("/checks", status_code=status.HTTP_202_ACCEPTED) async def start_check( payload: StartCheckRequest, @@ -545,13 +543,13 @@ async def start_check( } -# [/DEF:start_check:Function] +# #endregion start_check -# [DEF:get_check_status:Function] -# @PURPOSE: Return terminal/intermediate status payload for a check run. -# @PRE: check_run_id references an existing run. -# @POST: Deterministic payload shape includes checks and violations arrays. +# #region get_check_status [TYPE Function] +# @BRIEF Return terminal/intermediate status payload for a check run. +# @PRE check_run_id references an existing run. +# @POST Deterministic payload shape includes checks and violations arrays. @router.get("/checks/{check_run_id}") async def get_check_status( check_run_id: str, @@ -597,13 +595,13 @@ async def get_check_status( } -# [/DEF:get_check_status:Function] +# #endregion get_check_status -# [DEF:get_report:Function] -# @PURPOSE: Return persisted compliance report by report_id. -# @PRE: report_id references an existing report. -# @POST: Returns serialized report object. +# #region get_report [TYPE Function] +# @BRIEF Return persisted compliance report by report_id. +# @PRE report_id references an existing report. +# @POST Returns serialized report object. @router.get("/reports/{report_id}") async def get_report( report_id: str, @@ -635,5 +633,5 @@ async def get_report( } -# [/DEF:get_report:Function] -# [/DEF:CleanReleaseApi:Module] +# #endregion get_report +# #endregion CleanReleaseApi diff --git a/backend/src/api/routes/clean_release_v2.py b/backend/src/api/routes/clean_release_v2.py index e03903c1..83dc71ed 100644 --- a/backend/src/api/routes/clean_release_v2.py +++ b/backend/src/api/routes/clean_release_v2.py @@ -1,13 +1,12 @@ -# [DEF:CleanReleaseV2Api:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Redesigned clean release API for headless candidate lifecycle. -# @LAYER: UI (API) -# @RELATION: DEPENDS_ON -> [CleanReleaseRepository] -# @RELATION: CALLS -> [approve_candidate] -# @RELATION: CALLS -> [publish_candidate] -# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints. -# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes. -# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters. +# #region CleanReleaseV2Api [C:4] [TYPE Module] +# @BRIEF Redesigned clean release API for headless candidate lifecycle. +# @LAYER UI (API) +# @PRE Clean release repository dependency is available for candidate lifecycle endpoints. +# @POST Candidate registration, approval, publication, and revocation routes are registered without behavior changes. +# @SIDE_EFFECT Persists candidate lifecycle state through clean release services and repository adapters. +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] +# @RELATION CALLS -> [approve_candidate] +# @RELATION CALLS -> [publish_candidate] from fastapi import APIRouter, Depends, HTTPException, status from typing import List, Dict, Any @@ -33,44 +32,40 @@ from ...services.clean_release.dto import CandidateDTO, ManifestDTO router = APIRouter(prefix="/api/v2/clean-release", tags=["Clean Release V2"]) -# [DEF:ApprovalRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Schema for approval request payload. +# #region ApprovalRequest [C:1] [TYPE Class] +# @BRIEF Schema for approval request payload. class ApprovalRequest(dict): pass -# [/DEF:ApprovalRequest:Class] +# #endregion ApprovalRequest -# [DEF:PublishRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Schema for publication request payload. +# #region PublishRequest [C:1] [TYPE Class] +# @BRIEF Schema for publication request payload. class PublishRequest(dict): pass -# [/DEF:PublishRequest:Class] +# #endregion PublishRequest -# [DEF:RevokeRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Schema for revocation request payload. +# #region RevokeRequest [C:1] [TYPE Class] +# @BRIEF Schema for revocation request payload. class RevokeRequest(dict): pass -# [/DEF:RevokeRequest:Class] +# #endregion RevokeRequest -# [DEF:register_candidate:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Register a new release candidate. -# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by). -# @POST: Candidate is saved in repository. -# @RETURN: CandidateDTO -# @RELATION: DEPENDS_ON -> [CleanReleaseRepository] -# @RELATION: DEPENDS_ON -> [clean_release_dto] +# #region register_candidate [C:3] [TYPE Function] +# @BRIEF Register a new release candidate. +# @PRE Payload contains required fields (id, version, source_snapshot_ref, created_by). +# @POST Candidate is saved in repository. +# @RETURN CandidateDTO +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] +# @RELATION DEPENDS_ON -> [clean_release_dto] @router.post( "/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED ) @@ -97,15 +92,14 @@ async def register_candidate( ) -# [/DEF:register_candidate:Function] +# #endregion register_candidate -# [DEF:import_artifacts:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Associate artifacts with a release candidate. -# @PRE: Candidate exists. -# @POST: Artifacts are processed (placeholder). -# @RELATION: DEPENDS_ON -> [CleanReleaseRepository] +# #region import_artifacts [C:3] [TYPE Function] +# @BRIEF Associate artifacts with a release candidate. +# @PRE Candidate exists. +# @POST Artifacts are processed (placeholder). +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] @router.post("/candidates/{candidate_id}/artifacts") async def import_artifacts( candidate_id: str, @@ -131,16 +125,15 @@ async def import_artifacts( return {"status": "success"} -# [/DEF:import_artifacts:Function] +# #endregion import_artifacts -# [DEF:build_manifest:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Generate distribution manifest for a candidate. -# @PRE: Candidate exists. -# @POST: Manifest is created and saved. -# @RETURN: ManifestDTO -# @RELATION: DEPENDS_ON -> [CleanReleaseRepository] +# #region build_manifest [C:3] [TYPE Function] +# @BRIEF Generate distribution manifest for a candidate. +# @PRE Candidate exists. +# @POST Manifest is created and saved. +# @RETURN ManifestDTO +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] @router.post( "/candidates/{candidate_id}/manifests", response_model=ManifestDTO, @@ -180,13 +173,12 @@ async def build_manifest( ) -# [/DEF:build_manifest:Function] +# #endregion build_manifest -# [DEF:approve_candidate_endpoint:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Endpoint to record candidate approval. -# @RELATION: CALLS -> [approve_candidate] +# #region approve_candidate_endpoint [C:3] [TYPE Function] +# @BRIEF Endpoint to record candidate approval. +# @RELATION CALLS -> [approve_candidate] @router.post("/candidates/{candidate_id}/approve") async def approve_candidate_endpoint( candidate_id: str, @@ -209,13 +201,12 @@ async def approve_candidate_endpoint( return {"status": "ok", "decision": decision.decision, "decision_id": decision.id} -# [/DEF:approve_candidate_endpoint:Function] +# #endregion approve_candidate_endpoint -# [DEF:reject_candidate_endpoint:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Endpoint to record candidate rejection. -# @RELATION: CALLS -> [reject_candidate] +# #region reject_candidate_endpoint [C:3] [TYPE Function] +# @BRIEF Endpoint to record candidate rejection. +# @RELATION CALLS -> [reject_candidate] @router.post("/candidates/{candidate_id}/reject") async def reject_candidate_endpoint( candidate_id: str, @@ -238,13 +229,12 @@ async def reject_candidate_endpoint( return {"status": "ok", "decision": decision.decision, "decision_id": decision.id} -# [/DEF:reject_candidate_endpoint:Function] +# #endregion reject_candidate_endpoint -# [DEF:publish_candidate_endpoint:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Endpoint to publish an approved candidate. -# @RELATION: CALLS -> [publish_candidate] +# #region publish_candidate_endpoint [C:3] [TYPE Function] +# @BRIEF Endpoint to publish an approved candidate. +# @RELATION CALLS -> [publish_candidate] @router.post("/candidates/{candidate_id}/publish") async def publish_candidate_endpoint( candidate_id: str, @@ -283,13 +273,12 @@ async def publish_candidate_endpoint( } -# [/DEF:publish_candidate_endpoint:Function] +# #endregion publish_candidate_endpoint -# [DEF:revoke_publication_endpoint:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Endpoint to revoke a previous publication. -# @RELATION: CALLS -> [revoke_publication] +# #region revoke_publication_endpoint [C:3] [TYPE Function] +# @BRIEF Endpoint to revoke a previous publication. +# @RELATION CALLS -> [revoke_publication] @router.post("/publications/{publication_id}/revoke") async def revoke_publication_endpoint( publication_id: str, @@ -326,6 +315,6 @@ async def revoke_publication_endpoint( } -# [/DEF:revoke_publication_endpoint:Function] +# #endregion revoke_publication_endpoint -# [/DEF:CleanReleaseV2Api:Module] +# #endregion CleanReleaseV2Api diff --git a/backend/src/api/routes/connections.py b/backend/src/api/routes/connections.py index 522bc135..c415be4f 100644 --- a/backend/src/api/routes/connections.py +++ b/backend/src/api/routes/connections.py @@ -1,10 +1,8 @@ -# [DEF:ConnectionsRouter:Module] -# @SEMANTICS: api, router, connections, database -# @PURPOSE: Defines the FastAPI router for managing external database connections. -# @COMPLEXITY: 3 -# @LAYER: UI (API) -# @RELATION: DEPENDS_ON -> [get_db] -# @RELATION: DEPENDS_ON -> [ConnectionConfig] +# #region ConnectionsRouter [C:3] [TYPE Module] [SEMANTICS api, router, connections, database] +# @BRIEF Defines the FastAPI router for managing external database connections. +# @LAYER UI (API) +# @RELATION DEPENDS_ON -> [get_db] +# @RELATION DEPENDS_ON -> [ConnectionConfig] # [SECTION: IMPORTS] from typing import List, Optional @@ -20,24 +18,22 @@ from ...core.logger import logger, belief_scope router = APIRouter() -# [DEF:_ensure_connections_schema:Function] -# @PURPOSE: Ensures the connection_configs table exists before CRUD access. -# @COMPLEXITY: 3 -# @PRE: db is an active SQLAlchemy session. -# @POST: The current bind can safely query ConnectionConfig. -# @RELATION: CALLS -> ensure_connection_configs_table +# #region _ensure_connections_schema [C:3] [TYPE Function] +# @BRIEF Ensures the connection_configs table exists before CRUD access. +# @PRE db is an active SQLAlchemy session. +# @POST The current bind can safely query ConnectionConfig. +# @RELATION CALLS -> [ensure_connection_configs_table] def _ensure_connections_schema(db: Session): with belief_scope("ConnectionsRouter.ensure_schema"): ensure_connection_configs_table(db.get_bind()) -# [/DEF:_ensure_connections_schema:Function] +# #endregion _ensure_connections_schema -# [DEF:ConnectionSchema:Class] -# @PURPOSE: Pydantic model for connection response. -# @COMPLEXITY: 3 -# @RELATION: BINDS_TO -> ConnectionConfig +# #region ConnectionSchema [C:3] [TYPE Class] +# @BRIEF Pydantic model for connection response. +# @RELATION BINDS_TO -> [ConnectionConfig] class ConnectionSchema(BaseModel): id: str name: str @@ -52,13 +48,12 @@ class ConnectionSchema(BaseModel): orm_mode = True -# [/DEF:ConnectionSchema:Class] +# #endregion ConnectionSchema -# [DEF:ConnectionCreate:Class] -# @PURPOSE: Pydantic model for creating a connection. -# @COMPLEXITY: 3 -# @RELATION: BINDS_TO -> ConnectionConfig +# #region ConnectionCreate [C:3] [TYPE Class] +# @BRIEF Pydantic model for creating a connection. +# @RELATION BINDS_TO -> [ConnectionConfig] class ConnectionCreate(BaseModel): name: str type: str @@ -69,14 +64,13 @@ class ConnectionCreate(BaseModel): password: Optional[str] = None -# [/DEF:ConnectionCreate:Class] +# #endregion ConnectionCreate -# [DEF:list_connections:Function] -# @PURPOSE: Lists all saved connections. -# @COMPLEXITY: 3 -# @PRE: Database session is active. -# @POST: Returns list of connection configs. +# #region list_connections [C:3] [TYPE Function] +# @BRIEF Lists all saved connections. +# @PRE Database session is active. +# @POST Returns list of connection configs. # @PARAM: db (Session) - Database session. # @RETURN: List[ConnectionSchema] - List of connections. # @RELATION: CALLS -> _ensure_connections_schema @@ -89,14 +83,13 @@ async def list_connections(db: Session = Depends(get_db)): return connections -# [/DEF:list_connections:Function] +# #endregion list_connections -# [DEF:create_connection:Function] -# @PURPOSE: Creates a new connection configuration. -# @COMPLEXITY: 3 -# @PRE: Connection name is unique. -# @POST: Connection is saved to DB. +# #region create_connection [C:3] [TYPE Function] +# @BRIEF Creates a new connection configuration. +# @PRE Connection name is unique. +# @POST Connection is saved to DB. # @PARAM: connection (ConnectionCreate) - Config data. # @PARAM: db (Session) - Database session. # @RETURN: ConnectionSchema - Created connection. @@ -118,14 +111,13 @@ async def create_connection( return db_connection -# [/DEF:create_connection:Function] +# #endregion create_connection -# [DEF:delete_connection:Function] -# @PURPOSE: Deletes a connection configuration. -# @COMPLEXITY: 3 -# @PRE: Connection ID exists. -# @POST: Connection is removed from DB. +# #region delete_connection [C:3] [TYPE Function] +# @BRIEF Deletes a connection configuration. +# @PRE Connection ID exists. +# @POST Connection is removed from DB. # @PARAM: connection_id (str) - ID to delete. # @PARAM: db (Session) - Database session. # @RETURN: None. @@ -153,6 +145,6 @@ async def delete_connection(connection_id: str, db: Session = Depends(get_db)): return -# [/DEF:delete_connection:Function] +# #endregion delete_connection -# [/DEF:ConnectionsRouter:Module] +# #endregion ConnectionsRouter diff --git a/backend/src/api/routes/dashboards/__init__.py b/backend/src/api/routes/dashboards/__init__.py index 34536e07..cf2de5fb 100644 --- a/backend/src/api/routes/dashboards/__init__.py +++ b/backend/src/api/routes/dashboards/__init__.py @@ -1,19 +1,17 @@ -# [DEF:DashboardsApi:Module] +# #region DashboardsApi [C:5] [TYPE Module] [SEMANTICS api, dashboards, resources, hub] +# @BRIEF API endpoints for the Dashboard Hub - listing dashboards with Git and task status +# @LAYER API +# @PRE Valid environment configurations exist in ConfigManager. +# @POST Dashboard responses are projected into DashboardsResponse DTO. +# @SIDE_EFFECT Performs external calls to Superset API and potentially Git providers. +# @DATA_CONTRACT Input(env_id, filters) -> Output(DashboardsResponse) +# @INVARIANT All dashboard responses include git_status and last_task metadata +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [ResourceService] +# @RELATION DEPENDS_ON -> [SupersetClient] # -# @COMPLEXITY: 5 -# @SEMANTICS: api, dashboards, resources, hub -# @PURPOSE: API endpoints for the Dashboard Hub - listing dashboards with Git and task status -# @LAYER: API -# @RELATION: DEPENDS_ON ->[AppDependencies] -# @RELATION: DEPENDS_ON ->[ResourceService] -# @RELATION: DEPENDS_ON ->[SupersetClient] # -# @INVARIANT: All dashboard responses include git_status and last_task metadata # -# @PRE: Valid environment configurations exist in ConfigManager. -# @POST: Dashboard responses are projected into DashboardsResponse DTO. -# @SIDE_EFFECT: Performs external calls to Superset API and potentially Git providers. -# @DATA_CONTRACT: Input(env_id, filters) -> Output(DashboardsResponse) # # @TEST_CONTRACT: DashboardsAPI -> { # required_fields: {env_id: string, page: integer, page_size: integer}, @@ -41,4 +39,4 @@ from ._listing_routes import * from ._detail_routes import * from ._action_routes import * -# [/DEF:DashboardsApi:Module] +# #endregion DashboardsApi diff --git a/backend/src/api/routes/dashboards/_action_routes.py b/backend/src/api/routes/dashboards/_action_routes.py index d824ed91..277679f6 100644 --- a/backend/src/api/routes/dashboards/_action_routes.py +++ b/backend/src/api/routes/dashboards/_action_routes.py @@ -1,10 +1,8 @@ -# [DEF:DashboardActionRoutes:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: api, dashboards, actions, routes -# @PURPOSE: Dashboard action route handlers — migrate, backup. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DashboardRouter] -# @RELATION: DEPENDS_ON -> [DashboardSchemas] +# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS api, dashboards, actions, routes] +# @BRIEF Dashboard action route handlers — migrate, backup. +# @LAYER API +# @RELATION DEPENDS_ON -> [DashboardRouter] +# @RELATION DEPENDS_ON -> [DashboardSchemas] # [SECTION: IMPORTS] from fastapi import Depends, HTTPException @@ -24,14 +22,13 @@ from ._router import router # [/SECTION] -# [DEF:migrate_dashboards:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Trigger bulk migration of dashboards from source to target environment -# @PRE: User has permission plugin:migration:execute -# @PRE: source_env_id and target_env_id are valid environment IDs -# @PRE: dashboard_ids is a non-empty list -# @POST: Returns task_id for tracking migration progress -# @POST: Task is created and queued for execution +# #region migrate_dashboards [C:2] [TYPE Function] +# @BRIEF Trigger bulk migration of dashboards from source to target environment +# @PRE User has permission plugin:migration:execute +# @PRE source_env_id and target_env_id are valid environment IDs +# @PRE dashboard_ids is a non-empty list +# @POST Returns task_id for tracking migration progress +# @POST Task is created and queued for execution # @PARAM: request (MigrateRequest) - Migration request with source, target, and dashboard IDs # @RETURN: TaskResponse - Task ID for tracking # @RELATION: DISPATCHES ->[MigrationPlugin:execute] @@ -102,18 +99,17 @@ async def migrate_dashboards( ) -# [/DEF:migrate_dashboards:Function] +# #endregion migrate_dashboards -# [DEF:backup_dashboards:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Trigger bulk backup of dashboards with optional cron schedule -# @PRE: User has permission plugin:backup:execute -# @PRE: env_id is a valid environment ID -# @PRE: dashboard_ids is a non-empty list -# @POST: Returns task_id for tracking backup progress -# @POST: Task is created and queued for execution -# @POST: If schedule is provided, a scheduled task is created +# #region backup_dashboards [C:2] [TYPE Function] +# @BRIEF Trigger bulk backup of dashboards with optional cron schedule +# @PRE User has permission plugin:backup:execute +# @PRE env_id is a valid environment ID +# @PRE dashboard_ids is a non-empty list +# @POST Returns task_id for tracking backup progress +# @POST Task is created and queued for execution +# @POST If schedule is provided, a scheduled task is created # @PARAM: request (BackupRequest) - Backup request with environment and dashboard IDs # @RETURN: TaskResponse - Task ID for tracking # @RELATION: DISPATCHES ->[BackupPlugin:execute] @@ -172,5 +168,5 @@ async def backup_dashboards( ) -# [/DEF:backup_dashboards:Function] -# [/DEF:DashboardActionRoutes:Module] +# #endregion backup_dashboards +# #endregion DashboardActionRoutes diff --git a/backend/src/api/routes/dashboards/_detail_routes.py b/backend/src/api/routes/dashboards/_detail_routes.py index 71c689d4..e4e0d6ec 100644 --- a/backend/src/api/routes/dashboards/_detail_routes.py +++ b/backend/src/api/routes/dashboards/_detail_routes.py @@ -1,12 +1,10 @@ -# [DEF:DashboardDetailRoutes:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: api, dashboards, detail, routes -# @PURPOSE: Dashboard detail, db-mappings, task history, thumbnail route handlers. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DashboardRouter] -# @RELATION: DEPENDS_ON -> [DashboardSchemas] -# @RELATION: DEPENDS_ON -> [DashboardHelpers] -# @RELATION: DEPENDS_ON -> [DashboardProjection] +# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS api, dashboards, detail, routes] +# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers. +# @LAYER API +# @RELATION DEPENDS_ON -> [DashboardRouter] +# @RELATION DEPENDS_ON -> [DashboardSchemas] +# @RELATION DEPENDS_ON -> [DashboardHelpers] +# @RELATION DEPENDS_ON -> [DashboardProjection] # [SECTION: IMPORTS] from typing import Optional, List, Dict, Any @@ -44,12 +42,11 @@ from ._router import router # [/SECTION] -# [DEF:get_database_mappings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get database mapping suggestions between source and target environments -# @PRE: User has permission plugin:migration:read -# @PRE: source_env_id and target_env_id are valid environment IDs -# @POST: Returns list of suggested database mappings with confidence scores +# #region get_database_mappings [C:2] [TYPE Function] +# @BRIEF Get database mapping suggestions between source and target environments +# @PRE User has permission plugin:migration:read +# @PRE source_env_id and target_env_id are valid environment IDs +# @POST Returns list of suggested database mappings with confidence scores # @PARAM: source_env_id (str) - Source environment ID # @PARAM: target_env_id (str) - Target environment ID # @RETURN: DatabaseMappingsResponse - List of suggested mappings @@ -111,15 +108,14 @@ async def get_database_mappings( ) -# [/DEF:get_database_mappings:Function] +# #endregion get_database_mappings -# [DEF:get_dashboard_detail:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Fetch detailed dashboard info with related charts and datasets -# @PRE: env_id must be valid and dashboard ref (slug or id) must exist -# @POST: Returns dashboard detail payload for overview page -# @RELATION: CALLS ->[AsyncSupersetClient] +# #region get_dashboard_detail [C:2] [TYPE Function] +# @BRIEF Fetch detailed dashboard info with related charts and datasets +# @PRE env_id must be valid and dashboard ref (slug or id) must exist +# @POST Returns dashboard detail payload for overview page +# @RELATION CALLS -> [AsyncSupersetClient] @router.get("/{dashboard_ref}", response_model=DashboardDetailResponse) async def get_dashboard_detail( dashboard_ref: str, @@ -157,14 +153,13 @@ async def get_dashboard_detail( ) -# [/DEF:get_dashboard_detail:Function] +# #endregion get_dashboard_detail -# [DEF:get_dashboard_tasks_history:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard. -# @PRE: dashboard ref (slug or id) is valid. -# @POST: Response contains sorted task history (newest first). +# #region get_dashboard_tasks_history [C:2] [TYPE Function] +# @BRIEF Returns history of backup and LLM validation tasks for a dashboard. +# @PRE dashboard ref (slug or id) is valid. +# @POST Response contains sorted task history (newest first). @router.get("/{dashboard_ref}/tasks", response_model=DashboardTaskHistoryResponse) async def get_dashboard_tasks_history( dashboard_ref: str, @@ -260,15 +255,14 @@ async def get_dashboard_tasks_history( await client.aclose() -# [/DEF:get_dashboard_tasks_history:Function] +# #endregion get_dashboard_tasks_history -# [DEF:get_dashboard_thumbnail:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Proxies Superset dashboard thumbnail with cache support. -# @RELATION: CALLS ->[AsyncSupersetClient] -# @PRE: env_id must exist. -# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset. +# #region get_dashboard_thumbnail [C:3] [TYPE Function] +# @BRIEF Proxies Superset dashboard thumbnail with cache support. +# @PRE env_id must exist. +# @POST Returns image bytes or 202 when thumbnail is being prepared by Superset. +# @RELATION CALLS -> [AsyncSupersetClient] @router.get("/{dashboard_ref}/thumbnail") async def get_dashboard_thumbnail( dashboard_ref: str, @@ -388,5 +382,5 @@ async def get_dashboard_thumbnail( ) -# [/DEF:get_dashboard_thumbnail:Function] -# [/DEF:DashboardDetailRoutes:Module] +# #endregion get_dashboard_thumbnail +# #endregion DashboardDetailRoutes diff --git a/backend/src/api/routes/dashboards/_helpers.py b/backend/src/api/routes/dashboards/_helpers.py index 7a1cbf5c..e2429592 100644 --- a/backend/src/api/routes/dashboards/_helpers.py +++ b/backend/src/api/routes/dashboards/_helpers.py @@ -1,10 +1,8 @@ -# [DEF:DashboardHelpers:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: api, dashboards, helpers, resolution -# @PURPOSE: Basic helper functions for dashboard route handlers — slug resolution, filter normalization. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @RELATION: DEPENDS_ON -> [AsyncSupersetClient] +# #region DashboardHelpers [C:2] [TYPE Module] [SEMANTICS api, dashboards, helpers, resolution] +# @BRIEF Basic helper functions for dashboard route handlers — slug resolution, filter normalization. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [AsyncSupersetClient] from typing import List, Optional, Dict, Any from fastapi import HTTPException @@ -13,11 +11,10 @@ from src.core.async_superset_client import AsyncSupersetClient from src.core.logger import logger -# [DEF:_find_dashboard_id_by_slug:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard numeric ID by slug using Superset list endpoint. -# @PRE: `dashboard_slug` is non-empty. -# @POST: Returns dashboard ID when found, otherwise None. +# #region _find_dashboard_id_by_slug [C:2] [TYPE Function] +# @BRIEF Resolve dashboard numeric ID by slug using Superset list endpoint. +# @PRE `dashboard_slug` is non-empty. +# @POST Returns dashboard ID when found, otherwise None. def _find_dashboard_id_by_slug( client: SupersetClient, dashboard_slug: str, @@ -48,14 +45,13 @@ def _find_dashboard_id_by_slug( return None -# [/DEF:_find_dashboard_id_by_slug:Function] +# #endregion _find_dashboard_id_by_slug -# [DEF:_resolve_dashboard_id_from_ref:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard ID from slug-first reference with numeric fallback. -# @PRE: `dashboard_ref` is provided in route path. -# @POST: Returns a valid dashboard ID or raises HTTPException(404). +# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function] +# @BRIEF Resolve dashboard ID from slug-first reference with numeric fallback. +# @PRE `dashboard_ref` is provided in route path. +# @POST Returns a valid dashboard ID or raises HTTPException(404). def _resolve_dashboard_id_from_ref( dashboard_ref: str, client: SupersetClient, @@ -75,14 +71,13 @@ def _resolve_dashboard_id_from_ref( raise HTTPException(status_code=404, detail="Dashboard not found") -# [/DEF:_resolve_dashboard_id_from_ref:Function] +# #endregion _resolve_dashboard_id_from_ref -# [DEF:_find_dashboard_id_by_slug_async:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard numeric ID by slug using async Superset list endpoint. -# @PRE: dashboard_slug is non-empty. -# @POST: Returns dashboard ID when found, otherwise None. +# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function] +# @BRIEF Resolve dashboard numeric ID by slug using async Superset list endpoint. +# @PRE dashboard_slug is non-empty. +# @POST Returns dashboard ID when found, otherwise None. async def _find_dashboard_id_by_slug_async( client: AsyncSupersetClient, dashboard_slug: str, @@ -113,14 +108,13 @@ async def _find_dashboard_id_by_slug_async( return None -# [/DEF:_find_dashboard_id_by_slug_async:Function] +# #endregion _find_dashboard_id_by_slug_async -# [DEF:_resolve_dashboard_id_from_ref_async:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard ID from slug-first reference using async Superset client. -# @PRE: dashboard_ref is provided in route path. -# @POST: Returns valid dashboard ID or raises HTTPException(404). +# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function] +# @BRIEF Resolve dashboard ID from slug-first reference using async Superset client. +# @PRE dashboard_ref is provided in route path. +# @POST Returns valid dashboard ID or raises HTTPException(404). async def _resolve_dashboard_id_from_ref_async( dashboard_ref: str, client: AsyncSupersetClient, @@ -139,14 +133,13 @@ async def _resolve_dashboard_id_from_ref_async( raise HTTPException(status_code=404, detail="Dashboard not found") -# [/DEF:_resolve_dashboard_id_from_ref_async:Function] +# #endregion _resolve_dashboard_id_from_ref_async -# [DEF:_normalize_filter_values:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize query filter values to lower-cased non-empty tokens. -# @PRE: values may be None or list of strings. -# @POST: Returns trimmed normalized list preserving input order. +# #region _normalize_filter_values [C:2] [TYPE Function] +# @BRIEF Normalize query filter values to lower-cased non-empty tokens. +# @PRE values may be None or list of strings. +# @POST Returns trimmed normalized list preserving input order. def _normalize_filter_values(values: Optional[List[str]]) -> List[str]: if not values: return [] @@ -158,14 +151,13 @@ def _normalize_filter_values(values: Optional[List[str]]) -> List[str]: return normalized -# [/DEF:_normalize_filter_values:Function] +# #endregion _normalize_filter_values -# [DEF:_dashboard_git_filter_value:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Build comparable git status token for dashboards filtering. -# @PRE: dashboard payload may contain git_status or None. -# @POST: Returns one of ok|diff|no_repo|error|pending. +# #region _dashboard_git_filter_value [C:2] [TYPE Function] +# @BRIEF Build comparable git status token for dashboards filtering. +# @PRE dashboard payload may contain git_status or None. +# @POST Returns one of ok|diff|no_repo|error|pending. def _dashboard_git_filter_value(dashboard: Dict[str, Any]) -> str: git_status = dashboard.get("git_status") or {} sync_status = str(git_status.get("sync_status") or "").strip().upper() @@ -181,7 +173,7 @@ def _dashboard_git_filter_value(dashboard: Dict[str, Any]) -> str: return "pending" -# [/DEF:_dashboard_git_filter_value:Function] +# #endregion _dashboard_git_filter_value -# [/DEF:DashboardHelpers:Module] +# #endregion DashboardHelpers diff --git a/backend/src/api/routes/dashboards/_listing_routes.py b/backend/src/api/routes/dashboards/_listing_routes.py index 9bf22487..4f3ac8db 100644 --- a/backend/src/api/routes/dashboards/_listing_routes.py +++ b/backend/src/api/routes/dashboards/_listing_routes.py @@ -1,12 +1,10 @@ -# [DEF:DashboardListingRoutes:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: api, dashboards, listing, routes -# @PURPOSE: Dashboard listing route handler for Dashboard Hub. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DashboardRouter] -# @RELATION: DEPENDS_ON -> [DashboardSchemas] -# @RELATION: DEPENDS_ON -> [DashboardHelpers] -# @RELATION: DEPENDS_ON -> [DashboardProjection] +# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS api, dashboards, listing, routes] +# @BRIEF Dashboard listing route handler for Dashboard Hub. +# @LAYER API +# @RELATION DEPENDS_ON -> [DashboardRouter] +# @RELATION DEPENDS_ON -> [DashboardSchemas] +# @RELATION DEPENDS_ON -> [DashboardHelpers] +# @RELATION DEPENDS_ON -> [DashboardProjection] # [SECTION: IMPORTS] import os @@ -34,15 +32,14 @@ from ._router import router # [/SECTION] -# [DEF:get_dashboards:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status -# @PRE: env_id must be a valid environment ID -# @PRE: page must be >= 1 if provided -# @PRE: page_size must be between 1 and 100 if provided -# @POST: Returns a list of dashboards with enhanced metadata and pagination info -# @POST: Response includes pagination metadata (page, page_size, total, total_pages) -# @POST: Response includes effective profile filter metadata for main dashboards page context +# #region get_dashboards [C:3] [TYPE Function] +# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status +# @PRE env_id must be a valid environment ID +# @PRE page must be >= 1 if provided +# @PRE page_size must be between 1 and 100 if provided +# @POST Returns a list of dashboards with enhanced metadata and pagination info +# @POST Response includes pagination metadata (page, page_size, total, total_pages) +# @POST Response includes effective profile filter metadata for main dashboards page context # @PARAM: env_id (str) - The environment ID to fetch dashboards from # @PARAM: search (Optional[str]) - Filter by title/slug # @PARAM: page (Optional[int]) - Page number (default: 1) @@ -390,5 +387,5 @@ async def get_dashboards( ) -# [/DEF:get_dashboards:Function] -# [/DEF:DashboardListingRoutes:Module] +# #endregion get_dashboards +# #endregion DashboardListingRoutes diff --git a/backend/src/api/routes/dashboards/_projection.py b/backend/src/api/routes/dashboards/_projection.py index 6892d8c7..a8e06d2e 100644 --- a/backend/src/api/routes/dashboards/_projection.py +++ b/backend/src/api/routes/dashboards/_projection.py @@ -1,10 +1,8 @@ -# [DEF:DashboardProjection:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: api, dashboards, projection, profile, filtering -# @PURPOSE: Dashboard response projection and profile-filter helpers for Dashboard Hub routes. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @RELATION: DEPENDS_ON -> [ProfileService] +# #region DashboardProjection [C:2] [TYPE Module] [SEMANTICS api, dashboards, projection, profile, filtering] +# @BRIEF Dashboard response projection and profile-filter helpers for Dashboard Hub routes. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [ProfileService] from typing import Any, Dict, List, Optional @@ -15,9 +13,8 @@ from src.models.auth import User from src.services.profile_service import ProfileService -# [DEF:_normalize_actor_alias_token:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize actor alias token to comparable trim+lower text. +# #region _normalize_actor_alias_token [C:2] [TYPE Function] +# @BRIEF Normalize actor alias token to comparable trim+lower text. def _normalize_actor_alias_token(value: Any) -> Optional[str]: if value is None: return None @@ -25,12 +22,11 @@ def _normalize_actor_alias_token(value: Any) -> Optional[str]: return normalized if normalized else None -# [/DEF:_normalize_actor_alias_token:Function] +# #endregion _normalize_actor_alias_token -# [DEF:_normalize_owner_display_token:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Project owner payload value into stable display string for API response contracts. +# #region _normalize_owner_display_token [C:2] [TYPE Function] +# @BRIEF Project owner payload value into stable display string for API response contracts. def _normalize_owner_display_token(owner: Any) -> Optional[str]: if owner is None: return None @@ -45,12 +41,11 @@ def _normalize_owner_display_token(owner: Any) -> Optional[str]: return None -# [/DEF:_normalize_owner_display_token:Function] +# #endregion _normalize_owner_display_token -# [DEF:_normalize_dashboard_owner_values:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize dashboard owners payload to optional list of display strings. +# #region _normalize_dashboard_owner_values [C:2] [TYPE Function] +# @BRIEF Normalize dashboard owners payload to optional list of display strings. def _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]: if owners is None: return None @@ -67,12 +62,11 @@ def _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]: return normalized -# [/DEF:_normalize_dashboard_owner_values:Function] +# #endregion _normalize_dashboard_owner_values -# [DEF:_project_dashboard_response_items:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Project dashboard payloads to response-contract-safe shape. +# #region _project_dashboard_response_items [C:2] [TYPE Function] +# @BRIEF Project dashboard payloads to response-contract-safe shape. def _project_dashboard_response_items( dashboards: List[Dict[str, Any]], ) -> List[Dict[str, Any]]: @@ -86,12 +80,11 @@ def _project_dashboard_response_items( return projected -# [/DEF:_project_dashboard_response_items:Function] +# #endregion _project_dashboard_response_items -# [DEF:_get_profile_filter_binding:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard profile-filter binding through current or legacy profile service contracts. +# #region _get_profile_filter_binding [C:2] [TYPE Function] +# @BRIEF Resolve dashboard profile-filter binding through current or legacy profile service contracts. def _get_profile_filter_binding( profile_service: Any, current_user: User ) -> Dict[str, Any]: @@ -144,13 +137,12 @@ def _get_profile_filter_binding( } -# [/DEF:_get_profile_filter_binding:Function] +# #endregion _get_profile_filter_binding -# [DEF:_resolve_profile_actor_aliases:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out. -# @SIDE_EFFECT: Performs at most one Superset users-lookup request. +# #region _resolve_profile_actor_aliases [C:2] [TYPE Function] +# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out. +# @SIDE_EFFECT Performs at most one Superset users-lookup request. def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]: normalized_bound = _normalize_actor_alias_token(bound_username) if not normalized_bound: @@ -202,12 +194,11 @@ def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]: return aliases -# [/DEF:_resolve_profile_actor_aliases:Function] +# #endregion _resolve_profile_actor_aliases -# [DEF:_matches_dashboard_actor_aliases:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Apply profile actor matching against multiple aliases (username + optional display name). +# #region _matches_dashboard_actor_aliases [C:2] [TYPE Function] +# @BRIEF Apply profile actor matching against multiple aliases (username + optional display name). def _matches_dashboard_actor_aliases( profile_service: ProfileService, actor_aliases: List[str], @@ -224,12 +215,11 @@ def _matches_dashboard_actor_aliases( return False -# [/DEF:_matches_dashboard_actor_aliases:Function] +# #endregion _matches_dashboard_actor_aliases -# [DEF:_task_matches_dashboard:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Checks whether task params are tied to a specific dashboard and environment. +# #region _task_matches_dashboard [C:2] [TYPE Function] +# @BRIEF Checks whether task params are tied to a specific dashboard and environment. def _task_matches_dashboard( task: Any, dashboard_id: int, env_id: Optional[str] ) -> bool: @@ -256,5 +246,5 @@ def _task_matches_dashboard( return True -# [/DEF:_task_matches_dashboard:Function] -# [/DEF:DashboardProjection:Module] +# #endregion _task_matches_dashboard +# #endregion DashboardProjection diff --git a/backend/src/api/routes/dashboards/_router.py b/backend/src/api/routes/dashboards/_router.py index e1449166..3db1a770 100644 --- a/backend/src/api/routes/dashboards/_router.py +++ b/backend/src/api/routes/dashboards/_router.py @@ -1,8 +1,7 @@ -# [DEF:DashboardsRouter:Block] -# @COMPLEXITY: 1 -# @PURPOSE: Single APIRouter instance for all dashboard endpoints. -# @RELATION: BINDS_TO -> [DashboardsApi] +# #region DashboardsRouter [C:1] [TYPE Block] +# @BRIEF Single APIRouter instance for all dashboard endpoints. +# @RELATION BINDS_TO -> [DashboardsApi] from fastapi import APIRouter router = APIRouter(prefix="/api/dashboards", tags=["Dashboards"]) -# [/DEF:DashboardsRouter:Block] +# #endregion DashboardsRouter diff --git a/backend/src/api/routes/dashboards/_schemas.py b/backend/src/api/routes/dashboards/_schemas.py index a9298ca3..c91a8cf6 100644 --- a/backend/src/api/routes/dashboards/_schemas.py +++ b/backend/src/api/routes/dashboards/_schemas.py @@ -1,17 +1,14 @@ -# [DEF:DashboardSchemas:Module] -# @COMPLEXITY: 1 -# @SEMANTICS: api, dashboards, schemas, dto -# @PURPOSE: DTO classes for the Dashboard Hub API. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> [Pydantic] +# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS api, dashboards, schemas, dto] +# @BRIEF DTO classes for the Dashboard Hub API. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [Pydantic] from typing import List, Optional, Dict, Any, Literal from pydantic import BaseModel, Field -# [DEF:GitStatus:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for dashboard Git synchronization status. +# #region GitStatus [C:1] [TYPE DataClass] +# @BRIEF DTO for dashboard Git synchronization status. class GitStatus(BaseModel): branch: Optional[str] = None sync_status: Optional[str] = Field(None, pattern="^OK|DIFF|NO_REPO|ERROR$") @@ -19,12 +16,11 @@ class GitStatus(BaseModel): has_changes_for_commit: Optional[bool] = None -# [/DEF:GitStatus:DataClass] +# #endregion GitStatus -# [DEF:LastTask:DataClass] -# @COMPLEXITY: 2 -# @PURPOSE: DTO for the most recent background task associated with a dashboard. +# #region LastTask [C:2] [TYPE DataClass] +# @BRIEF DTO for the most recent background task associated with a dashboard. class LastTask(BaseModel): task_id: Optional[str] = None status: Optional[str] = Field( @@ -34,12 +30,11 @@ class LastTask(BaseModel): validation_status: Optional[str] = Field(None, pattern="^PASS|FAIL|WARN|UNKNOWN$") -# [/DEF:LastTask:DataClass] +# #endregion LastTask -# [DEF:DashboardItem:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO representing a single dashboard with projected metadata. +# #region DashboardItem [C:1] [TYPE DataClass] +# @BRIEF DTO representing a single dashboard with projected metadata. class DashboardItem(BaseModel): id: int title: str @@ -53,12 +48,11 @@ class DashboardItem(BaseModel): last_task: Optional[LastTask] = None -# [/DEF:DashboardItem:DataClass] +# #endregion DashboardItem -# [DEF:EffectiveProfileFilter:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Metadata about applied profile filters for UI context. +# #region EffectiveProfileFilter [C:1] [TYPE DataClass] +# @BRIEF Metadata about applied profile filters for UI context. class EffectiveProfileFilter(BaseModel): applied: bool source_page: Literal["dashboards_main", "other"] = "dashboards_main" @@ -69,12 +63,11 @@ class EffectiveProfileFilter(BaseModel): ] = None -# [/DEF:EffectiveProfileFilter:DataClass] +# #endregion EffectiveProfileFilter -# [DEF:DashboardsResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Envelope DTO for paginated dashboards list. +# #region DashboardsResponse [C:1] [TYPE DataClass] +# @BRIEF Envelope DTO for paginated dashboards list. class DashboardsResponse(BaseModel): dashboards: List[DashboardItem] total: int @@ -84,12 +77,11 @@ class DashboardsResponse(BaseModel): effective_profile_filter: Optional[EffectiveProfileFilter] = None -# [/DEF:DashboardsResponse:DataClass] +# #endregion DashboardsResponse -# [DEF:DashboardChartItem:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for a chart linked to a dashboard. +# #region DashboardChartItem [C:1] [TYPE DataClass] +# @BRIEF DTO for a chart linked to a dashboard. class DashboardChartItem(BaseModel): id: int title: str @@ -99,12 +91,11 @@ class DashboardChartItem(BaseModel): overview: Optional[str] = None -# [/DEF:DashboardChartItem:DataClass] +# #endregion DashboardChartItem -# [DEF:DashboardDatasetItem:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for a dataset associated with a dashboard. +# #region DashboardDatasetItem [C:1] [TYPE DataClass] +# @BRIEF DTO for a dataset associated with a dashboard. class DashboardDatasetItem(BaseModel): id: int table_name: str @@ -114,12 +105,11 @@ class DashboardDatasetItem(BaseModel): overview: Optional[str] = None -# [/DEF:DashboardDatasetItem:DataClass] +# #endregion DashboardDatasetItem -# [DEF:DashboardDetailResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Detailed dashboard metadata including children. +# #region DashboardDetailResponse [C:1] [TYPE DataClass] +# @BRIEF Detailed dashboard metadata including children. class DashboardDetailResponse(BaseModel): id: int title: str @@ -134,12 +124,11 @@ class DashboardDetailResponse(BaseModel): dataset_count: int -# [/DEF:DashboardDetailResponse:DataClass] +# #endregion DashboardDetailResponse -# [DEF:DashboardTaskHistoryItem:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Individual history record entry. +# #region DashboardTaskHistoryItem [C:1] [TYPE DataClass] +# @BRIEF Individual history record entry. class DashboardTaskHistoryItem(BaseModel): id: str plugin_id: str @@ -151,23 +140,21 @@ class DashboardTaskHistoryItem(BaseModel): summary: Optional[str] = None -# [/DEF:DashboardTaskHistoryItem:DataClass] +# #endregion DashboardTaskHistoryItem -# [DEF:DashboardTaskHistoryResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Collection DTO for task history. +# #region DashboardTaskHistoryResponse [C:1] [TYPE DataClass] +# @BRIEF Collection DTO for task history. class DashboardTaskHistoryResponse(BaseModel): dashboard_id: int items: List[DashboardTaskHistoryItem] -# [/DEF:DashboardTaskHistoryResponse:DataClass] +# #endregion DashboardTaskHistoryResponse -# [DEF:DatabaseMapping:DataClass] -# @COMPLEXITY: 2 -# @PURPOSE: DTO for cross-environment database ID mapping. +# #region DatabaseMapping [C:2] [TYPE DataClass] +# @BRIEF DTO for cross-environment database ID mapping. class DatabaseMapping(BaseModel): source_db: str target_db: str @@ -176,22 +163,20 @@ class DatabaseMapping(BaseModel): confidence: float -# [/DEF:DatabaseMapping:DataClass] +# #endregion DatabaseMapping -# [DEF:DatabaseMappingsResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Wrapper for database mappings. +# #region DatabaseMappingsResponse [C:1] [TYPE DataClass] +# @BRIEF Wrapper for database mappings. class DatabaseMappingsResponse(BaseModel): mappings: List[DatabaseMapping] -# [/DEF:DatabaseMappingsResponse:DataClass] +# #endregion DatabaseMappingsResponse -# [DEF:MigrateRequest:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for dashboard migration requests. +# #region MigrateRequest [C:1] [TYPE DataClass] +# @BRIEF DTO for dashboard migration requests. class MigrateRequest(BaseModel): source_env_id: str = Field(..., description="Source environment ID") target_env_id: str = Field(..., description="Target environment ID") @@ -204,22 +189,20 @@ class MigrateRequest(BaseModel): replace_db_config: bool = Field(False, description="Replace database configuration") -# [/DEF:MigrateRequest:DataClass] +# #endregion MigrateRequest -# [DEF:TaskResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for async task ID return. +# #region TaskResponse [C:1] [TYPE DataClass] +# @BRIEF DTO for async task ID return. class TaskResponse(BaseModel): task_id: str -# [/DEF:TaskResponse:DataClass] +# #endregion TaskResponse -# [DEF:BackupRequest:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for dashboard backup requests. +# #region BackupRequest [C:1] [TYPE DataClass] +# @BRIEF DTO for dashboard backup requests. class BackupRequest(BaseModel): env_id: str = Field(..., description="Environment ID") dashboard_ids: List[int] = Field(..., description="List of dashboard IDs to backup") @@ -228,5 +211,5 @@ class BackupRequest(BaseModel): ) -# [/DEF:BackupRequest:DataClass] -# [/DEF:DashboardSchemas:Module] +# #endregion BackupRequest +# #endregion DashboardSchemas diff --git a/backend/src/api/routes/dataset_review.py b/backend/src/api/routes/dataset_review.py index 20dece75..284ae7f9 100644 --- a/backend/src/api/routes/dataset_review.py +++ b/backend/src/api/routes/dataset_review.py @@ -1,10 +1,8 @@ -# [DEF:DatasetReviewApi:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: dataset_review, api, session_lifecycle, exports, rbac, feature_flags -# @PURPOSE: Thin facade re-exporting router and public symbols from decomposed dataset review API sub-modules. -# @LAYER: API -# @RATIONALE: Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers). -# @REJECTED: Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk. +# #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset_review, api, session_lifecycle, exports, rbac, feature_flags] +# @BRIEF Thin facade re-exporting router and public symbols from decomposed dataset review API sub-modules. +# @LAYER API +# @RATIONALE Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers). +# @REJECTED Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk. from src.api.routes.dataset_review_pkg._dependencies import ( # noqa: F401 StartSessionRequest, @@ -35,4 +33,4 @@ from src.api.routes.dataset_review_pkg._dependencies import ( # noqa: F401 _update_semantic_field_state, ) from src.api.routes.dataset_review_pkg._routes import router # noqa: F401 -# [/DEF:DatasetReviewApi:Module] +# #endregion DatasetReviewApi diff --git a/backend/src/api/routes/dataset_review_pkg/_dependencies.py b/backend/src/api/routes/dataset_review_pkg/_dependencies.py index 410c59e5..957f4f34 100644 --- a/backend/src/api/routes/dataset_review_pkg/_dependencies.py +++ b/backend/src/api/routes/dataset_review_pkg/_dependencies.py @@ -1,7 +1,6 @@ -# [DEF:DatasetReviewDependencies:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints. -# @LAYER: API +# #region DatasetReviewDependencies [C:2] [TYPE Module] +# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints. +# @LAYER API from __future__ import annotations @@ -73,32 +72,29 @@ from src.services.dataset_review.repositories.session_repository import ( log = MarkerLogger("DatasetReviewDeps") -# [DEF:StartSessionRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for starting one dataset review session. +# #region StartSessionRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for starting one dataset review session. class StartSessionRequest(BaseModel): source_kind: str = Field(..., pattern="^(superset_link|dataset_selection)$") source_input: str = Field(..., min_length=1) environment_id: str = Field(..., min_length=1) -# [/DEF:StartSessionRequest:Class] +# #endregion StartSessionRequest -# [DEF:UpdateSessionRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for lifecycle state updates on an existing session. +# #region UpdateSessionRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for lifecycle state updates on an existing session. class UpdateSessionRequest(BaseModel): status: SessionStatus note: Optional[str] = None -# [/DEF:UpdateSessionRequest:Class] +# #endregion UpdateSessionRequest -# [DEF:SessionCollectionResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Paginated session collection response. +# #region SessionCollectionResponse [C:1] [TYPE Class] +# @BRIEF Paginated session collection response. class SessionCollectionResponse(BaseModel): items: List[SessionSummary] total: int @@ -107,12 +103,11 @@ class SessionCollectionResponse(BaseModel): has_next: bool -# [/DEF:SessionCollectionResponse:Class] +# #endregion SessionCollectionResponse -# [DEF:ExportArtifactResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Inline export response for documentation or validation outputs. +# #region ExportArtifactResponse [C:1] [TYPE Class] +# @BRIEF Inline export response for documentation or validation outputs. class ExportArtifactResponse(BaseModel): artifact_id: str session_id: str @@ -124,12 +119,11 @@ class ExportArtifactResponse(BaseModel): content: Dict[str, Any] -# [/DEF:ExportArtifactResponse:Class] +# #endregion ExportArtifactResponse -# [DEF:FieldSemanticUpdateRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override. +# #region FieldSemanticUpdateRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override. class FieldSemanticUpdateRequest(BaseModel): candidate_id: Optional[str] = None verbose_name: Optional[str] = None @@ -139,34 +133,31 @@ class FieldSemanticUpdateRequest(BaseModel): resolution_note: Optional[str] = None -# [/DEF:FieldSemanticUpdateRequest:Class] +# #endregion FieldSemanticUpdateRequest -# [DEF:FeedbackRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for thumbs up/down feedback. +# #region FeedbackRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for thumbs up/down feedback. class FeedbackRequest(BaseModel): feedback: str = Field(..., pattern="^(up|down)$") -# [/DEF:FeedbackRequest:Class] +# #endregion FeedbackRequest -# [DEF:ClarificationAnswerRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for submitting one clarification answer. +# #region ClarificationAnswerRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for submitting one clarification answer. class ClarificationAnswerRequest(BaseModel): question_id: str = Field(..., min_length=1) answer_kind: AnswerKind answer_value: Optional[str] = None -# [/DEF:ClarificationAnswerRequest:Class] +# #endregion ClarificationAnswerRequest -# [DEF:ClarificationSessionSummaryResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Summary DTO for current clarification session state. +# #region ClarificationSessionSummaryResponse [C:1] [TYPE Class] +# @BRIEF Summary DTO for current clarification session state. class ClarificationSessionSummaryResponse(BaseModel): clarification_session_id: str session_id: str @@ -177,89 +168,81 @@ class ClarificationSessionSummaryResponse(BaseModel): summary_delta: Optional[str] = None -# [/DEF:ClarificationSessionSummaryResponse:Class] +# #endregion ClarificationSessionSummaryResponse -# [DEF:ClarificationStateResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Response DTO for current clarification state and active question payload. +# #region ClarificationStateResponse [C:1] [TYPE Class] +# @BRIEF Response DTO for current clarification state and active question payload. class ClarificationStateResponse(BaseModel): clarification_session: Optional[ClarificationSessionSummaryResponse] = None current_question: Optional[ClarificationQuestionDto] = None -# [/DEF:ClarificationStateResponse:Class] +# #endregion ClarificationStateResponse -# [DEF:ClarificationAnswerResultResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Response DTO for one clarification answer mutation result. +# #region ClarificationAnswerResultResponse [C:1] [TYPE Class] +# @BRIEF Response DTO for one clarification answer mutation result. class ClarificationAnswerResultResponse(BaseModel): clarification_state: ClarificationStateResponse session: SessionSummary changed_findings: List[ValidationFindingDto] -# [/DEF:ClarificationAnswerResultResponse:Class] +# #endregion ClarificationAnswerResultResponse -# [DEF:FeedbackResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal response DTO for persisted AI feedback actions. +# #region FeedbackResponse [C:1] [TYPE Class] +# @BRIEF Minimal response DTO for persisted AI feedback actions. class FeedbackResponse(BaseModel): target_id: str feedback: str -# [/DEF:FeedbackResponse:Class] +# #endregion FeedbackResponse -# [DEF:ApproveMappingRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Optional request DTO for explicit mapping approval audit notes. +# #region ApproveMappingRequest [C:1] [TYPE Class] +# @BRIEF Optional request DTO for explicit mapping approval audit notes. class ApproveMappingRequest(BaseModel): approval_note: Optional[str] = None -# [/DEF:ApproveMappingRequest:Class] +# #endregion ApproveMappingRequest -# [DEF:BatchApproveSemanticItemRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for one batch semantic-approval item. +# #region BatchApproveSemanticItemRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for one batch semantic-approval item. class BatchApproveSemanticItemRequest(BaseModel): field_id: str = Field(..., min_length=1) candidate_id: str = Field(..., min_length=1) lock_field: bool = False -# [/DEF:BatchApproveSemanticItemRequest:Class] +# #endregion BatchApproveSemanticItemRequest -# [DEF:BatchApproveSemanticRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for explicit batch semantic approvals. +# #region BatchApproveSemanticRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for explicit batch semantic approvals. class BatchApproveSemanticRequest(BaseModel): items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1) -# [/DEF:BatchApproveSemanticRequest:Class] +# #endregion BatchApproveSemanticRequest -# [DEF:BatchApproveMappingRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for explicit batch mapping approvals. +# #region BatchApproveMappingRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for explicit batch mapping approvals. class BatchApproveMappingRequest(BaseModel): mapping_ids: List[str] = Field(..., min_length=1) approval_note: Optional[str] = None -# [/DEF:BatchApproveMappingRequest:Class] +# #endregion BatchApproveMappingRequest -# [DEF:PreviewEnqueueResultResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Async preview trigger response exposing only enqueue state. +# #region PreviewEnqueueResultResponse [C:1] [TYPE Class] +# @BRIEF Async preview trigger response exposing only enqueue state. class PreviewEnqueueResultResponse(BaseModel): session_id: str session_version: Optional[int] = None @@ -267,47 +250,43 @@ class PreviewEnqueueResultResponse(BaseModel): task_id: Optional[str] = None -# [/DEF:PreviewEnqueueResultResponse:Class] +# #endregion PreviewEnqueueResultResponse -# [DEF:MappingCollectionResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Wrapper for execution mapping list responses. +# #region MappingCollectionResponse [C:1] [TYPE Class] +# @BRIEF Wrapper for execution mapping list responses. class MappingCollectionResponse(BaseModel): items: List[ExecutionMappingDto] -# [/DEF:MappingCollectionResponse:Class] +# #endregion MappingCollectionResponse -# [DEF:UpdateExecutionMappingRequest:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for one manual execution-mapping override update. +# #region UpdateExecutionMappingRequest [C:1] [TYPE Class] +# @BRIEF Request DTO for one manual execution-mapping override update. class UpdateExecutionMappingRequest(BaseModel): effective_value: Optional[Any] = None mapping_method: Optional[str] = Field(default=None, pattern="^(manual_override|direct_match|heuristic_match|semantic_match)$") transformation_note: Optional[str] = None -# [/DEF:UpdateExecutionMappingRequest:Class] +# #endregion UpdateExecutionMappingRequest -# [DEF:LaunchDatasetResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target. +# #region LaunchDatasetResponse [C:1] [TYPE Class] +# @BRIEF Launch result exposing audited run context and SQL Lab redirect target. class LaunchDatasetResponse(BaseModel): run_context: DatasetRunContextDto redirect_url: str -# [/DEF:LaunchDatasetResponse:Class] +# #endregion LaunchDatasetResponse # --- Dependency Injection --- -# [DEF:_require_auto_review_flag:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Guard US1 dataset review endpoints behind the configured feature flag. +# #region _require_auto_review_flag [C:2] [TYPE Function] +# @BRIEF Guard US1 dataset review endpoints behind the configured feature flag. def _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool: with belief_scope("dataset_review.require_auto_review_flag"): settings = config_manager.get_config().settings @@ -318,12 +297,11 @@ def _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> boo return True -# [/DEF:_require_auto_review_flag:Function] +# #endregion _require_auto_review_flag -# [DEF:_require_clarification_flag:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Guard clarification-specific US2 endpoints behind the configured feature flag. +# #region _require_clarification_flag [C:2] [TYPE Function] +# @BRIEF Guard clarification-specific US2 endpoints behind the configured feature flag. def _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool: with belief_scope("dataset_review.require_clarification_flag"): settings = config_manager.get_config().settings @@ -334,12 +312,11 @@ def _require_clarification_flag(config_manager=Depends(get_config_manager)) -> b return True -# [/DEF:_require_clarification_flag:Function] +# #endregion _require_clarification_flag -# [DEF:_require_execution_flag:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Guard US3 execution endpoints behind the configured feature flag. +# #region _require_execution_flag [C:2] [TYPE Function] +# @BRIEF Guard US3 execution endpoints behind the configured feature flag. def _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool: with belief_scope("dataset_review.require_execution_flag"): settings = config_manager.get_config().settings @@ -350,22 +327,20 @@ def _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool: return True -# [/DEF:_require_execution_flag:Function] +# #endregion _require_execution_flag -# [DEF:_get_repository:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build repository dependency. +# #region _get_repository [C:1] [TYPE Function] +# @BRIEF Build repository dependency. def _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository: return DatasetReviewSessionRepository(db) -# [/DEF:_get_repository:Function] +# #endregion _get_repository -# [DEF:_get_orchestrator:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build orchestrator dependency. +# #region _get_orchestrator [C:1] [TYPE Function] +# @BRIEF Build orchestrator dependency. def _get_orchestrator( repository: DatasetReviewSessionRepository = Depends(_get_repository), config_manager=Depends(get_config_manager), @@ -374,62 +349,57 @@ def _get_orchestrator( return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager) -# [/DEF:_get_orchestrator:Function] +# #endregion _get_orchestrator -# [DEF:_get_clarification_engine:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build clarification engine dependency. +# #region _get_clarification_engine [C:1] [TYPE Function] +# @BRIEF Build clarification engine dependency. def _get_clarification_engine( repository: DatasetReviewSessionRepository = Depends(_get_repository), ) -> ClarificationEngine: return ClarificationEngine(repository=repository) -# [/DEF:_get_clarification_engine:Function] +# #endregion _get_clarification_engine # --- Serialization Helpers --- -# [DEF:_serialize_session_summary:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map session aggregate into stable API summary DTO. +# #region _serialize_session_summary [C:1] [TYPE Function] +# @BRIEF Map session aggregate into stable API summary DTO. def _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary: summary = SessionSummary.model_validate(session, from_attributes=True) summary.session_version = summary.version return summary -# [/DEF:_serialize_session_summary:Function] +# #endregion _serialize_session_summary -# [DEF:_serialize_session_detail:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map session aggregate into stable API detail DTO. +# #region _serialize_session_detail [C:1] [TYPE Function] +# @BRIEF Map session aggregate into stable API detail DTO. def _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail: detail = SessionDetail.model_validate(session, from_attributes=True) detail.session_version = detail.version return detail -# [/DEF:_serialize_session_detail:Function] +# #endregion _serialize_session_detail -# [DEF:_require_session_version_header:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Read the optimistic-lock session version header. +# #region _require_session_version_header [C:1] [TYPE Function] +# @BRIEF Read the optimistic-lock session version header. def _require_session_version_header( session_version: int = Header(..., alias="X-Session-Version", ge=0), ) -> int: return session_version -# [/DEF:_require_session_version_header:Function] +# #endregion _require_session_version_header -# [DEF:_build_session_version_conflict_http_exception:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses. +# #region _build_session_version_conflict_http_exception [C:1] [TYPE Function] +# @BRIEF Normalize optimistic-lock conflict errors into HTTP 409 responses. def _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException: return HTTPException( status_code=status.HTTP_409_CONFLICT, @@ -437,12 +407,11 @@ def _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVers ) -# [/DEF:_build_session_version_conflict_http_exception:Function] +# #endregion _build_session_version_conflict_http_exception -# [DEF:_enforce_session_version:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses. +# #region _enforce_session_version [C:3] [TYPE Function] +# @BRIEF Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses. def _enforce_session_version(repository, session, expected_version): with belief_scope("_enforce_session_version"): try: @@ -453,12 +422,11 @@ def _enforce_session_version(repository, session, expected_version): return session -# [/DEF:_enforce_session_version:Function] +# #endregion _enforce_session_version -# [DEF:_get_owned_session_or_404:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Resolve one session for current user or collaborator scope, returning 404 when inaccessible. +# #region _get_owned_session_or_404 [C:3] [TYPE Function] +# @BRIEF Resolve one session for current user or collaborator scope, returning 404 when inaccessible. def _get_owned_session_or_404(repository, session_id, current_user): with belief_scope("_get_owned_session_or_404"): session = repository.load_session_detail(session_id, current_user.id) @@ -467,12 +435,11 @@ def _get_owned_session_or_404(repository, session_id, current_user): return session -# [/DEF:_get_owned_session_or_404:Function] +# #endregion _get_owned_session_or_404 -# [DEF:_require_owner_mutation_scope:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Enforce owner-only mutation scope. +# #region _require_owner_mutation_scope [C:2] [TYPE Function] +# @BRIEF Enforce owner-only mutation scope. def _require_owner_mutation_scope(session, current_user): with belief_scope("_require_owner_mutation_scope"): if session.user_id != current_user.id: @@ -480,12 +447,11 @@ def _require_owner_mutation_scope(session, current_user): return session -# [/DEF:_require_owner_mutation_scope:Function] +# #endregion _require_owner_mutation_scope -# [DEF:_prepare_owned_session_mutation:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Resolve owner-scoped mutation session and enforce optimistic-lock version. +# #region _prepare_owned_session_mutation [C:3] [TYPE Function] +# @BRIEF Resolve owner-scoped mutation session and enforce optimistic-lock version. def _prepare_owned_session_mutation(repository, session_id, current_user, expected_version): with belief_scope("_prepare_owned_session_mutation"): session = _get_owned_session_or_404(repository, session_id, current_user) @@ -493,12 +459,11 @@ def _prepare_owned_session_mutation(repository, session_id, current_user, expect return _enforce_session_version(repository, session, expected_version) -# [/DEF:_prepare_owned_session_mutation:Function] +# #endregion _prepare_owned_session_mutation -# [DEF:_commit_owned_session_mutation:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Centralize session version bumping and commit semantics. +# #region _commit_owned_session_mutation [C:3] [TYPE Function] +# @BRIEF Centralize session version bumping and commit semantics. def _commit_owned_session_mutation(repository, session, *, refresh_targets=None): with belief_scope("_commit_owned_session_mutation"): try: @@ -508,22 +473,20 @@ def _commit_owned_session_mutation(repository, session, *, refresh_targets=None) return session -# [/DEF:_commit_owned_session_mutation:Function] +# #endregion _commit_owned_session_mutation -# [DEF:_record_session_event:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint. +# #region _record_session_event [C:1] [TYPE Function] +# @BRIEF Persist one explicit audit event for an owned mutation endpoint. def _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None): repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {}) -# [/DEF:_record_session_event:Function] +# #endregion _record_session_event -# [DEF:_serialize_semantic_field:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map one semantic field into stable DTO. +# #region _serialize_semantic_field [C:1] [TYPE Function] +# @BRIEF Map one semantic field into stable DTO. def _serialize_semantic_field(field): payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True) session_ref = getattr(field, "session", None) @@ -532,12 +495,11 @@ def _serialize_semantic_field(field): return payload -# [/DEF:_serialize_semantic_field:Function] +# #endregion _serialize_semantic_field -# [DEF:_serialize_execution_mapping:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map one execution mapping into stable DTO. +# #region _serialize_execution_mapping [C:1] [TYPE Function] +# @BRIEF Map one execution mapping into stable DTO. def _serialize_execution_mapping(mapping): payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True) session_ref = getattr(mapping, "session", None) @@ -546,12 +508,11 @@ def _serialize_execution_mapping(mapping): return payload -# [/DEF:_serialize_execution_mapping:Function] +# #endregion _serialize_execution_mapping -# [DEF:_serialize_preview:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map one preview into stable DTO. +# #region _serialize_preview [C:1] [TYPE Function] +# @BRIEF Map one preview into stable DTO. def _serialize_preview(preview, *, session_version_fallback=None): payload = CompiledPreviewDto.model_validate(preview, from_attributes=True) session_ref = getattr(preview, "session", None) @@ -562,12 +523,11 @@ def _serialize_preview(preview, *, session_version_fallback=None): return payload -# [/DEF:_serialize_preview:Function] +# #endregion _serialize_preview -# [DEF:_serialize_run_context:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Map one run context into stable DTO. +# #region _serialize_run_context [C:1] [TYPE Function] +# @BRIEF Map one run context into stable DTO. def _serialize_run_context(run_context): payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True) session_ref = getattr(run_context, "session", None) @@ -576,12 +536,11 @@ def _serialize_run_context(run_context): return payload -# [/DEF:_serialize_run_context:Function] +# #endregion _serialize_run_context -# [DEF:_serialize_clarification_question_payload:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Convert clarification engine payload into API DTO. +# #region _serialize_clarification_question_payload [C:1] [TYPE Function] +# @BRIEF Convert clarification engine payload into API DTO. def _serialize_clarification_question_payload(payload): if payload is None: return None @@ -594,12 +553,11 @@ def _serialize_clarification_question_payload(payload): }) -# [/DEF:_serialize_clarification_question_payload:Function] +# #endregion _serialize_clarification_question_payload -# [DEF:_serialize_clarification_state:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Convert clarification engine state into API response. +# #region _serialize_clarification_state [C:1] [TYPE Function] +# @BRIEF Convert clarification engine state into API response. def _serialize_clarification_state(state): return ClarificationStateResponse( clarification_session=ClarificationSessionSummaryResponse( @@ -614,34 +572,31 @@ def _serialize_clarification_state(state): ) -# [/DEF:_serialize_clarification_state:Function] +# #endregion _serialize_clarification_state -# [DEF:_serialize_empty_clarification_state:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Return empty clarification payload. +# #region _serialize_empty_clarification_state [C:1] [TYPE Function] +# @BRIEF Return empty clarification payload. def _serialize_empty_clarification_state(): return ClarificationStateResponse(clarification_session=None, current_question=None) -# [/DEF:_serialize_empty_clarification_state:Function] +# #endregion _serialize_empty_clarification_state -# [DEF:_get_latest_clarification_session_or_404:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Resolve the latest clarification aggregate or raise. +# #region _get_latest_clarification_session_or_404 [C:1] [TYPE Function] +# @BRIEF Resolve the latest clarification aggregate or raise. def _get_latest_clarification_session_or_404(session): if not session.clarification_sessions: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Clarification session not found") return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0] -# [/DEF:_get_latest_clarification_session_or_404:Function] +# #endregion _get_latest_clarification_session_or_404 -# [DEF:_get_owned_mapping_or_404:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve one execution mapping inside one owned session. +# #region _get_owned_mapping_or_404 [C:2] [TYPE Function] +# @BRIEF Resolve one execution mapping inside one owned session. def _get_owned_mapping_or_404(session, mapping_id): for mapping in session.execution_mappings: if mapping.mapping_id == mapping_id: @@ -649,12 +604,11 @@ def _get_owned_mapping_or_404(session, mapping_id): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Execution mapping not found") -# [/DEF:_get_owned_mapping_or_404:Function] +# #endregion _get_owned_mapping_or_404 -# [DEF:_get_owned_field_or_404:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve a semantic field inside one owned session. +# #region _get_owned_field_or_404 [C:2] [TYPE Function] +# @BRIEF Resolve a semantic field inside one owned session. def _get_owned_field_or_404(session, field_id): for field in session.semantic_fields: if field.field_id == field_id: @@ -662,12 +616,11 @@ def _get_owned_field_or_404(session, field_id): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Semantic field not found") -# [/DEF:_get_owned_field_or_404:Function] +# #endregion _get_owned_field_or_404 -# [DEF:_map_candidate_provenance:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Translate accepted semantic candidate type into stable field provenance. +# #region _map_candidate_provenance [C:1] [TYPE Function] +# @BRIEF Translate accepted semantic candidate type into stable field provenance. def _map_candidate_provenance(candidate): if str(candidate.match_type.value) == "exact": return FieldProvenance.DICTIONARY_EXACT @@ -678,12 +631,11 @@ def _map_candidate_provenance(candidate): return FieldProvenance.FUZZY_INFERRED -# [/DEF:_map_candidate_provenance:Function] +# #endregion _map_candidate_provenance -# [DEF:_resolve_candidate_source_version:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Resolve the semantic source version for one accepted candidate. +# #region _resolve_candidate_source_version [C:1] [TYPE Function] +# @BRIEF Resolve the semantic source version for one accepted candidate. def _resolve_candidate_source_version(field, source_id): if not source_id: return None @@ -696,13 +648,12 @@ def _resolve_candidate_source_version(field, source_id): return None -# [/DEF:_resolve_candidate_source_version:Function] +# #endregion _resolve_candidate_source_version -# [DEF:_update_semantic_field_state:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Apply field-level semantic manual override or candidate acceptance. -# @POST: Manual overrides always set manual provenance plus lock. +# #region _update_semantic_field_state [C:3] [TYPE Function] +# @BRIEF Apply field-level semantic manual override or candidate acceptance. +# @POST Manual overrides always set manual provenance plus lock. def _update_semantic_field_state(field, request, changed_by): has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format]) selected_candidate = None @@ -746,12 +697,11 @@ def _update_semantic_field_state(field, request, changed_by): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Provide candidate_id or at least one manual override field") -# [/DEF:_update_semantic_field_state:Function] +# #endregion _update_semantic_field_state -# [DEF:_build_sql_lab_redirect_url:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build SQL Lab redirect URL. +# #region _build_sql_lab_redirect_url [C:1] [TYPE Function] +# @BRIEF Build SQL Lab redirect URL. def _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref): base_url = str(environment_url or "").rstrip("/") session_ref = str(sql_lab_session_ref or "").strip() @@ -762,12 +712,11 @@ def _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref): return f"{base_url}/superset/sqllab?queryId={session_ref}" -# [/DEF:_build_sql_lab_redirect_url:Function] +# #endregion _build_sql_lab_redirect_url -# [DEF:_build_documentation_export:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Produce session documentation export content. +# #region _build_documentation_export [C:2] [TYPE Function] +# @BRIEF Produce session documentation export content. def _build_documentation_export(session, export_format): profile = session.profile findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code)) @@ -783,12 +732,11 @@ def _build_documentation_export(session, export_format): return {"storage_ref": f"inline://dataset-review/{session.session_id}/documentation.json", "content": content} -# [/DEF:_build_documentation_export:Function] +# #endregion _build_documentation_export -# [DEF:_build_validation_export:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Produce validation-focused export content. +# #region _build_validation_export [C:2] [TYPE Function] +# @BRIEF Produce validation-focused export content. def _build_validation_export(session, export_format): findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code)) if export_format == ArtifactFormat.MARKDOWN: @@ -803,7 +751,7 @@ def _build_validation_export(session, export_format): return {"storage_ref": f"inline://dataset-review/{session.session_id}/validation.json", "content": content} -# [/DEF:_build_validation_export:Function] +# #endregion _build_validation_export -# [/DEF:DatasetReviewDependencies:Module] +# #endregion DatasetReviewDependencies diff --git a/backend/src/api/routes/dataset_review_pkg/_routes.py b/backend/src/api/routes/dataset_review_pkg/_routes.py index b1195d38..03cc00cd 100644 --- a/backend/src/api/routes/dataset_review_pkg/_routes.py +++ b/backend/src/api/routes/dataset_review_pkg/_routes.py @@ -1,6 +1,5 @@ -# [DEF:DatasetReviewRoutes:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content. +# #region DatasetReviewRoutes [C:3] [TYPE Module] +# @BRIEF Persist thumbs up/down feedback for clarification question/answer content. from __future__ import annotations @@ -95,9 +94,8 @@ log = MarkerLogger("DatasetReviewRoutes") router = APIRouter(prefix="/api/dataset-orchestration", tags=["Dataset Orchestration"]) -# [DEF:list_sessions:Function] -# @COMPLEXITY: 3 -# @PURPOSE: List resumable dataset review sessions for the current user. +# #region list_sessions [C:3] [TYPE Function] +# @BRIEF List resumable dataset review sessions for the current user. @router.get( "/sessions", response_model=SessionCollectionResponse, @@ -138,12 +136,11 @@ async def list_sessions( ) -# [/DEF:list_sessions:Function] +# #endregion list_sessions -# [DEF:start_session:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Start a new dataset review session from a Superset link or dataset selection. +# #region start_session [C:4] [TYPE Function] +# @BRIEF Start a new dataset review session from a Superset link or dataset selection. @router.post( "/sessions", response_model=SessionSummary, @@ -190,12 +187,11 @@ async def start_session( return _serialize_session_summary(result.session) -# [/DEF:start_session:Function] +# #endregion start_session -# [DEF:get_session_detail:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Return the full accessible dataset review session aggregate. +# #region get_session_detail [C:3] [TYPE Function] +# @BRIEF Return the full accessible dataset review session aggregate. @router.get( "/sessions/{session_id}", response_model=SessionSummary, @@ -214,12 +210,11 @@ async def get_session_detail( return _serialize_session_detail(session) -# [/DEF:get_session_detail:Function] +# #endregion get_session_detail -# [DEF:update_session:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Update resumable lifecycle status for an owned session. +# #region update_session [C:3] [TYPE Function] +# @BRIEF Update resumable lifecycle status for an owned session. @router.patch( "/sessions/{session_id}", response_model=SessionSummary, @@ -264,12 +259,11 @@ async def update_session( return _serialize_session_summary(session) -# [/DEF:update_session:Function] +# #endregion update_session -# [DEF:delete_session:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Archive or hard-delete a session owned by the current user. +# #region delete_session [C:3] [TYPE Function] +# @BRIEF Archive or hard-delete a session owned by the current user. @router.delete( "/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT, @@ -316,12 +310,11 @@ async def delete_session( return Response(status_code=status.HTTP_204_NO_CONTENT) -# [/DEF:delete_session:Function] +# #endregion delete_session -# [DEF:export_documentation:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Export documentation output for the current session. +# #region export_documentation [C:3] [TYPE Function] +# @BRIEF Export documentation output for the current session. @router.get( "/sessions/{session_id}/exports/documentation", response_model=ExportArtifactResponse, @@ -355,12 +348,11 @@ async def export_documentation( ) -# [/DEF:export_documentation:Function] +# #endregion export_documentation -# [DEF:export_validation:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Export validation findings for the current session. +# #region export_validation [C:3] [TYPE Function] +# @BRIEF Export validation findings for the current session. @router.get( "/sessions/{session_id}/exports/validation", response_model=ExportArtifactResponse, @@ -394,12 +386,11 @@ async def export_validation( ) -# [/DEF:export_validation:Function] +# #endregion export_validation -# [DEF:get_clarification_state:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Return the current clarification session summary and active question payload. +# #region get_clarification_state [C:3] [TYPE Function] +# @BRIEF Return the current clarification session summary and active question payload. @router.get( "/sessions/{session_id}/clarification", response_model=ClarificationStateResponse, @@ -431,12 +422,11 @@ async def get_clarification_state( ) -# [/DEF:get_clarification_state:Function] +# #endregion get_clarification_state -# [DEF:resume_clarification:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Resume clarification mode on the highest-priority unresolved question. +# #region resume_clarification [C:3] [TYPE Function] +# @BRIEF Resume clarification mode on the highest-priority unresolved question. @router.post( "/sessions/{session_id}/clarification/resume", response_model=ClarificationStateResponse, @@ -469,12 +459,11 @@ async def resume_clarification( ) -# [/DEF:resume_clarification:Function] +# #endregion resume_clarification -# [DEF:record_clarification_answer:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist one clarification answer before advancing the active pointer. +# #region record_clarification_answer [C:3] [TYPE Function] +# @BRIEF Persist one clarification answer before advancing the active pointer. @router.post( "/sessions/{session_id}/clarification/answers", response_model=ClarificationAnswerResultResponse, @@ -520,12 +509,11 @@ async def record_clarification_answer( ) -# [/DEF:record_clarification_answer:Function] +# #endregion record_clarification_answer -# [DEF:update_field_semantic:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Apply one field-level semantic candidate decision or manual override. +# #region update_field_semantic [C:3] [TYPE Function] +# @BRIEF Apply one field-level semantic candidate decision or manual override. @router.patch( "/sessions/{session_id}/fields/{field_id}/semantic", response_model=SemanticFieldEntryDto, @@ -561,12 +549,11 @@ async def update_field_semantic( return _serialize_semantic_field(field) -# [/DEF:update_field_semantic:Function] +# #endregion update_field_semantic -# [DEF:lock_field_semantic:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Lock one semantic field against later automatic overwrite. +# #region lock_field_semantic [C:3] [TYPE Function] +# @BRIEF Lock one semantic field against later automatic overwrite. @router.post( "/sessions/{session_id}/fields/{field_id}/lock", response_model=SemanticFieldEntryDto, @@ -602,12 +589,11 @@ async def lock_field_semantic( return _serialize_semantic_field(field) -# [/DEF:lock_field_semantic:Function] +# #endregion lock_field_semantic -# [DEF:unlock_field_semantic:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Unlock one semantic field so later automated candidate application may replace it. +# #region unlock_field_semantic [C:3] [TYPE Function] +# @BRIEF Unlock one semantic field so later automated candidate application may replace it. @router.post( "/sessions/{session_id}/fields/{field_id}/unlock", response_model=SemanticFieldEntryDto, @@ -646,12 +632,11 @@ async def unlock_field_semantic( return _serialize_semantic_field(field) -# [/DEF:unlock_field_semantic:Function] +# #endregion unlock_field_semantic -# [DEF:approve_batch_semantic_fields:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Approve multiple semantic candidate decisions in one batch. +# #region approve_batch_semantic_fields [C:3] [TYPE Function] +# @BRIEF Approve multiple semantic candidate decisions in one batch. @router.post( "/sessions/{session_id}/fields/semantic/approve-batch", response_model=List[SemanticFieldEntryDto], @@ -697,12 +682,11 @@ async def approve_batch_semantic_fields( return [_serialize_semantic_field(f) for f in updated] -# [/DEF:approve_batch_semantic_fields:Function] +# #endregion approve_batch_semantic_fields -# [DEF:list_execution_mappings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Return the current mapping-review set for one accessible session. +# #region list_execution_mappings [C:3] [TYPE Function] +# @BRIEF Return the current mapping-review set for one accessible session. @router.get( "/sessions/{session_id}/mappings", response_model=MappingCollectionResponse, @@ -724,12 +708,11 @@ async def list_execution_mappings( ) -# [/DEF:list_execution_mappings:Function] +# #endregion list_execution_mappings -# [DEF:update_execution_mapping:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist one owner-authorized execution-mapping effective value override. +# #region update_execution_mapping [C:3] [TYPE Function] +# @BRIEF Persist one owner-authorized execution-mapping effective value override. @router.patch( "/sessions/{session_id}/mappings/{mapping_id}", response_model=ExecutionMappingDto, @@ -790,12 +773,11 @@ async def update_execution_mapping( return _serialize_execution_mapping(mapping) -# [/DEF:update_execution_mapping:Function] +# #endregion update_execution_mapping -# [DEF:approve_execution_mapping:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Explicitly approve a warning-sensitive mapping transformation. +# #region approve_execution_mapping [C:3] [TYPE Function] +# @BRIEF Explicitly approve a warning-sensitive mapping transformation. @router.post( "/sessions/{session_id}/mappings/{mapping_id}/approve", response_model=ExecutionMappingDto, @@ -839,12 +821,11 @@ async def approve_execution_mapping( return _serialize_execution_mapping(mapping) -# [/DEF:approve_execution_mapping:Function] +# #endregion approve_execution_mapping -# [DEF:approve_batch_execution_mappings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Approve multiple warning-sensitive execution mappings in one batch. +# #region approve_batch_execution_mappings [C:3] [TYPE Function] +# @BRIEF Approve multiple warning-sensitive execution mappings in one batch. @router.post( "/sessions/{session_id}/mappings/approve-batch", response_model=List[ExecutionMappingDto], @@ -892,12 +873,11 @@ async def approve_batch_execution_mappings( return [_serialize_execution_mapping(m) for m in updated] -# [/DEF:approve_batch_execution_mappings:Function] +# #endregion approve_batch_execution_mappings -# [DEF:trigger_preview_generation:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Trigger Superset-side preview compilation for the current owned execution context. +# #region trigger_preview_generation [C:3] [TYPE Function] +# @BRIEF Trigger Superset-side preview compilation for the current owned execution context. @router.post( "/sessions/{session_id}/preview", response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse], @@ -954,12 +934,11 @@ async def trigger_preview_generation( ) -# [/DEF:trigger_preview_generation:Function] +# #endregion trigger_preview_generation -# [DEF:launch_dataset:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Execute the current owned session launch handoff and return audited SQL Lab run context. +# #region launch_dataset [C:3] [TYPE Function] +# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context. @router.post( "/sessions/{session_id}/launch", response_model=LaunchDatasetResponse, @@ -1013,12 +992,11 @@ async def launch_dataset( ) -# [/DEF:launch_dataset:Function] +# #endregion launch_dataset -# [DEF:record_field_feedback:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist thumbs up/down feedback for AI-assisted semantic field content. +# #region record_field_feedback [C:3] [TYPE Function] +# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content. @router.post( "/sessions/{session_id}/fields/{field_id}/feedback", response_model=FeedbackResponse, @@ -1058,12 +1036,11 @@ async def record_field_feedback( return FeedbackResponse(target_id=field.field_id, feedback=request.feedback) -# [/DEF:record_field_feedback:Function] +# #endregion record_field_feedback -# [DEF:record_clarification_feedback:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content. +# #region record_clarification_feedback [C:3] [TYPE Function] +# @BRIEF Persist thumbs up/down feedback for clarification question/answer content. @router.post( "/sessions/{session_id}/clarification/questions/{question_id}/feedback", response_model=FeedbackResponse, @@ -1117,7 +1094,7 @@ async def record_clarification_feedback( ) -# [/DEF:record_clarification_feedback:Function] +# #endregion record_clarification_feedback -# [/DEF:DatasetReviewRoutes:Module] +# #endregion DatasetReviewRoutes diff --git a/backend/src/api/routes/datasets.py b/backend/src/api/routes/datasets.py index 0bd38b21..63a5f2ed 100644 --- a/backend/src/api/routes/datasets.py +++ b/backend/src/api/routes/datasets.py @@ -1,14 +1,12 @@ -# [DEF:DatasetsApi:Module] +# #region DatasetsApi [C:3] [TYPE Module] [SEMANTICS api, datasets, resources, hub] +# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress +# @LAYER API +# @INVARIANT All dataset responses include last_task metadata +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [ResourceService] +# @RELATION DEPENDS_ON -> [SupersetClient] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, datasets, resources, hub -# @PURPOSE: API endpoints for the Dataset Hub - listing datasets with mapping progress -# @LAYER: API -# @RELATION: DEPENDS_ON ->[AppDependencies] -# @RELATION: DEPENDS_ON ->[ResourceService] -# @RELATION: DEPENDS_ON ->[SupersetClient] # -# @INVARIANT: All dataset responses include last_task metadata # [SECTION: IMPORTS] from fastapi import APIRouter, Depends, HTTPException, Query @@ -21,25 +19,22 @@ from ...core.superset_client import SupersetClient router = APIRouter(prefix="/api/datasets", tags=["Datasets"]) -# [DEF:MappedFields:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for dataset mapping progress statistics +# #region MappedFields [C:1] [TYPE DataClass] +# @BRIEF DTO for dataset mapping progress statistics class MappedFields(BaseModel): total: int mapped: int -# [/DEF:MappedFields:DataClass] +# #endregion MappedFields -# [DEF:LastTask:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for the most recent task associated with a dataset +# #region LastTask [C:1] [TYPE DataClass] +# @BRIEF DTO for the most recent task associated with a dataset class LastTask(BaseModel): task_id: Optional[str] = None status: Optional[str] = Field(None, pattern="^RUNNING|SUCCESS|ERROR|WAITING_INPUT$") -# [/DEF:LastTask:DataClass] +# #endregion LastTask -# [DEF:DatasetItem:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Summary DTO for a dataset in the hub listing +# #region DatasetItem [C:1] [TYPE DataClass] +# @BRIEF Summary DTO for a dataset in the hub listing class DatasetItem(BaseModel): id: int table_name: str @@ -50,20 +45,18 @@ class DatasetItem(BaseModel): class Config: allow_population_by_field_name = True -# [/DEF:DatasetItem:DataClass] +# #endregion DatasetItem -# [DEF:LinkedDashboard:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for a dashboard linked to a dataset +# #region LinkedDashboard [C:1] [TYPE DataClass] +# @BRIEF DTO for a dashboard linked to a dataset class LinkedDashboard(BaseModel): id: int title: str slug: Optional[str] = None -# [/DEF:LinkedDashboard:DataClass] +# #endregion LinkedDashboard -# [DEF:DatasetColumn:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: DTO for a single dataset column's metadata +# #region DatasetColumn [C:1] [TYPE DataClass] +# @BRIEF DTO for a single dataset column's metadata class DatasetColumn(BaseModel): id: int name: str @@ -71,11 +64,10 @@ class DatasetColumn(BaseModel): is_dttm: bool = False is_active: bool = True description: Optional[str] = None -# [/DEF:DatasetColumn:DataClass] +# #endregion DatasetColumn -# [DEF:DatasetDetailResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Detailed DTO for a dataset including columns and links +# #region DatasetDetailResponse [C:1] [TYPE DataClass] +# @BRIEF Detailed DTO for a dataset including columns and links class DatasetDetailResponse(BaseModel): id: int table_name: Optional[str] = None @@ -93,31 +85,28 @@ class DatasetDetailResponse(BaseModel): class Config: allow_population_by_field_name = True -# [/DEF:DatasetDetailResponse:DataClass] +# #endregion DatasetDetailResponse -# [DEF:DatasetsResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Paginated response DTO for dataset listings +# #region DatasetsResponse [C:1] [TYPE DataClass] +# @BRIEF Paginated response DTO for dataset listings class DatasetsResponse(BaseModel): datasets: List[DatasetItem] total: int page: int page_size: int total_pages: int -# [/DEF:DatasetsResponse:DataClass] +# #endregion DatasetsResponse -# [DEF:TaskResponse:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Response DTO containing a task ID for tracking +# #region TaskResponse [C:1] [TYPE DataClass] +# @BRIEF Response DTO containing a task ID for tracking class TaskResponse(BaseModel): task_id: str -# [/DEF:TaskResponse:DataClass] +# #endregion TaskResponse -# [DEF:get_dataset_ids:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Fetch list of all dataset IDs from a specific environment (without pagination) -# @PRE: env_id must be a valid environment ID -# @POST: Returns a list of all dataset IDs +# #region get_dataset_ids [C:3] [TYPE Function] +# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination) +# @PRE env_id must be a valid environment ID +# @POST Returns a list of all dataset IDs # @PARAM: env_id (str) - The environment ID to fetch datasets from # @PARAM: search (Optional[str]) - Filter by table name # @RETURN: List[int] - List of dataset IDs @@ -163,16 +152,15 @@ async def get_dataset_ids( except Exception as e: logger.error(f"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}") raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {str(e)}") -# [/DEF:get_dataset_ids:Function] +# #endregion get_dataset_ids -# [DEF:get_datasets:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Fetch list of datasets from a specific environment with mapping progress -# @PRE: env_id must be a valid environment ID -# @PRE: page must be >= 1 if provided -# @PRE: page_size must be between 1 and 100 if provided -# @POST: Returns a list of datasets with enhanced metadata and pagination info -# @POST: Response includes pagination metadata (page, page_size, total, total_pages) +# #region get_datasets [C:3] [TYPE Function] +# @BRIEF Fetch list of datasets from a specific environment with mapping progress +# @PRE env_id must be a valid environment ID +# @PRE page must be >= 1 if provided +# @PRE page_size must be between 1 and 100 if provided +# @POST Returns a list of datasets with enhanced metadata and pagination info +# @POST Response includes pagination metadata (page, page_size, total, total_pages) # @PARAM: env_id (str) - The environment ID to fetch datasets from # @PARAM: search (Optional[str]) - Filter by table name # @PARAM: page (Optional[int]) - Page number (default: 1) @@ -243,27 +231,25 @@ async def get_datasets( except Exception as e: logger.error(f"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}") raise HTTPException(status_code=503, detail=f"Failed to fetch datasets: {str(e)}") -# [/DEF:get_datasets:Function] +# #endregion get_datasets -# [DEF:MapColumnsRequest:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for initiating column mapping +# #region MapColumnsRequest [C:1] [TYPE DataClass] +# @BRIEF Request DTO for initiating column mapping class MapColumnsRequest(BaseModel): env_id: str = Field(..., description="Environment ID") dataset_ids: List[int] = Field(..., description="List of dataset IDs to map") source_type: str = Field(..., description="Source type: 'postgresql' or 'xlsx'") connection_id: Optional[str] = Field(None, description="Connection ID for PostgreSQL source") file_data: Optional[str] = Field(None, description="File path or data for XLSX source") -# [/DEF:MapColumnsRequest:DataClass] +# #endregion MapColumnsRequest -# [DEF:map_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Trigger bulk column mapping for datasets -# @PRE: User has permission plugin:mapper:execute -# @PRE: env_id is a valid environment ID -# @PRE: dataset_ids is a non-empty list -# @POST: Returns task_id for tracking mapping progress -# @POST: Task is created and queued for execution +# #region map_columns [C:3] [TYPE Function] +# @BRIEF Trigger bulk column mapping for datasets +# @PRE User has permission plugin:mapper:execute +# @PRE env_id is a valid environment ID +# @PRE dataset_ids is a non-empty list +# @POST Returns task_id for tracking mapping progress +# @POST Task is created and queued for execution # @PARAM: request (MapColumnsRequest) - Mapping request with environment and dataset IDs # @RETURN: TaskResponse - Task ID for tracking # @RELATION: DISPATCHES ->[MapperPlugin] @@ -316,26 +302,24 @@ async def map_columns( except Exception as e: logger.error(f"[map_columns][Coherence:Failed] Failed to create mapping task: {e}") raise HTTPException(status_code=503, detail=f"Failed to create mapping task: {str(e)}") -# [/DEF:map_columns:Function] +# #endregion map_columns -# [DEF:GenerateDocsRequest:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Request DTO for initiating documentation generation +# #region GenerateDocsRequest [C:1] [TYPE DataClass] +# @BRIEF Request DTO for initiating documentation generation class GenerateDocsRequest(BaseModel): env_id: str = Field(..., description="Environment ID") dataset_ids: List[int] = Field(..., description="List of dataset IDs to generate docs for") llm_provider: str = Field(..., description="LLM provider to use") options: Optional[dict] = Field(None, description="Additional options for documentation generation") -# [/DEF:GenerateDocsRequest:DataClass] +# #endregion GenerateDocsRequest -# [DEF:generate_docs:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Trigger bulk documentation generation for datasets -# @PRE: User has permission plugin:llm_analysis:execute -# @PRE: env_id is a valid environment ID -# @PRE: dataset_ids is a non-empty list -# @POST: Returns task_id for tracking documentation generation progress -# @POST: Task is created and queued for execution +# #region generate_docs [C:3] [TYPE Function] +# @BRIEF Trigger bulk documentation generation for datasets +# @PRE User has permission plugin:llm_analysis:execute +# @PRE env_id is a valid environment ID +# @PRE dataset_ids is a non-empty list +# @POST Returns task_id for tracking documentation generation progress +# @POST Task is created and queued for execution # @PARAM: request (GenerateDocsRequest) - Documentation generation request # @RETURN: TaskResponse - Task ID for tracking # @RELATION: DISPATCHES ->[DocumentationPlugin] @@ -382,14 +366,13 @@ async def generate_docs( except Exception as e: logger.error(f"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}") raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {str(e)}") -# [/DEF:generate_docs:Function] +# #endregion generate_docs -# [DEF:get_dataset_detail:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Get detailed dataset information including columns and linked dashboards -# @PRE: env_id is a valid environment ID -# @PRE: dataset_id is a valid dataset ID -# @POST: Returns detailed dataset info with columns and linked dashboards +# #region get_dataset_detail [C:3] [TYPE Function] +# @BRIEF Get detailed dataset information including columns and linked dashboards +# @PRE env_id is a valid environment ID +# @PRE dataset_id is a valid dataset ID +# @POST Returns detailed dataset info with columns and linked dashboards # @PARAM: env_id (str) - The environment ID # @PARAM: dataset_id (int) - The dataset ID # @RETURN: DatasetDetailResponse - Detailed dataset information @@ -421,6 +404,6 @@ async def get_dataset_detail( except Exception as e: logger.error(f"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}") raise HTTPException(status_code=503, detail=f"Failed to fetch dataset detail: {str(e)}") -# [/DEF:get_dataset_detail:Function] +# #endregion get_dataset_detail -# [/DEF:DatasetsApi:Module] +# #endregion DatasetsApi diff --git a/backend/src/api/routes/environments.py b/backend/src/api/routes/environments.py index 11eaa26c..129233d6 100644 --- a/backend/src/api/routes/environments.py +++ b/backend/src/api/routes/environments.py @@ -1,13 +1,11 @@ -# [DEF:EnvironmentsApi:Module] +# #region EnvironmentsApi [C:3] [TYPE Module] [SEMANTICS api, environments, superset, databases] +# @BRIEF API endpoints for listing environments and their databases. +# @LAYER API +# @INVARIANT Environment IDs must exist in the configuration. +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [SupersetClient] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, environments, superset, databases -# @PURPOSE: API endpoints for listing environments and their databases. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AppDependencies] -# @RELATION: DEPENDS_ON -> [SupersetClient] # -# @INVARIANT: Environment IDs must exist in the configuration. # [SECTION: IMPORTS] from fastapi import APIRouter, Depends, HTTPException @@ -21,24 +19,24 @@ from ...core.logger import belief_scope router = APIRouter(prefix="/api/environments", tags=["Environments"]) -# [DEF:_normalize_superset_env_url:Function] -# @PURPOSE: Canonicalize Superset environment URL to base host/path without trailing /api/v1. -# @PRE: raw_url can be empty. -# @POST: Returns normalized base URL. +# #region _normalize_superset_env_url [TYPE Function] +# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1. +# @PRE raw_url can be empty. +# @POST Returns normalized base URL. def _normalize_superset_env_url(raw_url: str) -> str: normalized = str(raw_url or "").strip().rstrip("/") if normalized.lower().endswith("/api/v1"): normalized = normalized[:-len("/api/v1")] return normalized.rstrip("/") -# [/DEF:_normalize_superset_env_url:Function] +# #endregion _normalize_superset_env_url -# [DEF:ScheduleSchema:DataClass] +# #region ScheduleSchema [TYPE DataClass] class ScheduleSchema(BaseModel): enabled: bool = False cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\d+,)*\d+|(\d+(\/|-)\d+)|\d+|\*) ?){4,6})$') -# [/DEF:ScheduleSchema:DataClass] +# #endregion ScheduleSchema -# [DEF:EnvironmentResponse:DataClass] +# #region EnvironmentResponse [TYPE DataClass] class EnvironmentResponse(BaseModel): id: str name: str @@ -46,22 +44,21 @@ class EnvironmentResponse(BaseModel): stage: str = "DEV" is_production: bool = False backup_schedule: Optional[ScheduleSchema] = None -# [/DEF:EnvironmentResponse:DataClass] +# #endregion EnvironmentResponse -# [DEF:DatabaseResponse:DataClass] +# #region DatabaseResponse [TYPE DataClass] class DatabaseResponse(BaseModel): uuid: str database_name: str engine: Optional[str] -# [/DEF:DatabaseResponse:DataClass] +# #endregion DatabaseResponse -# [DEF:get_environments:Function] -# @PURPOSE: List all configured environments. -# @LAYER: API -# @SEMANTICS: list, environments, config -# @PRE: config_manager is injected via Depends. -# @POST: Returns a list of EnvironmentResponse objects. -# @RETURN: List[EnvironmentResponse] +# #region get_environments [TYPE Function] [SEMANTICS list, environments, config] +# @BRIEF List all configured environments. +# @LAYER API +# @PRE config_manager is injected via Depends. +# @POST Returns a list of EnvironmentResponse objects. +# @RETURN List[EnvironmentResponse] @router.get("", response_model=List[EnvironmentResponse]) async def get_environments( config_manager=Depends(get_config_manager), @@ -92,14 +89,13 @@ async def get_environments( ) ) return response_items -# [/DEF:get_environments:Function] +# #endregion get_environments -# [DEF:update_environment_schedule:Function] -# @PURPOSE: Update backup schedule for an environment. -# @LAYER: API -# @SEMANTICS: update, schedule, backup, environment -# @PRE: Environment id exists, schedule is valid ScheduleSchema. -# @POST: Backup schedule updated and scheduler reloaded. +# #region update_environment_schedule [TYPE Function] [SEMANTICS update, schedule, backup, environment] +# @BRIEF Update backup schedule for an environment. +# @LAYER API +# @PRE Environment id exists, schedule is valid ScheduleSchema. +# @POST Backup schedule updated and scheduler reloaded. # @PARAM: id (str) - The environment ID. # @PARAM: schedule (ScheduleSchema) - The new schedule. @router.put("/{id}/schedule") @@ -126,14 +122,13 @@ async def update_environment_schedule( scheduler_service.load_schedules() return {"message": "Schedule updated successfully"} -# [/DEF:update_environment_schedule:Function] +# #endregion update_environment_schedule -# [DEF:get_environment_databases:Function] -# @PURPOSE: Fetch the list of databases from a specific environment. -# @LAYER: API -# @SEMANTICS: fetch, databases, superset, environment -# @PRE: Environment id exists. -# @POST: Returns a list of database summaries from the environment. +# #region get_environment_databases [TYPE Function] [SEMANTICS fetch, databases, superset, environment] +# @BRIEF Fetch the list of databases from a specific environment. +# @LAYER API +# @PRE Environment id exists. +# @POST Returns a list of database summaries from the environment. # @PARAM: id (str) - The environment ID. # @RETURN: List[Dict] - List of databases. @router.get("/{id}/databases") @@ -154,6 +149,6 @@ async def get_environment_databases( return client.get_databases_summary() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to fetch databases: {str(e)}") -# [/DEF:get_environment_databases:Function] +# #endregion get_environment_databases -# [/DEF:EnvironmentsApi:Module] +# #endregion EnvironmentsApi diff --git a/backend/src/api/routes/git/__init__.py b/backend/src/api/routes/git/__init__.py index 0c8390f0..8d38e78b 100644 --- a/backend/src/api/routes/git/__init__.py +++ b/backend/src/api/routes/git/__init__.py @@ -1,9 +1,7 @@ -# [DEF:GitPackage:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Package root for decomposed git routes. Re-exports all public symbols from submodules. -# @LAYER: API -# @SEMANTICS: git, package, re-exports -# @RELATION: USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes, +# #region GitPackage [C:3] [TYPE Module] [SEMANTICS git, package, re-exports] +# @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules. +# @LAYER API +# @RELATION USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes, # GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes, # GitMergeRoutes, GitEnvironmentRoutes] # @INVARIANT: git_service and os are module-level attributes for test monkeypatch compatibility. @@ -41,4 +39,4 @@ from ._merge_routes import get_merge_status, get_merge_conflicts, resolve_merge_ # -- Environment routes -- from ._environment_routes import get_environments # noqa: E501, F401 -# [/DEF:GitPackage:Module] +# #endregion GitPackage diff --git a/backend/src/api/routes/git/_config_routes.py b/backend/src/api/routes/git/_config_routes.py index 6b757ddd..57e81866 100644 --- a/backend/src/api/routes/git/_config_routes.py +++ b/backend/src/api/routes/git/_config_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitConfigRoutes:Module] -# @COMPLEXITY: 2 -# @PURPOSE: FastAPI endpoints for Git server configuration CRUD and connection testing. -# @LAYER: API -# @SEMANTICS: git, config, routes, crud +# #region GitConfigRoutes [C:2] [TYPE Module] [SEMANTICS git, config, routes, crud] +# @BRIEF FastAPI endpoints for Git server configuration CRUD and connection testing. +# @LAYER API from typing import List @@ -25,9 +23,8 @@ from ._helpers import _get_git_config_or_404 from ._deps import get_git_service -# [DEF:get_git_configs:Function] -# @COMPLEXITY: 2 -# @PURPOSE: List all configured Git servers. +# #region get_git_configs [C:2] [TYPE Function] +# @BRIEF List all configured Git servers. @router.get("/config", response_model=List[GitServerConfigSchema]) async def get_git_configs( db: Session = Depends(get_db), @@ -41,12 +38,11 @@ async def get_git_configs( schema.pat = "********" result.append(schema) return result -# [/DEF:get_git_configs:Function] +# #endregion get_git_configs -# [DEF:create_git_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Register a new Git server configuration. +# #region create_git_config [C:2] [TYPE Function] +# @BRIEF Register a new Git server configuration. @router.post("/config", response_model=GitServerConfigSchema) async def create_git_config( config: GitServerConfigCreate, @@ -60,12 +56,11 @@ async def create_git_config( db.commit() db.refresh(db_config) return db_config -# [/DEF:create_git_config:Function] +# #endregion create_git_config -# [DEF:update_git_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Update an existing Git server configuration. +# #region update_git_config [C:2] [TYPE Function] +# @BRIEF Update an existing Git server configuration. @router.put("/config/{config_id}", response_model=GitServerConfigSchema) async def update_git_config( config_id: str, @@ -93,12 +88,11 @@ async def update_git_config( result_schema = GitServerConfigSchema.from_orm(db_config) result_schema.pat = "********" return result_schema -# [/DEF:update_git_config:Function] +# #endregion update_git_config -# [DEF:delete_git_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Remove a Git server configuration. +# #region delete_git_config [C:2] [TYPE Function] +# @BRIEF Remove a Git server configuration. @router.delete("/config/{config_id}") async def delete_git_config( config_id: str, @@ -115,12 +109,11 @@ async def delete_git_config( db.delete(db_config) db.commit() return {"status": "success", "message": "Configuration deleted"} -# [/DEF:delete_git_config:Function] +# #endregion delete_git_config -# [DEF:test_git_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Validate connection to a Git server using provided credentials. +# #region test_git_config [C:2] [TYPE Function] +# @BRIEF Validate connection to a Git server using provided credentials. @router.post("/config/test") async def test_git_config( config: GitServerConfigCreate, @@ -155,5 +148,5 @@ async def test_git_config( return {"status": "success", "message": "Connection successful"} else: raise HTTPException(status_code=400, detail="Connection failed") -# [/DEF:test_git_config:Function] -# [/DEF:GitConfigRoutes:Module] +# #endregion test_git_config +# #endregion GitConfigRoutes diff --git a/backend/src/api/routes/git/_deps.py b/backend/src/api/routes/git/_deps.py index d8ea712c..5d3ca60d 100644 --- a/backend/src/api/routes/git/_deps.py +++ b/backend/src/api/routes/git/_deps.py @@ -1,9 +1,7 @@ -# [DEF:GitDeps:Module] -# @COMPLEXITY: 1 -# @PURPOSE: Shared dependency wiring for monkeypatch-safe git_service access and constants. -# @LAYER: API -# @SEMANTICS: git, deps, service-locator -# @INVARIANT: get_git_service() resolves from sys.modules at call time so test monkeypatching +# #region GitDeps [C:1] [TYPE Module] [SEMANTICS git, deps, service-locator] +# @BRIEF Shared dependency wiring for monkeypatch-safe git_service access and constants. +# @LAYER API +# @INVARIANT get_git_service() resolves from sys.modules at call time so test monkeypatching # of git_routes.git_service (the __init__ attribute) takes effect across all submodules. import sys @@ -21,4 +19,4 @@ def get_git_service(): re-resolves ``sys.modules['src.api.routes.git'].git_service``. """ return sys.modules["src.api.routes.git"].git_service -# [/DEF:GitDeps:Module] +# #endregion GitDeps diff --git a/backend/src/api/routes/git/_environment_routes.py b/backend/src/api/routes/git/_environment_routes.py index 385693e7..5e639215 100644 --- a/backend/src/api/routes/git/_environment_routes.py +++ b/backend/src/api/routes/git/_environment_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitEnvironmentRoutes:Module] -# @COMPLEXITY: 2 -# @PURPOSE: FastAPI endpoint for listing deployment environments. -# @LAYER: API -# @SEMANTICS: git, environments, routes +# #region GitEnvironmentRoutes [C:2] [TYPE Module] [SEMANTICS git, environments, routes] +# @BRIEF FastAPI endpoint for listing deployment environments. +# @LAYER API from typing import List @@ -16,9 +14,8 @@ from src.api.routes.git_schemas import DeploymentEnvironmentSchema from ._router import router -# [DEF:get_environments:Function] -# @COMPLEXITY: 2 -# @PURPOSE: List all deployment environments. +# #region get_environments [C:2] [TYPE Function] +# @BRIEF List all deployment environments. @router.get("/environments", response_model=List[DeploymentEnvironmentSchema]) async def get_environments( config_manager=Depends(get_config_manager), @@ -32,5 +29,5 @@ async def get_environments( ) for e in envs ] -# [/DEF:get_environments:Function] -# [/DEF:GitEnvironmentRoutes:Module] +# #endregion get_environments +# #endregion GitEnvironmentRoutes diff --git a/backend/src/api/routes/git/_gitea_routes.py b/backend/src/api/routes/git/_gitea_routes.py index ceac24aa..90b939db 100644 --- a/backend/src/api/routes/git/_gitea_routes.py +++ b/backend/src/api/routes/git/_gitea_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitGiteaRoutes:Module] -# @COMPLEXITY: 2 -# @PURPOSE: FastAPI endpoints for Gitea-specific repository operations. -# @LAYER: API -# @SEMANTICS: git, gitea, routes, repositories +# #region GitGiteaRoutes [C:2] [TYPE Module] [SEMANTICS git, gitea, routes, repositories] +# @BRIEF FastAPI endpoints for Gitea-specific repository operations. +# @LAYER API from typing import List @@ -26,9 +24,8 @@ from ._helpers import _get_git_config_or_404 from ._deps import get_git_service -# [DEF:list_gitea_repositories:Function] -# @COMPLEXITY: 2 -# @PURPOSE: List repositories in Gitea for a saved Gitea config. +# #region list_gitea_repositories [C:2] [TYPE Function] +# @BRIEF List repositories in Gitea for a saved Gitea config. @router.get("/config/{config_id}/gitea/repos", response_model=List[GiteaRepoSchema]) async def list_gitea_repositories( config_id: str, @@ -55,12 +52,11 @@ async def list_gitea_repositories( ) for repo in repos ] -# [/DEF:list_gitea_repositories:Function] +# #endregion list_gitea_repositories -# [DEF:create_gitea_repository:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Create a repository in Gitea for a saved Gitea config. +# #region create_gitea_repository [C:2] [TYPE Function] +# @BRIEF Create a repository in Gitea for a saved Gitea config. @router.post("/config/{config_id}/gitea/repos", response_model=GiteaRepoSchema) async def create_gitea_repository( config_id: str, @@ -93,12 +89,11 @@ async def create_gitea_repository( ssh_url=repo.get("ssh_url"), default_branch=repo.get("default_branch"), ) -# [/DEF:create_gitea_repository:Function] +# #endregion create_gitea_repository -# [DEF:delete_gitea_repository:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Delete repository in Gitea for a saved Gitea config. +# #region delete_gitea_repository [C:2] [TYPE Function] +# @BRIEF Delete repository in Gitea for a saved Gitea config. @router.delete("/config/{config_id}/gitea/repos/{owner}/{repo_name}") async def delete_gitea_repository( config_id: str, @@ -121,12 +116,11 @@ async def delete_gitea_repository( repo_name=repo_name, ) return {"status": "success", "message": "Repository deleted"} -# [/DEF:delete_gitea_repository:Function] +# #endregion delete_gitea_repository -# [DEF:create_remote_repository:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Create repository on remote Git server using selected provider config. +# #region create_remote_repository [C:2] [TYPE Function] +# @BRIEF Create repository on remote Git server using selected provider config. @router.post("/config/{config_id}/repositories", response_model=RemoteRepoSchema) async def create_remote_repository( config_id: str, @@ -184,5 +178,5 @@ async def create_remote_repository( ssh_url=repo.get("ssh_url"), default_branch=repo.get("default_branch"), ) -# [/DEF:create_remote_repository:Function] -# [/DEF:GitGiteaRoutes:Module] +# #endregion create_remote_repository +# #endregion GitGiteaRoutes diff --git a/backend/src/api/routes/git/_helpers.py b/backend/src/api/routes/git/_helpers.py index ab8728d1..639226a7 100644 --- a/backend/src/api/routes/git/_helpers.py +++ b/backend/src/api/routes/git/_helpers.py @@ -1,8 +1,6 @@ -# [DEF:GitHelpers:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Shared helper functions for Git route modules. -# @LAYER: API -# @SEMANTICS: git, helpers, resolution, identity +# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS git, helpers, resolution, identity] +# @BRIEF Shared helper functions for Git route modules. +# @LAYER API # @RELATION USES -> [GitDeps] # @RELATION USES -> [SupersetClient] # @RELATION USES -> [UserDashboardPreference] @@ -25,10 +23,9 @@ from src.models.profile import UserDashboardPreference from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH -# [DEF:_build_no_repo_status_payload:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build a consistent status payload for dashboards without initialized repositories. -# @POST: Returns a stable payload compatible with frontend repository status parsing. +# #region _build_no_repo_status_payload [C:1] [TYPE Function] +# @BRIEF Build a consistent status payload for dashboards without initialized repositories. +# @POST Returns a stable payload compatible with frontend repository status parsing. def _build_no_repo_status_payload() -> dict: return { "is_dirty": False, @@ -45,25 +42,23 @@ def _build_no_repo_status_payload() -> dict: "sync_status": "NO_REPO", "has_repo": False, } -# [/DEF:_build_no_repo_status_payload:Function] +# #endregion _build_no_repo_status_payload -# [DEF:_handle_unexpected_git_route_error:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Convert unexpected route-level exceptions to stable 500 API responses. -# @PRE: `error` is a non-HTTPException instance. -# @POST: Raises HTTPException(500) with route-specific context. +# #region _handle_unexpected_git_route_error [C:1] [TYPE Function] +# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses. +# @PRE `error` is a non-HTTPException instance. +# @POST Raises HTTPException(500) with route-specific context. def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None: log.explore(f"{route_name} failed: {error}", error=str(error)) raise HTTPException(status_code=500, detail=f"{route_name} failed: {str(error)}") -# [/DEF:_handle_unexpected_git_route_error:Function] +# #endregion _handle_unexpected_git_route_error -# [DEF:_resolve_repository_status:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve repository status for one dashboard with graceful NO_REPO semantics. -# @PRE: `dashboard_id` is a valid integer. -# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent. +# #region _resolve_repository_status [C:2] [TYPE Function] +# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics. +# @PRE `dashboard_id` is a valid integer. +# @POST Returns standard status payload or `NO_REPO` payload when repository path is absent. def _resolve_repository_status(dashboard_id: int) -> dict: git_service = get_git_service() repo_path = git_service._get_repo_path(dashboard_id) @@ -82,23 +77,21 @@ def _resolve_repository_status(dashboard_id: int) -> dict: ) return _build_no_repo_status_payload() raise -# [/DEF:_resolve_repository_status:Function] +# #endregion _resolve_repository_status -# [DEF:_get_git_config_or_404:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve GitServerConfig by id or raise 404. +# #region _get_git_config_or_404 [C:2] [TYPE Function] +# @BRIEF Resolve GitServerConfig by id or raise 404. def _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig: config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first() if not config: raise HTTPException(status_code=404, detail="Git configuration not found") return config -# [/DEF:_get_git_config_or_404:Function] +# #endregion _get_git_config_or_404 -# [DEF:_find_dashboard_id_by_slug:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard numeric ID by slug in a specific environment. +# #region _find_dashboard_id_by_slug [C:2] [TYPE Function] +# @BRIEF Resolve dashboard numeric ID by slug in a specific environment. def _find_dashboard_id_by_slug( client: SupersetClient, dashboard_slug: str, @@ -126,12 +119,11 @@ def _find_dashboard_id_by_slug( except Exception: continue return None -# [/DEF:_find_dashboard_id_by_slug:Function] +# #endregion _find_dashboard_id_by_slug -# [DEF:_resolve_dashboard_id_from_ref:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard ID from slug-or-id reference for Git routes. +# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function] +# @BRIEF Resolve dashboard ID from slug-or-id reference for Git routes. def _resolve_dashboard_id_from_ref( dashboard_ref: str, config_manager, @@ -161,12 +153,11 @@ def _resolve_dashboard_id_from_ref( status_code=404, detail=f"Dashboard slug '{normalized_ref}' not found" ) return dashboard_id -# [/DEF:_resolve_dashboard_id_from_ref:Function] +# #endregion _resolve_dashboard_id_from_ref -# [DEF:_find_dashboard_id_by_slug_async:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes. +# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function] +# @BRIEF Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes. async def _find_dashboard_id_by_slug_async( client: "AsyncSupersetClient", dashboard_slug: str, @@ -194,12 +185,11 @@ async def _find_dashboard_id_by_slug_async( except Exception: continue return None -# [/DEF:_find_dashboard_id_by_slug_async:Function] +# #endregion _find_dashboard_id_by_slug_async -# [DEF:_resolve_dashboard_id_from_ref_async:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes. +# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function] +# @BRIEF Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes. async def _resolve_dashboard_id_from_ref_async( dashboard_ref: str, config_manager, @@ -235,12 +225,11 @@ async def _resolve_dashboard_id_from_ref_async( return dashboard_id finally: await client.aclose() -# [/DEF:_resolve_dashboard_id_from_ref_async:Function] +# #endregion _resolve_dashboard_id_from_ref_async -# [DEF:_resolve_repo_key_from_ref:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve repository folder key with slug-first strategy and deterministic fallback. +# #region _resolve_repo_key_from_ref [C:2] [TYPE Function] +# @BRIEF Resolve repository folder key with slug-first strategy and deterministic fallback. def _resolve_repo_key_from_ref( dashboard_ref: str, dashboard_id: int, @@ -267,23 +256,21 @@ def _resolve_repo_key_from_ref( pass return f"dashboard-{dashboard_id}" -# [/DEF:_resolve_repo_key_from_ref:Function] +# #endregion _resolve_repo_key_from_ref -# [DEF:_sanitize_optional_identity_value:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Normalize optional identity value into trimmed string or None. +# #region _sanitize_optional_identity_value [C:1] [TYPE Function] +# @BRIEF Normalize optional identity value into trimmed string or None. def _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]: normalized = str(value or "").strip() if not normalized: return None return normalized -# [/DEF:_sanitize_optional_identity_value:Function] +# #endregion _sanitize_optional_identity_value -# [DEF:_resolve_current_user_git_identity:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve configured Git username/email from current user's profile preferences. +# #region _resolve_current_user_git_identity [C:2] [TYPE Function] +# @BRIEF Resolve configured Git username/email from current user's profile preferences. def _resolve_current_user_git_identity( db: Session, current_user: Optional[User], @@ -317,12 +304,11 @@ def _resolve_current_user_git_identity( if not git_username or not git_email: return None return git_username, git_email -# [/DEF:_resolve_current_user_git_identity:Function] +# #endregion _resolve_current_user_git_identity -# [DEF:_apply_git_identity_from_profile:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Apply user-scoped Git identity to repository-local config before write/pull operations. +# #region _apply_git_identity_from_profile [C:2] [TYPE Function] +# @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations. def _apply_git_identity_from_profile( dashboard_id: int, db: Session, @@ -339,5 +325,5 @@ def _apply_git_identity_from_profile( git_username, git_email = identity configure_identity(dashboard_id, git_username, git_email) -# [/DEF:_apply_git_identity_from_profile:Function] -# [/DEF:GitHelpers:Module] +# #endregion _apply_git_identity_from_profile +# #endregion GitHelpers diff --git a/backend/src/api/routes/git/_merge_routes.py b/backend/src/api/routes/git/_merge_routes.py index 92dd415a..ca73e1bc 100644 --- a/backend/src/api/routes/git/_merge_routes.py +++ b/backend/src/api/routes/git/_merge_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitMergeRoutes:Module] -# @COMPLEXITY: 3 -# @PURPOSE: FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue). -# @LAYER: API -# @SEMANTICS: git, merge, routes, conflicts +# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS git, merge, routes, conflicts] +# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue). +# @LAYER API from typing import List, Optional @@ -25,9 +23,8 @@ from ._helpers import ( from ._deps import get_git_service -# [DEF:get_merge_status:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return unfinished-merge status for repository (web-only recovery support). +# #region get_merge_status [C:2] [TYPE Function] +# @BRIEF Return unfinished-merge status for repository (web-only recovery support). @router.get( "/repositories/{dashboard_ref}/merge/status", response_model=MergeStatusSchema ) @@ -50,12 +47,11 @@ async def get_merge_status( raise except Exception as e: _handle_unexpected_git_route_error("get_merge_status", e) -# [/DEF:get_merge_status:Function] +# #endregion get_merge_status -# [DEF:get_merge_conflicts:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return conflicted files with mine/theirs previews for web conflict resolver. +# #region get_merge_conflicts [C:2] [TYPE Function] +# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver. @router.get( "/repositories/{dashboard_ref}/merge/conflicts", response_model=List[MergeConflictFileSchema], @@ -79,13 +75,12 @@ async def get_merge_conflicts( raise except Exception as e: _handle_unexpected_git_route_error("get_merge_conflicts", e) -# [/DEF:get_merge_conflicts:Function] +# #endregion get_merge_conflicts -# [DEF:resolve_merge_conflicts:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Apply mine/theirs/manual conflict resolutions from WebUI and stage files. -# @RELATION: CALLS -> [GitService] +# #region resolve_merge_conflicts [C:3] [TYPE Function] +# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files. +# @RELATION CALLS -> [GitService] @router.post("/repositories/{dashboard_ref}/merge/resolve") async def resolve_merge_conflicts( dashboard_ref: str, @@ -111,12 +106,11 @@ async def resolve_merge_conflicts( raise except Exception as e: _handle_unexpected_git_route_error("resolve_merge_conflicts", e) -# [/DEF:resolve_merge_conflicts:Function] +# #endregion resolve_merge_conflicts -# [DEF:abort_merge:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Abort unfinished merge from WebUI flow. +# #region abort_merge [C:2] [TYPE Function] +# @BRIEF Abort unfinished merge from WebUI flow. @router.post("/repositories/{dashboard_ref}/merge/abort") async def abort_merge( dashboard_ref: str, @@ -137,13 +131,12 @@ async def abort_merge( raise except Exception as e: _handle_unexpected_git_route_error("abort_merge", e) -# [/DEF:abort_merge:Function] +# #endregion abort_merge -# [DEF:continue_merge:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Finalize unfinished merge from WebUI flow. -# @RELATION: CALLS -> [GitService] +# #region continue_merge [C:3] [TYPE Function] +# @BRIEF Finalize unfinished merge from WebUI flow. +# @RELATION CALLS -> [GitService] @router.post("/repositories/{dashboard_ref}/merge/continue") async def continue_merge( dashboard_ref: str, @@ -165,5 +158,5 @@ async def continue_merge( raise except Exception as e: _handle_unexpected_git_route_error("continue_merge", e) -# [/DEF:continue_merge:Function] -# [/DEF:GitMergeRoutes:Module] +# #endregion continue_merge +# #endregion GitMergeRoutes diff --git a/backend/src/api/routes/git/_repo_lifecycle_routes.py b/backend/src/api/routes/git/_repo_lifecycle_routes.py index ba26d0d8..547542d5 100644 --- a/backend/src/api/routes/git/_repo_lifecycle_routes.py +++ b/backend/src/api/routes/git/_repo_lifecycle_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitRepoLifecycleRoutes:Module] -# @COMPLEXITY: 3 -# @PURPOSE: FastAPI endpoints for Git lifecycle operations (sync, promote, deploy). -# @LAYER: API -# @SEMANTICS: git, lifecycle, sync, promote, deploy +# #region GitRepoLifecycleRoutes [C:3] [TYPE Module] [SEMANTICS git, lifecycle, sync, promote, deploy] +# @BRIEF FastAPI endpoints for Git lifecycle operations (sync, promote, deploy). +# @LAYER API import typing from typing import Optional @@ -31,10 +29,9 @@ from ._helpers import ( from ._deps import get_git_service -# [DEF:sync_dashboard:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin. -# @RELATION: CALLS -> [GitPlugin] +# #region sync_dashboard [C:3] [TYPE Function] +# @BRIEF Sync dashboard state from Superset to Git using the GitPlugin. +# @RELATION CALLS -> [GitPlugin] @router.post("/repositories/{dashboard_ref}/sync") async def sync_dashboard( dashboard_ref: str, @@ -64,13 +61,12 @@ async def sync_dashboard( raise except Exception as e: _handle_unexpected_git_route_error("sync_dashboard", e) -# [/DEF:sync_dashboard:Function] +# #endregion sync_dashboard -# [DEF:promote_dashboard:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Promote changes between branches via MR or direct merge. -# @RELATION: CALLS -> [GitPlugin] +# #region promote_dashboard [C:3] [TYPE Function] +# @BRIEF Promote changes between branches via MR or direct merge. +# @RELATION CALLS -> [GitPlugin] @router.post("/repositories/{dashboard_ref}/promote", response_model=PromoteResponse) async def promote_dashboard( dashboard_ref: str, @@ -188,13 +184,12 @@ async def promote_dashboard( reference_id=str(pr.get("id")) if pr.get("id") is not None else None, policy_violation=False, ) -# [/DEF:promote_dashboard:Function] +# #endregion promote_dashboard -# [DEF:deploy_dashboard:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Deploy dashboard from Git to a target environment. -# @RELATION: CALLS -> [GitPlugin] +# #region deploy_dashboard [C:3] [TYPE Function] +# @BRIEF Deploy dashboard from Git to a target environment. +# @RELATION CALLS -> [GitPlugin] @router.post("/repositories/{dashboard_ref}/deploy") async def deploy_dashboard( dashboard_ref: str, @@ -224,5 +219,5 @@ async def deploy_dashboard( raise except Exception as e: _handle_unexpected_git_route_error("deploy_dashboard", e) -# [/DEF:deploy_dashboard:Function] -# [/DEF:GitRepoLifecycleRoutes:Module] +# #endregion deploy_dashboard +# #endregion GitRepoLifecycleRoutes diff --git a/backend/src/api/routes/git/_repo_operations_routes.py b/backend/src/api/routes/git/_repo_operations_routes.py index eae48840..173005b7 100644 --- a/backend/src/api/routes/git/_repo_operations_routes.py +++ b/backend/src/api/routes/git/_repo_operations_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitRepoOperationsRoutes:Module] -# @COMPLEXITY: 3 -# @PURPOSE: FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history). -# @LAYER: API -# @SEMANTICS: git, repository, operations, commit, push, pull, status +# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS git, repository, operations, commit, push, pull, status] +# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history). +# @LAYER API from typing import List, Optional @@ -35,9 +33,8 @@ from ._helpers import ( from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH -# [DEF:commit_changes:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Stage and commit changes in the dashboard's repository. +# #region commit_changes [C:2] [TYPE Function] +# @BRIEF Stage and commit changes in the dashboard's repository. @router.post("/repositories/{dashboard_ref}/commit") async def commit_changes( dashboard_ref: str, @@ -65,12 +62,11 @@ async def commit_changes( raise except Exception as e: _handle_unexpected_git_route_error("commit_changes", e) -# [/DEF:commit_changes:Function] +# #endregion commit_changes -# [DEF:push_changes:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Push local commits to the remote repository. +# #region push_changes [C:2] [TYPE Function] +# @BRIEF Push local commits to the remote repository. @router.post("/repositories/{dashboard_ref}/push") async def push_changes( dashboard_ref: str, @@ -92,13 +88,12 @@ async def push_changes( raise except Exception as e: _handle_unexpected_git_route_error("push_changes", e) -# [/DEF:push_changes:Function] +# #endregion push_changes -# [DEF:pull_changes:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Pull changes from the remote repository. -# @RELATION: CALLS -> [GitService] +# #region pull_changes [C:3] [TYPE Function] +# @BRIEF Pull changes from the remote repository. +# @RELATION CALLS -> [GitService] @router.post("/repositories/{dashboard_ref}/pull") async def pull_changes( dashboard_ref: str, @@ -151,12 +146,11 @@ async def pull_changes( raise except Exception as e: _handle_unexpected_git_route_error("pull_changes", e) -# [/DEF:pull_changes:Function] +# #endregion pull_changes -# [DEF:get_repository_status:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get current Git status for a dashboard repository. +# #region get_repository_status [C:2] [TYPE Function] +# @BRIEF Get current Git status for a dashboard repository. @router.get("/repositories/{dashboard_ref}/status") async def get_repository_status( dashboard_ref: str, @@ -176,12 +170,11 @@ async def get_repository_status( raise except Exception as e: _handle_unexpected_git_route_error("get_repository_status", e) -# [/DEF:get_repository_status:Function] +# #endregion get_repository_status -# [DEF:get_repository_status_batch:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get Git statuses for multiple dashboard repositories in one request. +# #region get_repository_status_batch [C:2] [TYPE Function] +# @BRIEF Get Git statuses for multiple dashboard repositories in one request. @router.post("/repositories/status/batch", response_model=RepoStatusBatchResponse) async def get_repository_status_batch( request: RepoStatusBatchRequest, @@ -211,12 +204,11 @@ async def get_repository_status_batch( "sync_status": "ERROR", } return RepoStatusBatchResponse(statuses=statuses) -# [/DEF:get_repository_status_batch:Function] +# #endregion get_repository_status_batch -# [DEF:get_repository_diff:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get Git diff for a dashboard repository. +# #region get_repository_diff [C:2] [TYPE Function] +# @BRIEF Get Git diff for a dashboard repository. @router.get("/repositories/{dashboard_ref}/diff") async def get_repository_diff( dashboard_ref: str, @@ -239,12 +231,11 @@ async def get_repository_diff( raise except Exception as e: _handle_unexpected_git_route_error("get_repository_diff", e) -# [/DEF:get_repository_diff:Function] +# #endregion get_repository_diff -# [DEF:get_history:Function] -# @COMPLEXITY: 2 -# @PURPOSE: View commit history for a dashboard's repository. +# #region get_history [C:2] [TYPE Function] +# @BRIEF View commit history for a dashboard's repository. @router.get("/repositories/{dashboard_ref}/history", response_model=List[CommitSchema]) async def get_history( dashboard_ref: str, @@ -266,13 +257,12 @@ async def get_history( raise except Exception as e: _handle_unexpected_git_route_error("get_history", e) -# [/DEF:get_history:Function] +# #endregion get_history -# [DEF:generate_commit_message:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Generate a suggested commit message using LLM. -# @RELATION: CALLS -> [GitService] +# #region generate_commit_message [C:3] [TYPE Function] +# @BRIEF Generate a suggested commit message using LLM. +# @RELATION CALLS -> [GitService] @router.post("/repositories/{dashboard_ref}/generate-message") async def generate_commit_message( dashboard_ref: str, @@ -348,5 +338,5 @@ async def generate_commit_message( raise except Exception as e: _handle_unexpected_git_route_error("generate_commit_message", e) -# [/DEF:generate_commit_message:Function] -# [/DEF:GitRepoOperationsRoutes:Module] +# #endregion generate_commit_message +# #endregion GitRepoOperationsRoutes diff --git a/backend/src/api/routes/git/_repo_routes.py b/backend/src/api/routes/git/_repo_routes.py index 4bc3cbe1..5ad37ef0 100644 --- a/backend/src/api/routes/git/_repo_routes.py +++ b/backend/src/api/routes/git/_repo_routes.py @@ -1,8 +1,6 @@ -# [DEF:GitRepoRoutes:Module] -# @COMPLEXITY: 3 -# @PURPOSE: FastAPI endpoints for core Git repository operations (init, binding, branches, checkout). -# @LAYER: API -# @SEMANTICS: git, repository, routes, init, branches +# #region GitRepoRoutes [C:3] [TYPE Module] [SEMANTICS git, repository, routes, init, branches] +# @BRIEF FastAPI endpoints for core Git repository operations (init, binding, branches, checkout). +# @LAYER API from typing import List, Optional @@ -35,10 +33,9 @@ from ._helpers import ( from ._deps import get_git_service -# [DEF:init_repository:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init. -# @RELATION: CALLS -> [GitService] +# #region init_repository [C:3] [TYPE Function] +# @BRIEF Link a dashboard to a Git repository and perform initial clone/init. +# @RELATION CALLS -> [GitService] @router.post("/repositories/{dashboard_ref}/init") async def init_repository( dashboard_ref: str, @@ -105,12 +102,11 @@ async def init_repository( if isinstance(e, HTTPException): raise _handle_unexpected_git_route_error("init_repository", e) -# [/DEF:init_repository:Function] +# #endregion init_repository -# [DEF:get_repository_binding:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return repository binding with provider metadata for selected dashboard. +# #region get_repository_binding [C:2] [TYPE Function] +# @BRIEF Return repository binding with provider metadata for selected dashboard. @router.get("/repositories/{dashboard_ref}", response_model=RepositoryBindingSchema) async def get_repository_binding( dashboard_ref: str, @@ -147,12 +143,11 @@ async def get_repository_binding( raise except Exception as e: _handle_unexpected_git_route_error("get_repository_binding", e) -# [/DEF:get_repository_binding:Function] +# #endregion get_repository_binding -# [DEF:delete_repository:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Delete local repository workspace and DB binding for selected dashboard. +# #region delete_repository [C:2] [TYPE Function] +# @BRIEF Delete local repository workspace and DB binding for selected dashboard. @router.delete("/repositories/{dashboard_ref}") async def delete_repository( dashboard_ref: str, @@ -174,12 +169,11 @@ async def delete_repository( raise except Exception as e: _handle_unexpected_git_route_error("delete_repository", e) -# [/DEF:delete_repository:Function] +# #endregion delete_repository -# [DEF:get_branches:Function] -# @COMPLEXITY: 2 -# @PURPOSE: List all branches for a dashboard's repository. +# #region get_branches [C:2] [TYPE Function] +# @BRIEF List all branches for a dashboard's repository. @router.get("/repositories/{dashboard_ref}/branches", response_model=List[BranchSchema]) async def get_branches( dashboard_ref: str, @@ -200,12 +194,11 @@ async def get_branches( raise except Exception as e: _handle_unexpected_git_route_error("get_branches", e) -# [/DEF:get_branches:Function] +# #endregion get_branches -# [DEF:create_branch:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Create a new branch in the dashboard's repository. +# #region create_branch [C:2] [TYPE Function] +# @BRIEF Create a new branch in the dashboard's repository. @router.post("/repositories/{dashboard_ref}/branches") async def create_branch( dashboard_ref: str, @@ -233,12 +226,11 @@ async def create_branch( raise except Exception as e: _handle_unexpected_git_route_error("create_branch", e) -# [/DEF:create_branch:Function] +# #endregion create_branch -# [DEF:checkout_branch:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Switch the dashboard's repository to a specific branch. +# #region checkout_branch [C:2] [TYPE Function] +# @BRIEF Switch the dashboard's repository to a specific branch. @router.post("/repositories/{dashboard_ref}/checkout") async def checkout_branch( dashboard_ref: str, @@ -261,5 +253,5 @@ async def checkout_branch( raise except Exception as e: _handle_unexpected_git_route_error("checkout_branch", e) -# [/DEF:checkout_branch:Function] -# [/DEF:GitRepoRoutes:Module] +# #endregion checkout_branch +# #endregion GitRepoRoutes diff --git a/backend/src/api/routes/git/_router.py b/backend/src/api/routes/git/_router.py index f720fda8..83e4e7e2 100644 --- a/backend/src/api/routes/git/_router.py +++ b/backend/src/api/routes/git/_router.py @@ -1,10 +1,8 @@ -# [DEF:GitRouter:Module] -# @COMPLEXITY: 1 -# @PURPOSE: Shared APIRouter for all Git route modules. -# @LAYER: API -# @SEMANTICS: git, routes, fastapi, router +# #region GitRouter [C:1] [TYPE Module] [SEMANTICS git, routes, fastapi, router] +# @BRIEF Shared APIRouter for all Git route modules. +# @LAYER API from fastapi import APIRouter router = APIRouter(tags=["git"]) -# [/DEF:GitRouter:Module] +# #endregion GitRouter diff --git a/backend/src/api/routes/git_schemas.py b/backend/src/api/routes/git_schemas.py index 473c8865..e0eb1c67 100644 --- a/backend/src/api/routes/git_schemas.py +++ b/backend/src/api/routes/git_schemas.py @@ -1,21 +1,18 @@ -# [DEF:GitSchemas:Module] +# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS git, schemas, pydantic, api, contracts] +# @BRIEF Defines Pydantic models for the Git integration API layer. +# @LAYER API +# @INVARIANT All schemas must be compatible with the FastAPI router. +# @RELATION DEPENDS_ON -> [backend.src.models.git] # -# @COMPLEXITY: 1 -# @SEMANTICS: git, schemas, pydantic, api, contracts -# @PURPOSE: Defines Pydantic models for the Git integration API layer. -# @LAYER: API -# @RELATION: DEPENDS_ON -> backend.src.models.git # -# @INVARIANT: All schemas must be compatible with the FastAPI router. from pydantic import BaseModel, Field from typing import Any, Dict, List, Optional from datetime import datetime from src.models.git import GitProvider, GitStatus, SyncStatus -# [DEF:GitServerConfigBase:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Base schema for Git server configuration attributes. +# #region GitServerConfigBase [C:1] [TYPE Class] +# @BRIEF Base schema for Git server configuration attributes. class GitServerConfigBase(BaseModel): name: str = Field(..., description="Display name for the Git server") provider: GitProvider = Field(..., description="Git provider (GITHUB, GITLAB, GITEA)") @@ -24,10 +21,10 @@ class GitServerConfigBase(BaseModel): pat: str = Field(..., description="Personal Access Token") default_repository: Optional[str] = Field(None, description="Default repository path (org/repo)") default_branch: Optional[str] = Field("main", description="Default branch logic/name") -# [/DEF:GitServerConfigBase:Class] +# #endregion GitServerConfigBase -# [DEF:GitServerConfigUpdate:Class] -# @PURPOSE: Schema for updating an existing Git server configuration. +# #region GitServerConfigUpdate [TYPE Class] +# @BRIEF Schema for updating an existing Git server configuration. class GitServerConfigUpdate(BaseModel): name: Optional[str] = Field(None, description="Display name for the Git server") provider: Optional[GitProvider] = Field(None, description="Git provider (GITHUB, GITLAB, GITEA)") @@ -35,17 +32,17 @@ class GitServerConfigUpdate(BaseModel): pat: Optional[str] = Field(None, description="Personal Access Token") default_repository: Optional[str] = Field(None, description="Default repository path (org/repo)") default_branch: Optional[str] = Field(None, description="Default branch logic/name") -# [/DEF:GitServerConfigUpdate:Class] +# #endregion GitServerConfigUpdate -# [DEF:GitServerConfigCreate:Class] -# @PURPOSE: Schema for creating a new Git server configuration. +# #region GitServerConfigCreate [TYPE Class] +# @BRIEF Schema for creating a new Git server configuration. class GitServerConfigCreate(GitServerConfigBase): """Schema for creating a new Git server configuration.""" config_id: Optional[str] = Field(None, description="Optional config ID, useful for testing an existing config without sending its full PAT") -# [/DEF:GitServerConfigCreate:Class] +# #endregion GitServerConfigCreate -# [DEF:GitServerConfigSchema:Class] -# @PURPOSE: Schema for representing a Git server configuration with metadata. +# #region GitServerConfigSchema [TYPE Class] +# @BRIEF Schema for representing a Git server configuration with metadata. class GitServerConfigSchema(GitServerConfigBase): """Schema for representing a Git server configuration with metadata.""" id: str @@ -54,10 +51,10 @@ class GitServerConfigSchema(GitServerConfigBase): class Config: from_attributes = True -# [/DEF:GitServerConfigSchema:Class] +# #endregion GitServerConfigSchema -# [DEF:GitRepositorySchema:Class] -# @PURPOSE: Schema for tracking a local Git repository linked to a dashboard. +# #region GitRepositorySchema [TYPE Class] +# @BRIEF Schema for tracking a local Git repository linked to a dashboard. class GitRepositorySchema(BaseModel): """Schema for tracking a local Git repository linked to a dashboard.""" id: str @@ -70,20 +67,20 @@ class GitRepositorySchema(BaseModel): class Config: from_attributes = True -# [/DEF:GitRepositorySchema:Class] +# #endregion GitRepositorySchema -# [DEF:BranchSchema:Class] -# @PURPOSE: Schema for representing a Git branch metadata. +# #region BranchSchema [TYPE Class] +# @BRIEF Schema for representing a Git branch metadata. class BranchSchema(BaseModel): """Schema for representing a Git branch.""" name: str commit_hash: str is_remote: bool last_updated: datetime -# [/DEF:BranchSchema:Class] +# #endregion BranchSchema -# [DEF:CommitSchema:Class] -# @PURPOSE: Schema for representing Git commit details. +# #region CommitSchema [TYPE Class] +# @BRIEF Schema for representing Git commit details. class CommitSchema(BaseModel): """Schema for representing a Git commit.""" hash: str @@ -92,43 +89,43 @@ class CommitSchema(BaseModel): timestamp: datetime message: str files_changed: List[str] -# [/DEF:CommitSchema:Class] +# #endregion CommitSchema -# [DEF:BranchCreate:Class] -# @PURPOSE: Schema for branch creation requests. +# #region BranchCreate [TYPE Class] +# @BRIEF Schema for branch creation requests. class BranchCreate(BaseModel): """Schema for branch creation requests.""" name: str from_branch: str -# [/DEF:BranchCreate:Class] +# #endregion BranchCreate -# [DEF:BranchCheckout:Class] -# @PURPOSE: Schema for branch checkout requests. +# #region BranchCheckout [TYPE Class] +# @BRIEF Schema for branch checkout requests. class BranchCheckout(BaseModel): """Schema for branch checkout requests.""" name: str -# [/DEF:BranchCheckout:Class] +# #endregion BranchCheckout -# [DEF:CommitCreate:Class] -# @PURPOSE: Schema for staging and committing changes. +# #region CommitCreate [TYPE Class] +# @BRIEF Schema for staging and committing changes. class CommitCreate(BaseModel): """Schema for staging and committing changes.""" message: str files: List[str] -# [/DEF:CommitCreate:Class] +# #endregion CommitCreate -# [DEF:ConflictResolution:Class] -# @PURPOSE: Schema for resolving merge conflicts. +# #region ConflictResolution [TYPE Class] +# @BRIEF Schema for resolving merge conflicts. class ConflictResolution(BaseModel): """Schema for resolving merge conflicts.""" file_path: str resolution: str = Field(pattern="^(mine|theirs|manual)$") content: Optional[str] = None -# [/DEF:ConflictResolution:Class] +# #endregion ConflictResolution -# [DEF:MergeStatusSchema:Class] -# @PURPOSE: Schema representing unfinished merge status for repository. +# #region MergeStatusSchema [TYPE Class] +# @BRIEF Schema representing unfinished merge status for repository. class MergeStatusSchema(BaseModel): has_unfinished_merge: bool repository_path: str @@ -137,33 +134,33 @@ class MergeStatusSchema(BaseModel): merge_head: Optional[str] = None merge_message_preview: Optional[str] = None conflicts_count: int = 0 -# [/DEF:MergeStatusSchema:Class] +# #endregion MergeStatusSchema -# [DEF:MergeConflictFileSchema:Class] -# @PURPOSE: Schema describing one conflicted file with optional side snapshots. +# #region MergeConflictFileSchema [TYPE Class] +# @BRIEF Schema describing one conflicted file with optional side snapshots. class MergeConflictFileSchema(BaseModel): file_path: str mine: Optional[str] = None theirs: Optional[str] = None -# [/DEF:MergeConflictFileSchema:Class] +# #endregion MergeConflictFileSchema -# [DEF:MergeResolveRequest:Class] -# @PURPOSE: Request schema for resolving one or multiple merge conflicts. +# #region MergeResolveRequest [TYPE Class] +# @BRIEF Request schema for resolving one or multiple merge conflicts. class MergeResolveRequest(BaseModel): resolutions: List[ConflictResolution] = Field(default_factory=list) -# [/DEF:MergeResolveRequest:Class] +# #endregion MergeResolveRequest -# [DEF:MergeContinueRequest:Class] -# @PURPOSE: Request schema for finishing merge with optional explicit commit message. +# #region MergeContinueRequest [TYPE Class] +# @BRIEF Request schema for finishing merge with optional explicit commit message. class MergeContinueRequest(BaseModel): message: Optional[str] = None -# [/DEF:MergeContinueRequest:Class] +# #endregion MergeContinueRequest -# [DEF:DeploymentEnvironmentSchema:Class] -# @PURPOSE: Schema for representing a target deployment environment. +# #region DeploymentEnvironmentSchema [TYPE Class] +# @BRIEF Schema for representing a target deployment environment. class DeploymentEnvironmentSchema(BaseModel): """Schema for representing a target deployment environment.""" id: str @@ -173,50 +170,50 @@ class DeploymentEnvironmentSchema(BaseModel): class Config: from_attributes = True -# [/DEF:DeploymentEnvironmentSchema:Class] +# #endregion DeploymentEnvironmentSchema -# [DEF:DeployRequest:Class] -# @PURPOSE: Schema for dashboard deployment requests. +# #region DeployRequest [TYPE Class] +# @BRIEF Schema for dashboard deployment requests. class DeployRequest(BaseModel): """Schema for deployment requests.""" environment_id: str -# [/DEF:DeployRequest:Class] +# #endregion DeployRequest -# [DEF:RepoInitRequest:Class] -# @PURPOSE: Schema for repository initialization requests. +# #region RepoInitRequest [TYPE Class] +# @BRIEF Schema for repository initialization requests. class RepoInitRequest(BaseModel): """Schema for repository initialization requests.""" config_id: str remote_url: str -# [/DEF:RepoInitRequest:Class] +# #endregion RepoInitRequest -# [DEF:RepositoryBindingSchema:Class] -# @PURPOSE: Schema describing repository-to-config binding and provider metadata. +# #region RepositoryBindingSchema [TYPE Class] +# @BRIEF Schema describing repository-to-config binding and provider metadata. class RepositoryBindingSchema(BaseModel): dashboard_id: int config_id: str provider: GitProvider remote_url: str local_path: str -# [/DEF:RepositoryBindingSchema:Class] +# #endregion RepositoryBindingSchema -# [DEF:RepoStatusBatchRequest:Class] -# @PURPOSE: Schema for requesting repository statuses for multiple dashboards in a single call. +# #region RepoStatusBatchRequest [TYPE Class] +# @BRIEF Schema for requesting repository statuses for multiple dashboards in a single call. class RepoStatusBatchRequest(BaseModel): dashboard_ids: List[int] = Field(default_factory=list, description="Dashboard IDs to resolve repository statuses for") -# [/DEF:RepoStatusBatchRequest:Class] +# #endregion RepoStatusBatchRequest -# [DEF:RepoStatusBatchResponse:Class] -# @PURPOSE: Schema for returning repository statuses keyed by dashboard ID. +# #region RepoStatusBatchResponse [TYPE Class] +# @BRIEF Schema for returning repository statuses keyed by dashboard ID. class RepoStatusBatchResponse(BaseModel): statuses: Dict[str, Dict[str, Any]] -# [/DEF:RepoStatusBatchResponse:Class] +# #endregion RepoStatusBatchResponse -# [DEF:GiteaRepoSchema:Class] -# @PURPOSE: Schema describing a Gitea repository. +# #region GiteaRepoSchema [TYPE Class] +# @BRIEF Schema describing a Gitea repository. class GiteaRepoSchema(BaseModel): name: str full_name: str @@ -225,22 +222,22 @@ class GiteaRepoSchema(BaseModel): html_url: Optional[str] = None ssh_url: Optional[str] = None default_branch: Optional[str] = None -# [/DEF:GiteaRepoSchema:Class] +# #endregion GiteaRepoSchema -# [DEF:GiteaRepoCreateRequest:Class] -# @PURPOSE: Request schema for creating a Gitea repository. +# #region GiteaRepoCreateRequest [TYPE Class] +# @BRIEF Request schema for creating a Gitea repository. class GiteaRepoCreateRequest(BaseModel): name: str = Field(..., min_length=1, max_length=255) private: bool = True description: Optional[str] = None auto_init: bool = True default_branch: Optional[str] = "main" -# [/DEF:GiteaRepoCreateRequest:Class] +# #endregion GiteaRepoCreateRequest -# [DEF:RemoteRepoSchema:Class] -# @PURPOSE: Provider-agnostic remote repository payload. +# #region RemoteRepoSchema [TYPE Class] +# @BRIEF Provider-agnostic remote repository payload. class RemoteRepoSchema(BaseModel): provider: GitProvider name: str @@ -250,22 +247,22 @@ class RemoteRepoSchema(BaseModel): html_url: Optional[str] = None ssh_url: Optional[str] = None default_branch: Optional[str] = None -# [/DEF:RemoteRepoSchema:Class] +# #endregion RemoteRepoSchema -# [DEF:RemoteRepoCreateRequest:Class] -# @PURPOSE: Provider-agnostic repository creation request. +# #region RemoteRepoCreateRequest [TYPE Class] +# @BRIEF Provider-agnostic repository creation request. class RemoteRepoCreateRequest(BaseModel): name: str = Field(..., min_length=1, max_length=255) private: bool = True description: Optional[str] = None auto_init: bool = True default_branch: Optional[str] = "main" -# [/DEF:RemoteRepoCreateRequest:Class] +# #endregion RemoteRepoCreateRequest -# [DEF:PromoteRequest:Class] -# @PURPOSE: Request schema for branch promotion workflow. +# #region PromoteRequest [TYPE Class] +# @BRIEF Request schema for branch promotion workflow. class PromoteRequest(BaseModel): from_branch: str = Field(..., min_length=1, max_length=255) to_branch: str = Field(..., min_length=1, max_length=255) @@ -275,11 +272,11 @@ class PromoteRequest(BaseModel): reason: Optional[str] = None draft: bool = False remove_source_branch: bool = False -# [/DEF:PromoteRequest:Class] +# #endregion PromoteRequest -# [DEF:PromoteResponse:Class] -# @PURPOSE: Response schema for promotion operation result. +# #region PromoteResponse [TYPE Class] +# @BRIEF Response schema for promotion operation result. class PromoteResponse(BaseModel): mode: str from_branch: str @@ -288,6 +285,6 @@ class PromoteResponse(BaseModel): url: Optional[str] = None reference_id: Optional[str] = None policy_violation: bool = False -# [/DEF:PromoteResponse:Class] +# #endregion PromoteResponse -# [/DEF:GitSchemas:Module] +# #endregion GitSchemas diff --git a/backend/src/api/routes/health.py b/backend/src/api/routes/health.py index 7ca0ebdf..6aaab40a 100644 --- a/backend/src/api/routes/health.py +++ b/backend/src/api/routes/health.py @@ -1,9 +1,7 @@ -# [DEF:health_router:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: health, monitoring, dashboards -# @PURPOSE: API endpoints for dashboard health monitoring and status aggregation. -# @LAYER: UI/API -# @RELATION: DEPENDS_ON -> health_service +# #region health_router [C:3] [TYPE Module] [SEMANTICS health, monitoring, dashboards] +# @BRIEF API endpoints for dashboard health monitoring and status aggregation. +# @LAYER UI/API +# @RELATION DEPENDS_ON -> [health_service] from fastapi import APIRouter, Depends, Query, HTTPException, status from typing import List, Optional @@ -15,11 +13,11 @@ from ...dependencies import has_permission, get_config_manager, get_task_manager router = APIRouter(prefix="/api/health", tags=["Health"]) -# [DEF:get_health_summary:Function] -# @PURPOSE: Get aggregated health status for all dashboards. -# @PRE: Caller has read permission for dashboard health view. -# @POST: Returns HealthSummaryResponse. -# @RELATION: CALLS -> backend.src.services.health_service.HealthService +# #region get_health_summary [TYPE Function] +# @BRIEF Get aggregated health status for all dashboards. +# @PRE Caller has read permission for dashboard health view. +# @POST Returns HealthSummaryResponse. +# @RELATION CALLS -> [backend.src.services.health_service.HealthService] @router.get("/summary", response_model=HealthSummaryResponse) async def get_health_summary( environment_id: Optional[str] = Query(None), @@ -35,14 +33,14 @@ async def get_health_summary( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Health monitor feature is disabled") service = HealthService(db, config_manager=config_manager) return await service.get_health_summary(environment_id=environment_id) -# [/DEF:get_health_summary:Function] +# #endregion get_health_summary -# [DEF:delete_health_report:Function] -# @PURPOSE: Delete one persisted dashboard validation report from health summary. -# @PRE: Caller has write permission for tasks/report maintenance. -# @POST: Validation record is removed; linked task/logs are cleaned when available. -# @RELATION: CALLS -> backend.src.services.health_service.HealthService +# #region delete_health_report [TYPE Function] +# @BRIEF Delete one persisted dashboard validation report from health summary. +# @PRE Caller has write permission for tasks/report maintenance. +# @POST Validation record is removed; linked task/logs are cleaned when available. +# @RELATION CALLS -> [backend.src.services.health_service.HealthService] @router.delete("/summary/{record_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_health_report( record_id: str, @@ -61,6 +59,6 @@ async def delete_health_report( if not service.delete_validation_report(record_id, task_manager=task_manager): raise HTTPException(status_code=404, detail="Health report not found") return -# [/DEF:delete_health_report:Function] +# #endregion delete_health_report -# [/DEF:health_router:Module] +# #endregion health_router diff --git a/backend/src/api/routes/llm.py b/backend/src/api/routes/llm.py index c534e72e..bd41066a 100644 --- a/backend/src/api/routes/llm.py +++ b/backend/src/api/routes/llm.py @@ -1,12 +1,10 @@ -# [DEF:LlmRoutes:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: api, routes, llm -# @PURPOSE: API routes for LLM provider configuration and management. -# @LAYER: UI (API) -# @RELATION: DEPENDS_ON -> [LLMProviderService] -# @RELATION: DEPENDS_ON -> [LLMProviderConfig] -# @RELATION: DEPENDS_ON -> [get_current_user] -# @RELATION: DEPENDS_ON -> [get_db] +# #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS api, routes, llm] +# @BRIEF API routes for LLM provider configuration and management. +# @LAYER UI (API) +# @RELATION DEPENDS_ON -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] +# @RELATION DEPENDS_ON -> [get_current_user] +# @RELATION DEPENDS_ON -> [get_db] from fastapi import APIRouter, Depends, HTTPException, status from typing import List, Optional @@ -26,17 +24,17 @@ from ...services.llm_provider import LLMProviderService from ...core.database import get_db from sqlalchemy.orm import Session -# [DEF:router:Global] -# @PURPOSE: APIRouter instance for LLM routes. +# #region router [TYPE Global] +# @BRIEF APIRouter instance for LLM routes. router = APIRouter(tags=["LLM"]) -# [/DEF:router:Global] +# #endregion router -# [DEF:_is_valid_runtime_api_key:Function] -# @PURPOSE: Validate decrypted runtime API key presence/shape. -# @PRE: value can be None. -# @POST: Returns True only for non-placeholder key. -# @RELATION: BINDS_TO -> [LlmRoutes] +# #region _is_valid_runtime_api_key [TYPE Function] +# @BRIEF Validate decrypted runtime API key presence/shape. +# @PRE value can be None. +# @POST Returns True only for non-placeholder key. +# @RELATION BINDS_TO -> [LlmRoutes] def _is_valid_runtime_api_key(value: Optional[str]) -> bool: key = (value or "").strip() if not key: @@ -46,13 +44,13 @@ def _is_valid_runtime_api_key(value: Optional[str]) -> bool: return len(key) >= 16 -# [/DEF:_is_valid_runtime_api_key:Function] +# #endregion _is_valid_runtime_api_key -# [DEF:_build_provider_auth_headers:Function] -# @PURPOSE: Build auth headers for provider model listing based on provider type. -# @PRE: provider_type is a valid LLMProviderType. -# @POST: Returns dict of HTTP headers for the provider's model list API. +# #region _build_provider_auth_headers [TYPE Function] +# @BRIEF Build auth headers for provider model listing based on provider type. +# @PRE provider_type is a valid LLMProviderType. +# @POST Returns dict of HTTP headers for the provider's model list API. def _build_provider_auth_headers(api_key: Optional[str], provider_type: LLMProviderType) -> dict: headers = {} if not api_key: @@ -66,15 +64,15 @@ def _build_provider_auth_headers(api_key: Optional[str], provider_type: LLMProvi return headers -# [/DEF:_build_provider_auth_headers:Function] +# #endregion _build_provider_auth_headers -# [DEF:get_providers:Function] -# @PURPOSE: Retrieve all LLM provider configurations. -# @PRE: User is authenticated. -# @POST: Returns list of LLMProviderConfig. -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: DEPENDS_ON -> [LLMProviderConfig] +# #region get_providers [TYPE Function] +# @BRIEF Retrieve all LLM provider configurations. +# @PRE User is authenticated. +# @POST Returns list of LLMProviderConfig. +# @RELATION CALLS -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] @router.get("/providers", response_model=List[LLMProviderConfig]) async def get_providers( current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) @@ -101,15 +99,15 @@ async def get_providers( ] -# [/DEF:get_providers:Function] +# #endregion get_providers -# [DEF:get_llm_status:Function] -# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation. -# @PRE: User is authenticated. -# @POST: configured=true only when an active provider with valid decrypted key exists. -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: CALLS -> [_is_valid_runtime_api_key] +# #region get_llm_status [TYPE Function] +# @BRIEF Returns whether LLM runtime is configured for dashboard validation. +# @PRE User is authenticated. +# @POST configured=true only when an active provider with valid decrypted key exists. +# @RELATION CALLS -> [LLMProviderService] +# @RELATION CALLS -> [_is_valid_runtime_api_key] @router.get("/status") async def get_llm_status( current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db) @@ -172,15 +170,15 @@ async def get_llm_status( } -# [/DEF:get_llm_status:Function] +# #endregion get_llm_status -# [DEF:create_provider:Function] -# @PURPOSE: Create a new LLM provider configuration. -# @PRE: User is authenticated and has admin permissions. -# @POST: Returns the created LLMProviderConfig. -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: DEPENDS_ON -> [LLMProviderConfig] +# #region create_provider [TYPE Function] +# @BRIEF Create a new LLM provider configuration. +# @PRE User is authenticated and has admin permissions. +# @POST Returns the created LLMProviderConfig. +# @RELATION CALLS -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] @router.post( "/providers", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED ) @@ -205,15 +203,15 @@ async def create_provider( ) -# [/DEF:create_provider:Function] +# #endregion create_provider -# [DEF:update_provider:Function] -# @PURPOSE: Update an existing LLM provider configuration. -# @PRE: User is authenticated and has admin permissions. -# @POST: Returns the updated LLMProviderConfig. -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: DEPENDS_ON -> [LLMProviderConfig] +# #region update_provider [TYPE Function] +# @BRIEF Update an existing LLM provider configuration. +# @PRE User is authenticated and has admin permissions. +# @POST Returns the updated LLMProviderConfig. +# @RELATION CALLS -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] @router.put("/providers/{provider_id}", response_model=LLMProviderConfig) async def update_provider( provider_id: str, @@ -240,14 +238,14 @@ async def update_provider( ) -# [/DEF:update_provider:Function] +# #endregion update_provider -# [DEF:delete_provider:Function] -# @PURPOSE: Delete an LLM provider configuration. -# @PRE: User is authenticated and has admin permissions. -# @POST: Returns success status. -# @RELATION: CALLS -> [LLMProviderService] +# #region delete_provider [TYPE Function] +# @BRIEF Delete an LLM provider configuration. +# @PRE User is authenticated and has admin permissions. +# @POST Returns success status. +# @RELATION CALLS -> [LLMProviderService] @router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_provider( provider_id: str, @@ -263,14 +261,14 @@ async def delete_provider( return -# [/DEF:delete_provider:Function] +# #endregion delete_provider -# [DEF:fetch_models_for_provider:Function] -# @PURPOSE: Fetch available models from a provider's API endpoint. -# @PRE: valid base_url and optional api_key/provider_id. -# @POST: Returns list of model IDs sorted alphabetically. -# @SIDE_EFFECT: Makes HTTP GET to multiple possible model endpoints. +# #region fetch_models_for_provider [TYPE Function] +# @BRIEF Fetch available models from a provider's API endpoint. +# @PRE valid base_url and optional api_key/provider_id. +# @POST Returns list of model IDs sorted alphabetically. +# @SIDE_EFFECT Makes HTTP GET to multiple possible model endpoints. @router.post("/providers/fetch-models", response_model=FetchModelsResponse) async def fetch_models_for_provider( request: FetchModelsRequest, @@ -382,15 +380,15 @@ async def fetch_models_for_provider( return FetchModelsResponse(models=[], total=0) -# [/DEF:fetch_models_for_provider:Function] +# #endregion fetch_models_for_provider -# [DEF:test_connection:Function] -# @PURPOSE: Test connection to an LLM provider. -# @PRE: User is authenticated. -# @POST: Returns success status and message. -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: DEPENDS_ON -> [LLMClient] +# #region test_connection [TYPE Function] +# @BRIEF Test connection to an LLM provider. +# @PRE User is authenticated. +# @POST Returns success status and message. +# @RELATION CALLS -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [LLMClient] @router.post("/providers/{provider_id}/test") async def test_connection( provider_id: str, @@ -434,15 +432,15 @@ async def test_connection( return {"success": False, "error": str(e)} -# [/DEF:test_connection:Function] +# #endregion test_connection -# [DEF:test_provider_config:Function] -# @PURPOSE: Test connection with a provided configuration (not yet saved). -# @PRE: User is authenticated. -# @POST: Returns success status and message. -# @RELATION: DEPENDS_ON -> [LLMClient] -# @RELATION: DEPENDS_ON -> [LLMProviderConfig] +# #region test_provider_config [TYPE Function] +# @BRIEF Test connection with a provided configuration (not yet saved). +# @PRE User is authenticated. +# @POST Returns success status and message. +# @RELATION DEPENDS_ON -> [LLMClient] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] @router.post("/providers/test") async def test_provider_config( config: LLMProviderConfig, current_user: User = Depends(get_current_active_user) @@ -476,6 +474,6 @@ async def test_provider_config( return {"success": False, "error": str(e)} -# [/DEF:test_provider_config:Function] +# #endregion test_provider_config -# [/DEF:LlmRoutes:Module] +# #endregion LlmRoutes diff --git a/backend/src/api/routes/mappings.py b/backend/src/api/routes/mappings.py index 7a4337f2..0d3ba51f 100644 --- a/backend/src/api/routes/mappings.py +++ b/backend/src/api/routes/mappings.py @@ -1,14 +1,12 @@ -# [DEF:MappingsApi:Module] +# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS api, mappings, database, fuzzy-matching] +# @BRIEF API endpoints for managing database mappings and getting suggestions. +# @LAYER API +# @INVARIANT Mappings are persisted in the SQLite database. +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [DatabaseModule] +# @RELATION DEPENDS_ON -> [mapping_service] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, mappings, database, fuzzy-matching -# @PURPOSE: API endpoints for managing database mappings and getting suggestions. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [AppDependencies] -# @RELATION: DEPENDS_ON -> [DatabaseModule] -# @RELATION: DEPENDS_ON -> [mapping_service] # -# @INVARIANT: Mappings are persisted in the SQLite database. # [SECTION: IMPORTS] from fastapi import APIRouter, Depends, HTTPException @@ -23,7 +21,7 @@ from pydantic import BaseModel router = APIRouter(tags=["mappings"]) -# [DEF:MappingCreate:DataClass] +# #region MappingCreate [TYPE DataClass] class MappingCreate(BaseModel): source_env_id: str target_env_id: str @@ -32,9 +30,9 @@ class MappingCreate(BaseModel): source_db_name: str target_db_name: str engine: Optional[str] = None -# [/DEF:MappingCreate:DataClass] +# #endregion MappingCreate -# [DEF:MappingResponse:DataClass] +# #region MappingResponse [TYPE DataClass] class MappingResponse(BaseModel): id: str source_env_id: str @@ -47,18 +45,18 @@ class MappingResponse(BaseModel): class Config: from_attributes = True -# [/DEF:MappingResponse:DataClass] +# #endregion MappingResponse -# [DEF:SuggestRequest:DataClass] +# #region SuggestRequest [TYPE DataClass] class SuggestRequest(BaseModel): source_env_id: str target_env_id: str -# [/DEF:SuggestRequest:DataClass] +# #endregion SuggestRequest -# [DEF:get_mappings:Function] -# @PURPOSE: List all saved database mappings. -# @PRE: db session is injected. -# @POST: Returns filtered list of DatabaseMapping records. +# #region get_mappings [TYPE Function] +# @BRIEF List all saved database mappings. +# @PRE db session is injected. +# @POST Returns filtered list of DatabaseMapping records. @router.get("", response_model=List[MappingResponse]) async def get_mappings( source_env_id: Optional[str] = None, @@ -73,12 +71,12 @@ async def get_mappings( if target_env_id: query = query.filter(DatabaseMapping.target_env_id == target_env_id) return query.all() -# [/DEF:get_mappings:Function] +# #endregion get_mappings -# [DEF:create_mapping:Function] -# @PURPOSE: Create or update a database mapping. -# @PRE: mapping is valid MappingCreate, db session is injected. -# @POST: DatabaseMapping created or updated in database. +# #region create_mapping [TYPE Function] +# @BRIEF Create or update a database mapping. +# @PRE mapping is valid MappingCreate, db session is injected. +# @POST DatabaseMapping created or updated in database. @router.post("", response_model=MappingResponse) async def create_mapping( mapping: MappingCreate, @@ -106,12 +104,12 @@ async def create_mapping( db.commit() db.refresh(new_mapping) return new_mapping -# [/DEF:create_mapping:Function] +# #endregion create_mapping -# [DEF:suggest_mappings_api:Function] -# @PURPOSE: Get suggested mappings based on fuzzy matching. -# @PRE: request is valid SuggestRequest, config_manager is injected. -# @POST: Returns mapping suggestions. +# #region suggest_mappings_api [TYPE Function] +# @BRIEF Get suggested mappings based on fuzzy matching. +# @PRE request is valid SuggestRequest, config_manager is injected. +# @POST Returns mapping suggestions. @router.post("/suggest") async def suggest_mappings_api( request: SuggestRequest, @@ -125,6 +123,6 @@ async def suggest_mappings_api( return await service.get_suggestions(request.source_env_id, request.target_env_id) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -# [/DEF:suggest_mappings_api:Function] +# #endregion suggest_mappings_api -# [/DEF:MappingsApi:Module] +# #endregion MappingsApi diff --git a/backend/src/api/routes/migration.py b/backend/src/api/routes/migration.py index 618a5a15..815c2539 100644 --- a/backend/src/api/routes/migration.py +++ b/backend/src/api/routes/migration.py @@ -1,20 +1,18 @@ -# [DEF:MigrationApi:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: api, migration, dashboards, sync, dry-run -# @PURPOSE: HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints. -# @LAYER: Infra -# @RELATION: DEPENDS_ON ->[AppDependencies] -# @RELATION: DEPENDS_ON ->[DatabaseModule] -# @RELATION: DEPENDS_ON ->[DashboardSelection] -# @RELATION: DEPENDS_ON ->[DashboardMetadata] -# @RELATION: DEPENDS_ON ->[MigrationDryRunService] -# @RELATION: DEPENDS_ON ->[IdMappingService] -# @RELATION: DEPENDS_ON ->[ResourceMapping] -# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures. -# @PRE: Backend core services initialized and Database session available. -# @POST: Migration tasks are enqueued or dry-run results are computed and returned. -# @SIDE_EFFECT: Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls. -# @DATA_CONTRACT: [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary] +# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS api, migration, dashboards, sync, dry-run] +# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints. +# @LAYER Infra +# @PRE Backend core services initialized and Database session available. +# @POST Migration tasks are enqueued or dry-run results are computed and returned. +# @SIDE_EFFECT Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls. +# @DATA_CONTRACT [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary] +# @INVARIANT Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures. +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [DatabaseModule] +# @RELATION DEPENDS_ON -> [DashboardSelection] +# @RELATION DEPENDS_ON -> [DashboardMetadata] +# @RELATION DEPENDS_ON -> [MigrationDryRunService] +# @RELATION DEPENDS_ON -> [IdMappingService] +# @RELATION DEPENDS_ON -> [ResourceMapping] # @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary] # @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404] # @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields] @@ -40,14 +38,13 @@ logger = cast(Any, logger) router = APIRouter(prefix="/api", tags=["migration"]) -# [DEF:get_dashboards:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Fetch dashboard metadata from a requested environment for migration selection UI. -# @PRE: env_id is provided and exists in configured environments. -# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent. -# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network. -# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]] -# @RELATION: CALLS ->[SupersetClient.get_dashboards_summary] +# #region get_dashboards [C:3] [TYPE Function] +# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI. +# @PRE env_id is provided and exists in configured environments. +# @POST Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent. +# @SIDE_EFFECT Reads environment configuration and performs remote Superset metadata retrieval over network. +# @DATA_CONTRACT Input[str env_id] -> Output[List[DashboardMetadata]] +# @RELATION CALLS -> [SupersetClient.get_dashboards_summary] @router.get("/environments/{env_id}/dashboards", response_model=List[DashboardMetadata]) async def get_dashboards( env_id: str, @@ -69,19 +66,18 @@ async def get_dashboards( return dashboards -# [/DEF:get_dashboards:Function] +# #endregion get_dashboards -# [DEF:execute_migration:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Validate migration selection and enqueue asynchronous migration task execution. -# @PRE: DashboardSelection payload is valid and both source/target environments exist. -# @POST: Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure. -# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs. -# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]] -# @RELATION: CALLS ->[create_task] -# @RELATION: DEPENDS_ON ->[DashboardSelection] -# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation. +# #region execute_migration [C:5] [TYPE Function] +# @BRIEF Validate migration selection and enqueue asynchronous migration task execution. +# @PRE DashboardSelection payload is valid and both source/target environments exist. +# @POST Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure. +# @SIDE_EFFECT Reads configuration, writes task record through task manager, and writes operational logs. +# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, str]] +# @INVARIANT Migration task dispatch never occurs before source and target environment ids pass guard validation. +# @RELATION CALLS -> [create_task] +# @RELATION DEPENDS_ON -> [DashboardSelection] @router.post("/migration/execute") async def execute_migration( selection: DashboardSelection, @@ -133,19 +129,18 @@ async def execute_migration( ) -# [/DEF:execute_migration:Function] +# #endregion execute_migration -# [DEF:dry_run_migration:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Build pre-flight migration diff and risk summary without mutating target systems. -# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty. -# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors. -# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API. -# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]] -# @RELATION: DEPENDS_ON ->[DashboardSelection] -# @RELATION: DEPENDS_ON ->[MigrationDryRunService] -# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution. +# #region dry_run_migration [C:5] [TYPE Function] +# @BRIEF Build pre-flight migration diff and risk summary without mutating target systems. +# @PRE DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty. +# @POST Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors. +# @SIDE_EFFECT Reads local mappings from DB and fetches source/target metadata via Superset API. +# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, Any]] +# @INVARIANT Dry-run flow remains read-only and rejects identical source/target environments before service execution. +# @RELATION DEPENDS_ON -> [DashboardSelection] +# @RELATION DEPENDS_ON -> [MigrationDryRunService] @router.post("/migration/dry-run", response_model=Dict[str, Any]) async def dry_run_migration( selection: DashboardSelection, @@ -200,17 +195,16 @@ async def dry_run_migration( raise HTTPException(status_code=500, detail=str(exc)) from exc -# [/DEF:dry_run_migration:Function] +# #endregion dry_run_migration -# [DEF:get_migration_settings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Read and return configured migration synchronization cron expression. -# @PRE: Configuration store is available and requester has READ permission. -# @POST: Returns {"cron": str} reflecting current persisted settings value. -# @SIDE_EFFECT: Reads configuration from config manager. -# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]] -# @RELATION: DEPENDS_ON ->[AppDependencies] +# #region get_migration_settings [C:3] [TYPE Function] +# @BRIEF Read and return configured migration synchronization cron expression. +# @PRE Configuration store is available and requester has READ permission. +# @POST Returns {"cron": str} reflecting current persisted settings value. +# @SIDE_EFFECT Reads configuration from config manager. +# @DATA_CONTRACT Input[None] -> Output[Dict[str, str]] +# @RELATION DEPENDS_ON -> [AppDependencies] @router.get("/migration/settings", response_model=Dict[str, str]) async def get_migration_settings( config_manager=Depends(get_config_manager), @@ -222,17 +216,16 @@ async def get_migration_settings( return {"cron": cron} -# [/DEF:get_migration_settings:Function] +# #endregion get_migration_settings -# [DEF:update_migration_settings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Validate and persist migration synchronization cron expression update. -# @PRE: Payload includes "cron" key and requester has WRITE permission. -# @POST: Returns {"cron": str, "status": "updated"} and persists updated cron value. -# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager. -# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]] -# @RELATION: DEPENDS_ON ->[AppDependencies] +# #region update_migration_settings [C:3] [TYPE Function] +# @BRIEF Validate and persist migration synchronization cron expression update. +# @PRE Payload includes "cron" key and requester has WRITE permission. +# @POST Returns {"cron": str, "status": "updated"} and persists updated cron value. +# @SIDE_EFFECT Mutates configuration and writes persisted config through config manager. +# @DATA_CONTRACT Input[Dict[str, str]] -> Output[Dict[str, str]] +# @RELATION DEPENDS_ON -> [AppDependencies] @router.put("/migration/settings", response_model=Dict[str, str]) async def update_migration_settings( payload: Dict[str, str], @@ -254,17 +247,16 @@ async def update_migration_settings( return {"cron": cron_expr, "status": "updated"} -# [/DEF:update_migration_settings:Function] +# #endregion update_migration_settings -# [DEF:get_resource_mappings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Fetch synchronized resource mappings with optional filters and pagination for migration mappings view. -# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission. -# @POST: Returns {"items": [...], "total": int} where items reflect applied filters and pagination. -# @SIDE_EFFECT: Executes database read queries against ResourceMapping table. -# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]] -# @RELATION: DEPENDS_ON ->[ResourceMapping] +# #region get_resource_mappings [C:3] [TYPE Function] +# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view. +# @PRE skip>=0, 1<=limit<=500, DB session is active, requester has READ permission. +# @POST Returns {"items": [...], "total": int} where items reflect applied filters and pagination. +# @SIDE_EFFECT Executes database read queries against ResourceMapping table. +# @DATA_CONTRACT Input[QueryParams] -> Output[Dict[str, Any]] +# @RELATION DEPENDS_ON -> [ResourceMapping] @router.get("/migration/mappings-data", response_model=Dict[str, Any]) async def get_resource_mappings( skip: int = Query(0, ge=0), @@ -327,18 +319,17 @@ async def get_resource_mappings( return {"items": items, "total": total} -# [/DEF:get_resource_mappings:Function] +# #endregion get_resource_mappings -# [DEF:trigger_sync_now:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Trigger immediate ID synchronization for every configured environment. -# @PRE: At least one environment is configured and requester has EXECUTE permission. -# @POST: Returns sync summary with synced/failed counts after attempting all environments. -# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs. -# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]] -# @RELATION: DEPENDS_ON ->[IdMappingService] -# @RELATION: CALLS ->[sync_environment] +# #region trigger_sync_now [C:3] [TYPE Function] +# @BRIEF Trigger immediate ID synchronization for every configured environment. +# @PRE At least one environment is configured and requester has EXECUTE permission. +# @POST Returns sync summary with synced/failed counts after attempting all environments. +# @SIDE_EFFECT Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs. +# @DATA_CONTRACT Input[None] -> Output[Dict[str, Any]] +# @RELATION DEPENDS_ON -> [IdMappingService] +# @RELATION CALLS -> [sync_environment] @router.post("/migration/sync-now", response_model=Dict[str, Any]) async def trigger_sync_now( config_manager=Depends(get_config_manager), @@ -394,6 +385,6 @@ async def trigger_sync_now( } -# [/DEF:trigger_sync_now:Function] +# #endregion trigger_sync_now -# [/DEF:MigrationApi:Module] +# #endregion MigrationApi diff --git a/backend/src/api/routes/plugins.py b/backend/src/api/routes/plugins.py index f5b830ed..847c24b2 100755 --- a/backend/src/api/routes/plugins.py +++ b/backend/src/api/routes/plugins.py @@ -1,11 +1,9 @@ -# [DEF:PluginsRouter:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: api, router, plugins, list -# @PURPOSE: Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins. -# @LAYER: UI (API) -# @RELATION: DEPENDS_ON -> [PluginConfig] -# @RELATION: DEPENDS_ON -> [get_plugin_loader] -# @RELATION: BINDS_TO -> [API_Routes] +# #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS api, router, plugins, list] +# @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins. +# @LAYER UI (API) +# @RELATION DEPENDS_ON -> [PluginConfig] +# @RELATION DEPENDS_ON -> [get_plugin_loader] +# @RELATION BINDS_TO -> [API_Routes] from typing import List from fastapi import APIRouter, Depends @@ -16,13 +14,13 @@ from ...core.logger import belief_scope router = APIRouter() -# [DEF:list_plugins:Function] -# @PURPOSE: Retrieve a list of all available plugins. -# @PRE: plugin_loader is injected via Depends. -# @POST: Returns a list of PluginConfig objects. -# @RETURN: List[PluginConfig] - List of registered plugins. -# @RELATION: CALLS -> [get_plugin_loader] -# @RELATION: DEPENDS_ON -> [PluginConfig] +# #region list_plugins [TYPE Function] +# @BRIEF Retrieve a list of all available plugins. +# @PRE plugin_loader is injected via Depends. +# @POST Returns a list of PluginConfig objects. +# @RETURN List[PluginConfig] - List of registered plugins. +# @RELATION CALLS -> [get_plugin_loader] +# @RELATION DEPENDS_ON -> [PluginConfig] @router.get("", response_model=List[PluginConfig]) async def list_plugins( plugin_loader=Depends(get_plugin_loader), @@ -35,5 +33,5 @@ async def list_plugins( return plugin_loader.get_all_plugin_configs() -# [/DEF:list_plugins:Function] -# [/DEF:PluginsRouter:Module] +# #endregion list_plugins +# #endregion PluginsRouter diff --git a/backend/src/api/routes/profile.py b/backend/src/api/routes/profile.py index a723f7a0..2c701c1e 100644 --- a/backend/src/api/routes/profile.py +++ b/backend/src/api/routes/profile.py @@ -1,14 +1,12 @@ -# [DEF:ProfileApiModule:Module] +# #region ProfileApiModule [C:3] [TYPE Module] [SEMANTICS api, profile, preferences, self-service, account-lookup] +# @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup. +# @LAYER API +# @INVARIANT Endpoints are self-scoped and never mutate another user preference. +# @RELATION DEPENDS_ON -> [ProfileService] +# @RELATION DEPENDS_ON -> [get_current_user] +# @RELATION DEPENDS_ON -> [get_db] # -# @COMPLEXITY: 3 -# @SEMANTICS: api, profile, preferences, self-service, account-lookup -# @PURPOSE: Exposes self-scoped profile preference endpoints and environment-based Superset account lookup. -# @LAYER: API -# @RELATION: [DEPENDS_ON] ->[ProfileService] -# @RELATION: [DEPENDS_ON] ->[get_current_user] -# @RELATION: [DEPENDS_ON] ->[get_db] # -# @INVARIANT: Endpoints are self-scoped and never mutate another user preference. # @UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user. # @UX_STATE: Saving -> Validation errors map to actionable 422 details. # @UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload. @@ -49,25 +47,25 @@ log = MarkerLogger("Profile") router = APIRouter(prefix="/api/profile", tags=["profile"]) -# [DEF:_get_profile_service:Function] -# @RELATION: CALLS -> ProfileService -# @PURPOSE: Build profile service for current request scope. -# @PRE: db session and config manager are available. -# @POST: Returns a ready ProfileService instance. +# #region _get_profile_service [TYPE Function] +# @BRIEF Build profile service for current request scope. +# @PRE db session and config manager are available. +# @POST Returns a ready ProfileService instance. +# @RELATION CALLS -> [ProfileService] def _get_profile_service(db: Session, config_manager, plugin_loader=None) -> ProfileService: return ProfileService( db=db, config_manager=config_manager, plugin_loader=plugin_loader, ) -# [/DEF:_get_profile_service:Function] +# #endregion _get_profile_service -# [DEF:get_preferences:Function] -# @RELATION: CALLS -> ProfileService -# @PURPOSE: Get authenticated user's dashboard filter preference. -# @PRE: Valid JWT and authenticated user context. -# @POST: Returns preference payload for current user only. +# #region get_preferences [TYPE Function] +# @BRIEF Get authenticated user's dashboard filter preference. +# @PRE Valid JWT and authenticated user context. +# @POST Returns preference payload for current user only. +# @RELATION CALLS -> [ProfileService] @router.get("/preferences", response_model=ProfilePreferenceResponse) async def get_preferences( current_user: User = Depends(get_current_user), @@ -79,14 +77,14 @@ async def get_preferences( log.reason("Resolving current user preference") service = _get_profile_service(db, config_manager, plugin_loader) return service.get_my_preference(current_user) -# [/DEF:get_preferences:Function] +# #endregion get_preferences -# [DEF:update_preferences:Function] -# @RELATION: CALLS -> ProfileService -# @PURPOSE: Update authenticated user's dashboard filter preference. -# @PRE: Valid JWT and valid request payload. -# @POST: Persists normalized preference for current user or raises validation/authorization errors. +# #region update_preferences [TYPE Function] +# @BRIEF Update authenticated user's dashboard filter preference. +# @PRE Valid JWT and valid request payload. +# @POST Persists normalized preference for current user or raises validation/authorization errors. +# @RELATION CALLS -> [ProfileService] @router.patch("/preferences", response_model=ProfilePreferenceResponse) async def update_preferences( payload: ProfilePreferenceUpdateRequest, @@ -106,14 +104,14 @@ async def update_preferences( except ProfileAuthorizationError as exc: log.explore("Cross-user mutation guard blocked request", error="User attempted to mutate another user's resource") raise HTTPException(status_code=403, detail=str(exc)) from exc -# [/DEF:update_preferences:Function] +# #endregion update_preferences -# [DEF:lookup_superset_accounts:Function] -# @RELATION: CALLS -> ProfileService -# @PURPOSE: Lookup Superset account candidates in selected environment. -# @PRE: Valid JWT, authenticated context, and environment_id query parameter. -# @POST: Returns success or degraded lookup payload with stable shape. +# #region lookup_superset_accounts [TYPE Function] +# @BRIEF Lookup Superset account candidates in selected environment. +# @PRE Valid JWT, authenticated context, and environment_id query parameter. +# @POST Returns success or degraded lookup payload with stable shape. +# @RELATION CALLS -> [ProfileService] @router.get("/superset-accounts", response_model=SupersetAccountLookupResponse) async def lookup_superset_accounts( environment_id: str = Query(...), @@ -149,6 +147,6 @@ async def lookup_superset_accounts( except EnvironmentNotFoundError as exc: log.explore("Lookup request references unknown environment", error="EnvironmentNotFoundError raised during Superset account lookup") raise HTTPException(status_code=404, detail=str(exc)) from exc -# [/DEF:lookup_superset_accounts:Function] +# #endregion lookup_superset_accounts -# [/DEF:ProfileApiModule:Module] \ No newline at end of file +# #endregion ProfileApiModule diff --git a/backend/src/api/routes/reports.py b/backend/src/api/routes/reports.py index 86b57905..dfb7d12f 100644 --- a/backend/src/api/routes/reports.py +++ b/backend/src/api/routes/reports.py @@ -1,17 +1,15 @@ -# [DEF:ReportsRouter:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: api, reports, list, detail, pagination, filters -# @PURPOSE: FastAPI router for unified task report list and detail retrieval endpoints. -# @LAYER: UI (API) -# @RELATION: [DEPENDS_ON] ->[ReportsService:Class] -# @RELATION: [DEPENDS_ON] ->[get_task_manager:Function] -# @RELATION: [DEPENDS_ON] ->[get_clean_release_repository:Function] -# @RELATION: [DEPENDS_ON] ->[has_permission:Function] -# @INVARIANT: Endpoints are read-only and do not trigger long-running tasks. -# @PRE: Reports service and dependencies are initialized. -# @POST: Router is configured and endpoints are ready for registration. -# @SIDE_EFFECT: None -# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView] +# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS api, reports, list, detail, pagination, filters] +# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints. +# @LAYER UI (API) +# @PRE Reports service and dependencies are initialized. +# @POST Router is configured and endpoints are ready for registration. +# @SIDE_EFFECT None +# @DATA_CONTRACT [ReportQuery] -> [ReportCollection | ReportDetailView] +# @INVARIANT Endpoints are read-only and do not trigger long-running tasks. +# @RELATION DEPENDS_ON -> [ReportsService:Class] +# @RELATION DEPENDS_ON -> [get_task_manager:Function] +# @RELATION DEPENDS_ON -> [get_clean_release_repository:Function] +# @RELATION DEPENDS_ON -> [has_permission:Function] # [SECTION: IMPORTS] from datetime import datetime @@ -40,11 +38,10 @@ from ...services.reports.report_service import ReportsService router = APIRouter(prefix="/api/reports", tags=["Reports"]) -# [DEF:_parse_csv_enum_list:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Parse comma-separated query value into enum list. -# @PRE: raw may be None/empty or comma-separated values. -# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload. +# #region _parse_csv_enum_list [C:1] [TYPE Function] +# @BRIEF Parse comma-separated query value into enum list. +# @PRE raw may be None/empty or comma-separated values. +# @POST Returns enum list or raises HTTP 400 with deterministic machine-readable payload. # @PARAM: raw (Optional[str]) - Comma-separated enum values. # @PARAM: enum_cls (type) - Enum class for validation. # @PARAM: field_name (str) - Query field name for diagnostics. @@ -75,18 +72,17 @@ def _parse_csv_enum_list(raw: Optional[str], enum_cls, field_name: str) -> List: return parsed -# [/DEF:_parse_csv_enum_list:Function] +# #endregion _parse_csv_enum_list -# [DEF:list_reports:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return paginated unified reports list. -# @PRE: authenticated/authorized request and validated query params. -# @POST: returns {items,total,page,page_size,has_next,applied_filters}. -# @POST: deterministic error payload for invalid filters. -# @RELATION: [CALLS] ->[_parse_csv_enum_list:Function] -# @RELATION: [DEPENDS_ON] ->[ReportQuery:Class] -# @RELATION: [DEPENDS_ON] ->[ReportsService:Class] +# #region list_reports [C:2] [TYPE Function] +# @BRIEF Return paginated unified reports list. +# @PRE authenticated/authorized request and validated query params. +# @POST returns {items,total,page,page_size,has_next,applied_filters}. +# @POST deterministic error payload for invalid filters. +# @RELATION CALLS -> [_parse_csv_enum_list:Function] +# @RELATION DEPENDS_ON -> [ReportQuery:Class] +# @RELATION DEPENDS_ON -> [ReportsService:Class] # # @TEST_CONTRACT: ListReportsApi -> # { @@ -151,15 +147,14 @@ async def list_reports( return service.list_reports(query) -# [/DEF:list_reports:Function] +# #endregion list_reports -# [DEF:get_report_detail:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Return one normalized report detail with diagnostics and next actions. -# @PRE: authenticated/authorized request and existing report_id. -# @POST: returns normalized detail envelope or 404 when report is not found. -# @RELATION: CALLS -> [ReportsService:Class] +# #region get_report_detail [C:2] [TYPE Function] +# @BRIEF Return one normalized report detail with diagnostics and next actions. +# @PRE authenticated/authorized request and existing report_id. +# @POST returns normalized detail envelope or 404 when report is not found. +# @RELATION CALLS -> [ReportsService:Class] @router.get("/{report_id}", response_model=ReportDetailView) async def get_report_detail( report_id: str, @@ -182,6 +177,6 @@ async def get_report_detail( return detail -# [/DEF:get_report_detail:Function] +# #endregion get_report_detail -# [/DEF:ReportsRouter:Module] +# #endregion ReportsRouter diff --git a/backend/src/api/routes/settings.py b/backend/src/api/routes/settings.py index a9f8022a..fd2cc688 100755 --- a/backend/src/api/routes/settings.py +++ b/backend/src/api/routes/settings.py @@ -1,14 +1,12 @@ -# [DEF:SettingsRouter:Module] +# #region SettingsRouter [C:3] [TYPE Module] [SEMANTICS settings, api, router, fastapi] +# @BRIEF Provides API endpoints for managing application settings and Superset environments. +# @LAYER API +# @INVARIANT All settings changes must be persisted via ConfigManager. +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [get_config_manager] +# @RELATION DEPENDS_ON -> [has_permission] # -# @COMPLEXITY: 3 -# @SEMANTICS: settings, api, router, fastapi -# @PURPOSE: Provides API endpoints for managing application settings and Superset environments. -# @LAYER: API -# @RELATION: [DEPENDS_ON] ->[ConfigManager] -# @RELATION: [DEPENDS_ON] ->[get_config_manager] -# @RELATION: [DEPENDS_ON] ->[has_permission] # -# @INVARIANT: All settings changes must be persisted via ConfigManager. # @PUBLIC_API: router # [SECTION: IMPORTS] @@ -37,26 +35,23 @@ from sqlalchemy.orm import Session # [/SECTION] -# [DEF:LoggingConfigResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Response model for logging configuration with current task log level. -# @SEMANTICS: logging, config, response +# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response] +# @BRIEF Response model for logging configuration with current task log level. class LoggingConfigResponse(BaseModel): level: str task_log_level: str enable_belief_state: bool -# [/DEF:LoggingConfigResponse:Class] +# #endregion LoggingConfigResponse router = APIRouter() -# [DEF:_normalize_superset_env_url:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Canonicalize Superset environment URL to base host/path without trailing /api/v1. -# @PRE: raw_url can be empty. -# @POST: Returns normalized base URL. +# #region _normalize_superset_env_url [C:1] [TYPE Function] +# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1. +# @PRE raw_url can be empty. +# @POST Returns normalized base URL. def _normalize_superset_env_url(raw_url: str) -> str: normalized = str(raw_url or "").strip().rstrip("/") if normalized.lower().endswith("/api/v1"): @@ -64,14 +59,13 @@ def _normalize_superset_env_url(raw_url: str) -> str: return normalized.rstrip("/") -# [/DEF:_normalize_superset_env_url:Function] +# #endregion _normalize_superset_env_url -# [DEF:_validate_superset_connection_fast:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Run lightweight Superset connectivity validation without full pagination scan. -# @PRE: env contains valid URL and credentials. -# @POST: Raises on auth/API failures; returns None on success. +# #region _validate_superset_connection_fast [C:2] [TYPE Function] +# @BRIEF Run lightweight Superset connectivity validation without full pagination scan. +# @PRE env contains valid URL and credentials. +# @POST Raises on auth/API failures; returns None on success. def _validate_superset_connection_fast(env: Environment) -> None: client = SupersetClient(env) # 1) Explicit auth check @@ -86,15 +80,14 @@ def _validate_superset_connection_fast(env: Environment) -> None: ) -# [/DEF:_validate_superset_connection_fast:Function] +# #endregion _validate_superset_connection_fast -# [DEF:get_settings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Retrieves all application settings. -# @PRE: Config manager is available. -# @POST: Returns masked AppConfig. -# @RETURN: AppConfig - The current configuration. +# #region get_settings [C:2] [TYPE Function] +# @BRIEF Retrieves all application settings. +# @PRE Config manager is available. +# @POST Returns masked AppConfig. +# @RETURN AppConfig - The current configuration. @router.get("", response_model=AppConfig) async def get_settings( config_manager: ConfigManager = Depends(get_config_manager), @@ -111,15 +104,14 @@ async def get_settings( return config -# [/DEF:get_settings:Function] +# #endregion get_settings -# [DEF:get_features:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Public endpoint returning feature flags for frontend sidebar filtering. -# @RATIONALE: Unauthenticated because sidebar filtering must work for all users, not just admins. -# @PRE: Config manager is available. -# @POST: Returns dict with dataset_review and health_monitor booleans. +# #region get_features [C:1] [TYPE Function] +# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering. +# @PRE Config manager is available. +# @POST Returns dict with dataset_review and health_monitor booleans. +# @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins. @router.get("/features") async def get_features( config_manager: ConfigManager = Depends(get_config_manager), @@ -127,14 +119,13 @@ async def get_features( return config_manager.get_config().settings.features.model_dump() -# [/DEF:get_features:Function] +# #endregion get_features -# [DEF:update_global_settings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Updates global application settings. -# @PRE: New settings are provided. -# @POST: Global settings are updated. +# #region update_global_settings [C:2] [TYPE Function] +# @BRIEF Updates global application settings. +# @PRE New settings are provided. +# @POST Global settings are updated. # @PARAM: settings (GlobalSettings) - The new global settings. # @RETURN: GlobalSettings - The updated settings. @router.patch("/global", response_model=GlobalSettings) @@ -150,13 +141,12 @@ async def update_global_settings( return settings -# [/DEF:update_global_settings:Function] +# #endregion update_global_settings -# [DEF:get_storage_settings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Retrieves storage-specific settings. -# @RETURN: StorageConfig - The storage configuration. +# #region get_storage_settings [C:2] [TYPE Function] +# @BRIEF Retrieves storage-specific settings. +# @RETURN StorageConfig - The storage configuration. @router.get("/storage", response_model=StorageConfig) async def get_storage_settings( config_manager: ConfigManager = Depends(get_config_manager), @@ -166,12 +156,11 @@ async def get_storage_settings( return config_manager.get_config().settings.storage -# [/DEF:get_storage_settings:Function] +# #endregion get_storage_settings -# [DEF:update_storage_settings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Updates storage-specific settings. +# #region update_storage_settings [C:2] [TYPE Function] +# @BRIEF Updates storage-specific settings. # @PARAM: storage (StorageConfig) - The new storage settings. # @POST: Storage settings are updated and saved. # @RETURN: StorageConfig - The updated storage settings. @@ -192,15 +181,14 @@ async def update_storage_settings( return config_manager.get_config().settings.storage -# [/DEF:update_storage_settings:Function] +# #endregion update_storage_settings -# [DEF:get_environments:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Lists all configured Superset environments. -# @PRE: Config manager is available. -# @POST: Returns list of environments. -# @RETURN: List[Environment] - List of environments. +# #region get_environments [C:2] [TYPE Function] +# @BRIEF Lists all configured Superset environments. +# @PRE Config manager is available. +# @POST Returns list of environments. +# @RETURN List[Environment] - List of environments. @router.get("/environments", response_model=List[Environment]) async def get_environments( config_manager: ConfigManager = Depends(get_config_manager), @@ -215,14 +203,13 @@ async def get_environments( ] -# [/DEF:get_environments:Function] +# #endregion get_environments -# [DEF:add_environment:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Adds a new Superset environment. -# @PRE: Environment data is valid and reachable. -# @POST: Environment is added to config. +# #region add_environment [C:2] [TYPE Function] +# @BRIEF Adds a new Superset environment. +# @PRE Environment data is valid and reachable. +# @POST Environment is added to config. # @PARAM: env (Environment) - The environment to add. # @RETURN: Environment - The added environment. @router.post("/environments", response_model=Environment) @@ -248,14 +235,13 @@ async def add_environment( return env -# [/DEF:add_environment:Function] +# #endregion add_environment -# [DEF:update_environment:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Updates an existing Superset environment. -# @PRE: ID and valid environment data are provided. -# @POST: Environment is updated in config. +# #region update_environment [C:2] [TYPE Function] +# @BRIEF Updates an existing Superset environment. +# @PRE ID and valid environment data are provided. +# @POST Environment is updated in config. # @PARAM: id (str) - The ID of the environment to update. # @PARAM: env (Environment) - The updated environment data. # @RETURN: Environment - The updated environment. @@ -293,14 +279,13 @@ async def update_environment( raise HTTPException(status_code=404, detail=f"Environment {id} not found") -# [/DEF:update_environment:Function] +# #endregion update_environment -# [DEF:delete_environment:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Deletes a Superset environment. -# @PRE: ID is provided. -# @POST: Environment is removed from config. +# #region delete_environment [C:2] [TYPE Function] +# @BRIEF Deletes a Superset environment. +# @PRE ID is provided. +# @POST Environment is removed from config. # @PARAM: id (str) - The ID of the environment to delete. @router.delete("/environments/{id}") async def delete_environment( @@ -312,14 +297,13 @@ async def delete_environment( return {"message": f"Environment {id} deleted"} -# [/DEF:delete_environment:Function] +# #endregion delete_environment -# [DEF:test_environment_connection:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Tests the connection to a Superset environment. -# @PRE: ID is provided. -# @POST: Returns success or error status. +# #region test_environment_connection [C:2] [TYPE Function] +# @BRIEF Tests the connection to a Superset environment. +# @PRE ID is provided. +# @POST Returns success or error status. # @PARAM: id (str) - The ID of the environment to test. # @RETURN: dict - Success message or error. @router.post("/environments/{id}/test") @@ -344,15 +328,14 @@ async def test_environment_connection( return {"status": "error", "message": str(e)} -# [/DEF:test_environment_connection:Function] +# #endregion test_environment_connection -# [DEF:get_logging_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Retrieves current logging configuration. -# @PRE: Config manager is available. -# @POST: Returns logging configuration. -# @RETURN: LoggingConfigResponse - The current logging config. +# #region get_logging_config [C:2] [TYPE Function] +# @BRIEF Retrieves current logging configuration. +# @PRE Config manager is available. +# @POST Returns logging configuration. +# @RETURN LoggingConfigResponse - The current logging config. @router.get("/logging", response_model=LoggingConfigResponse) async def get_logging_config( config_manager: ConfigManager = Depends(get_config_manager), @@ -367,14 +350,13 @@ async def get_logging_config( ) -# [/DEF:get_logging_config:Function] +# #endregion get_logging_config -# [DEF:update_logging_config:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Updates logging configuration. -# @PRE: New logging config is provided. -# @POST: Logging configuration is updated and saved. +# #region update_logging_config [C:2] [TYPE Function] +# @BRIEF Updates logging configuration. +# @PRE New logging config is provided. +# @POST Logging configuration is updated and saved. # @PARAM: config (LoggingConfig) - The new logging configuration. # @RETURN: LoggingConfigResponse - The updated logging config. @router.patch("/logging", response_model=LoggingConfigResponse) @@ -398,12 +380,11 @@ async def update_logging_config( ) -# [/DEF:update_logging_config:Function] +# #endregion update_logging_config -# [DEF:ConsolidatedSettingsResponse:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Response model for consolidated application settings. +# #region ConsolidatedSettingsResponse [C:1] [TYPE Class] +# @BRIEF Response model for consolidated application settings. class ConsolidatedSettingsResponse(BaseModel): environments: List[dict] connections: List[dict] @@ -415,22 +396,21 @@ class ConsolidatedSettingsResponse(BaseModel): features: dict = {} -# [/DEF:ConsolidatedSettingsResponse:Class] +# #endregion ConsolidatedSettingsResponse -# [DEF:get_consolidated_settings:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Retrieves all settings categories in a single call. -# @PRE: Config manager is available and the caller holds admin settings read permission. -# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response. -# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it. -# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse] -# @RELATION: [DEPENDS_ON] ->[ConfigManager] -# @RELATION: [DEPENDS_ON] ->[LLMProviderService] -# @RELATION: [DEPENDS_ON] ->[AppConfigRecord] -# @RELATION: [DEPENDS_ON] ->[SessionLocal] -# @RELATION: [DEPENDS_ON] ->[has_permission] -# @RELATION: [DEPENDS_ON] ->[normalize_llm_settings] +# #region get_consolidated_settings [C:4] [TYPE Function] +# @BRIEF Retrieves all settings categories in a single call. +# @PRE Config manager is available and the caller holds admin settings read permission. +# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response. +# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it. +# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse] +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [AppConfigRecord] +# @RELATION DEPENDS_ON -> [SessionLocal] +# @RELATION DEPENDS_ON -> [has_permission] +# @RELATION DEPENDS_ON -> [normalize_llm_settings] @router.get("/consolidated", response_model=ConsolidatedSettingsResponse) async def get_consolidated_settings( config_manager: ConfigManager = Depends(get_config_manager), @@ -494,14 +474,13 @@ async def get_consolidated_settings( return response_payload -# [/DEF:get_consolidated_settings:Function] +# #endregion get_consolidated_settings -# [DEF:update_consolidated_settings:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Bulk update application settings from the consolidated view. -# @PRE: User has admin permissions, config is valid. -# @POST: Settings are updated and saved via ConfigManager. +# #region update_consolidated_settings [C:2] [TYPE Function] +# @BRIEF Bulk update application settings from the consolidated view. +# @PRE User has admin permissions, config is valid. +# @POST Settings are updated and saved via ConfigManager. @router.patch("/consolidated") async def update_consolidated_settings( settings_patch: dict, @@ -549,13 +528,12 @@ async def update_consolidated_settings( return {"status": "success", "message": "Settings updated"} -# [/DEF:update_consolidated_settings:Function] +# #endregion update_consolidated_settings -# [DEF:get_validation_policies:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Lists all validation policies. -# @RETURN: List[ValidationPolicyResponse] - List of policies. +# #region get_validation_policies [C:2] [TYPE Function] +# @BRIEF Lists all validation policies. +# @RETURN List[ValidationPolicyResponse] - List of policies. @router.get("/automation/policies", response_model=List[ValidationPolicyResponse]) async def get_validation_policies( db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "READ")) @@ -564,12 +542,11 @@ async def get_validation_policies( return db.query(ValidationPolicy).all() -# [/DEF:get_validation_policies:Function] +# #endregion get_validation_policies -# [DEF:create_validation_policy:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Creates a new validation policy. +# #region create_validation_policy [C:2] [TYPE Function] +# @BRIEF Creates a new validation policy. # @PARAM: policy (ValidationPolicyCreate) - The policy data. # @RETURN: ValidationPolicyResponse - The created policy. @router.post("/automation/policies", response_model=ValidationPolicyResponse) @@ -586,12 +563,11 @@ async def create_validation_policy( return db_policy -# [/DEF:create_validation_policy:Function] +# #endregion create_validation_policy -# [DEF:update_validation_policy:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Updates an existing validation policy. +# #region update_validation_policy [C:2] [TYPE Function] +# @BRIEF Updates an existing validation policy. # @PARAM: id (str) - The ID of the policy to update. # @PARAM: policy (ValidationPolicyUpdate) - The updated policy data. # @RETURN: ValidationPolicyResponse - The updated policy. @@ -616,12 +592,11 @@ async def update_validation_policy( return db_policy -# [/DEF:update_validation_policy:Function] +# #endregion update_validation_policy -# [DEF:delete_validation_policy:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Deletes a validation policy. +# #region delete_validation_policy [C:2] [TYPE Function] +# @BRIEF Deletes a validation policy. # @PARAM: id (str) - The ID of the policy to delete. @router.delete("/automation/policies/{id}") async def delete_validation_policy( @@ -639,6 +614,6 @@ async def delete_validation_policy( return {"message": "Policy deleted"} -# [/DEF:delete_validation_policy:Function] +# #endregion delete_validation_policy -# [/DEF:SettingsRouter:Module] +# #endregion SettingsRouter diff --git a/backend/src/api/routes/storage.py b/backend/src/api/routes/storage.py index 6acdc430..72e9efed 100644 --- a/backend/src/api/routes/storage.py +++ b/backend/src/api/routes/storage.py @@ -1,13 +1,11 @@ -# [DEF:storage_routes:Module] +# #region storage_routes [C:3] [TYPE Module] [SEMANTICS storage, files, upload, download, backup, repository] +# @BRIEF API endpoints for file storage management (backups and repositories). +# @LAYER API +# @INVARIANT All paths must be validated against path traversal. +# @RELATION DEPENDS_ON -> [StorageModels] +# @RELATION DEPENDS_ON -> [StoragePlugin] # -# @COMPLEXITY: 3 -# @SEMANTICS: storage, files, upload, download, backup, repository -# @PURPOSE: API endpoints for file storage management (backups and repositories). -# @LAYER: API -# @RELATION: DEPENDS_ON -> [StorageModels] -# @RELATION: DEPENDS_ON -> [StoragePlugin] # -# @INVARIANT: All paths must be validated against path traversal. # [SECTION: IMPORTS] from pathlib import Path @@ -22,12 +20,11 @@ from ...core.logger import belief_scope router = APIRouter(tags=["storage"]) -# [DEF:list_files:Function] -# @COMPLEXITY: 3 -# @PURPOSE: List all files and directories in the storage system. +# #region list_files [C:3] [TYPE Function] +# @BRIEF List all files and directories in the storage system. +# @PRE None. +# @POST Returns a list of StoredFile objects. # -# @PRE: None. -# @POST: Returns a list of StoredFile objects. # # @PARAM: category (Optional[FileCategory]) - Filter by category. # @PARAM: path (Optional[str]) - Subpath within the category. @@ -46,15 +43,14 @@ async def list_files( if not storage_plugin: raise HTTPException(status_code=500, detail="Storage plugin not loaded") return storage_plugin.list_files(category, path, recursive) -# [/DEF:list_files:Function] +# #endregion list_files -# [DEF:upload_file:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Upload a file to the storage system. +# #region upload_file [C:3] [TYPE Function] +# @BRIEF Upload a file to the storage system. +# @PRE category must be a valid FileCategory. +# @PRE file must be a valid UploadFile. +# @POST Returns the StoredFile object of the uploaded file. # -# @PRE: category must be a valid FileCategory. -# @PRE: file must be a valid UploadFile. -# @POST: Returns the StoredFile object of the uploaded file. # # @PARAM: category (FileCategory) - Target category. # @PARAM: path (Optional[str]) - Target subpath. @@ -80,14 +76,13 @@ async def upload_file( return await storage_plugin.save_file(file, category, path) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) -# [/DEF:upload_file:Function] +# #endregion upload_file -# [DEF:delete_file:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Delete a specific file or directory. +# #region delete_file [C:3] [TYPE Function] +# @BRIEF Delete a specific file or directory. +# @PRE category must be a valid FileCategory. +# @POST Item is removed from storage. # -# @PRE: category must be a valid FileCategory. -# @POST: Item is removed from storage. # # @PARAM: category (FileCategory) - File category. # @PARAM: path (str) - Relative path of the item. @@ -113,14 +108,13 @@ async def delete_file( raise HTTPException(status_code=404, detail="File not found") except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) -# [/DEF:delete_file:Function] +# #endregion delete_file -# [DEF:download_file:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Retrieve a file for download. +# #region download_file [C:3] [TYPE Function] +# @BRIEF Retrieve a file for download. +# @PRE category must be a valid FileCategory. +# @POST Returns a FileResponse. # -# @PRE: category must be a valid FileCategory. -# @POST: Returns a FileResponse. # # @PARAM: category (FileCategory) - File category. # @PARAM: path (str) - Relative path of the file. @@ -146,14 +140,13 @@ async def download_file( raise HTTPException(status_code=404, detail="File not found") except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) -# [/DEF:download_file:Function] +# #endregion download_file -# [DEF:get_file_by_path:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Retrieve a file by validated absolute/relative path under storage root. +# #region get_file_by_path [C:3] [TYPE Function] +# @BRIEF Retrieve a file by validated absolute/relative path under storage root. +# @PRE path must resolve under configured storage root. +# @POST Returns a FileResponse for existing files. # -# @PRE: path must resolve under configured storage root. -# @POST: Returns a FileResponse for existing files. # # @PARAM: path (str) - Absolute or storage-root-relative file path. # @RETURN: FileResponse - The file content. @@ -188,6 +181,6 @@ async def get_file_by_path( raise HTTPException(status_code=404, detail="File not found") return FileResponse(path=str(abs_path), filename=abs_path.name) -# [/DEF:get_file_by_path:Function] +# #endregion get_file_by_path -# [/DEF:storage_routes:Module] +# #endregion storage_routes diff --git a/backend/src/api/routes/tasks.py b/backend/src/api/routes/tasks.py index 46e460ee..5d686453 100755 --- a/backend/src/api/routes/tasks.py +++ b/backend/src/api/routes/tasks.py @@ -1,11 +1,9 @@ -# [DEF:TasksRouter:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: api, router, tasks, create, list, get, logs -# @PURPOSE: Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks. -# @LAYER: UI (API) -# @RELATION: DEPENDS_ON -> [TaskManager] -# @RELATION: DEPENDS_ON -> [ConfigManager] -# @RELATION: DEPENDS_ON -> [LLMProviderService] +# #region TasksRouter [C:3] [TYPE Module] [SEMANTICS api, router, tasks, create, list, get, logs] +# @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks. +# @LAYER UI (API) +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [LLMProviderService] # [SECTION: IMPORTS] from typing import List, Dict, Any, Optional @@ -51,9 +49,8 @@ class ResumeTaskRequest(BaseModel): passwords: Dict[str, str] -# [DEF:create_task:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Create and start a new task for a given plugin. +# #region create_task [C:3] [TYPE Function] +# @BRIEF Create and start a new task for a given plugin. # @PARAM: request (CreateTaskRequest) - The request body containing plugin_id and params. # @PARAM: task_manager (TaskManager) - The task manager instance. # @PRE: plugin_id must exist and params must be valid for that plugin. @@ -131,12 +128,11 @@ async def create_task( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -# [/DEF:create_task:Function] +# #endregion create_task -# [DEF:list_tasks:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Retrieve a list of tasks with pagination and optional status filter. +# #region list_tasks [C:2] [TYPE Function] +# @BRIEF Retrieve a list of tasks with pagination and optional status filter. # @PARAM: limit (int) - Maximum number of tasks to return. # @PARAM: offset (int) - Number of tasks to skip. # @PARAM: status (Optional[TaskStatus]) - Filter by task status. @@ -182,12 +178,11 @@ async def list_tasks( ) -# [/DEF:list_tasks:Function] +# #endregion list_tasks -# [DEF:get_task:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Retrieve the details of a specific task. +# #region get_task [C:2] [TYPE Function] +# @BRIEF Retrieve the details of a specific task. # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: task_manager (TaskManager) - The task manager instance. # @PRE: task_id must exist. @@ -209,12 +204,11 @@ async def get_task( return task -# [/DEF:get_task:Function] +# #endregion get_task -# [DEF:get_task_logs:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Retrieve logs for a specific task with optional filtering. +# #region get_task_logs [C:5] [TYPE Function] +# @BRIEF Retrieve logs for a specific task with optional filtering. # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: level (Optional[str]) - Filter by log level (DEBUG, INFO, WARNING, ERROR). # @PARAM: source (Optional[str]) - Filter by source component. @@ -265,12 +259,11 @@ async def get_task_logs( return task_manager.get_task_logs(task_id, log_filter) -# [/DEF:get_task_logs:Function] +# #endregion get_task_logs -# [DEF:get_task_log_stats:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get statistics about logs for a task (counts by level and source). +# #region get_task_log_stats [C:2] [TYPE Function] +# @BRIEF Get statistics about logs for a task (counts by level and source). # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: task_manager (TaskManager) - The task manager instance. # @PRE: task_id must exist. @@ -314,12 +307,11 @@ async def get_task_log_stats( ) -# [/DEF:get_task_log_stats:Function] +# #endregion get_task_log_stats -# [DEF:get_task_log_sources:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Get unique sources for a task's logs. +# #region get_task_log_sources [C:2] [TYPE Function] +# @BRIEF Get unique sources for a task's logs. # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: task_manager (TaskManager) - The task manager instance. # @PRE: task_id must exist. @@ -340,12 +332,11 @@ async def get_task_log_sources( return task_manager.get_task_log_sources(task_id) -# [/DEF:get_task_log_sources:Function] +# #endregion get_task_log_sources -# [DEF:resolve_task:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve a task that is awaiting mapping. +# #region resolve_task [C:2] [TYPE Function] +# @BRIEF Resolve a task that is awaiting mapping. # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: request (ResolveTaskRequest) - The resolution parameters. # @PARAM: task_manager (TaskManager) - The task manager instance. @@ -368,12 +359,11 @@ async def resolve_task( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -# [/DEF:resolve_task:Function] +# #endregion resolve_task -# [DEF:resume_task:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resume a task that is awaiting input (e.g., passwords). +# #region resume_task [C:2] [TYPE Function] +# @BRIEF Resume a task that is awaiting input (e.g., passwords). # @PARAM: task_id (str) - The unique identifier of the task. # @PARAM: request (ResumeTaskRequest) - The input (passwords). # @PARAM: task_manager (TaskManager) - The task manager instance. @@ -396,12 +386,11 @@ async def resume_task( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -# [/DEF:resume_task:Function] +# #endregion resume_task -# [DEF:clear_tasks:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Clear tasks matching the status filter. +# #region clear_tasks [C:2] [TYPE Function] +# @BRIEF Clear tasks matching the status filter. # @PARAM: status (Optional[TaskStatus]) - Filter by task status. # @PARAM: task_manager (TaskManager) - The task manager instance. # @PRE: task_manager is available. @@ -418,6 +407,6 @@ async def clear_tasks( return -# [/DEF:clear_tasks:Function] +# #endregion clear_tasks -# [/DEF:TasksRouter:Module] +# #endregion TasksRouter diff --git a/backend/src/app.py b/backend/src/app.py index a916ec53..99a3dbd9 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,16 +1,14 @@ -# [DEF:AppModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: app, main, entrypoint, fastapi -# @PURPOSE: The main entry point for the FastAPI application. It initializes the app, configures CORS, sets up dependencies, includes API routers, and defines the WebSocket endpoint for log streaming. -# @LAYER: UI (API) -# @RELATION: [DEPENDS_ON] ->[AppDependencies] -# @RELATION: [DEPENDS_ON] ->[ApiRoutesModule] -# @INVARIANT: Only one FastAPI app instance exists per process. -# @INVARIANT: All WebSocket connections must be properly cleaned up on disconnect. -# @PRE: Python environment and dependencies installed; configuration database available. -# @POST: FastAPI app instance is created, middleware configured, and routes registered. -# @SIDE_EFFECT: Starts background scheduler and binds network ports for HTTP/WS traffic. -# @DATA_CONTRACT: [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream] +# #region AppModule [C:5] [TYPE Module] [SEMANTICS app, main, entrypoint, fastapi] +# @BRIEF The main entry point for the FastAPI application. It initializes the app, configures CORS, sets up dependencies, includes API routers, and defines the WebSocket endpoint for log streaming. +# @LAYER UI (API) +# @PRE Python environment and dependencies installed; configuration database available. +# @POST FastAPI app instance is created, middleware configured, and routes registered. +# @SIDE_EFFECT Starts background scheduler and binds network ports for HTTP/WS traffic. +# @DATA_CONTRACT [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream] +# @INVARIANT Only one FastAPI app instance exists per process. +# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect. +# @RELATION DEPENDS_ON -> [AppDependencies] +# @RELATION DEPENDS_ON -> [ApiRoutesModule] import os from pathlib import Path @@ -60,24 +58,21 @@ from .api.routes import ( ) from .api import auth -# [DEF:FastAPI_App:Global] -# @COMPLEXITY: 3 -# @SEMANTICS: app, fastapi, instance, route-registry -# @PURPOSE: Canonical FastAPI application instance for route, middleware, and websocket registration. -# @RELATION: DEPENDS_ON -> [ApiRoutesModule] -# @RELATION: BINDS_TO -> [API_Routes] +# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry] +# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration. +# @RELATION DEPENDS_ON -> [ApiRoutesModule] +# @RELATION BINDS_TO -> [API_Routes] app = FastAPI( title="Superset Tools API", description="API for managing Superset automation tools and plugins.", version="1.0.0", ) -# [/DEF:FastAPI_App:Global] +# #endregion FastAPI_App -# [DEF:ensure_initial_admin_user:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Ensures initial admin user exists when bootstrap env flags are enabled. -# @RELATION: DEPENDS_ON -> AuthRepository +# #region ensure_initial_admin_user [C:3] [TYPE Function] +# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled. +# @RELATION DEPENDS_ON -> [AuthRepository] def ensure_initial_admin_user() -> None: raw_flag = os.getenv("INITIAL_ADMIN_CREATE", "false").strip().lower() if raw_flag not in {"1", "true", "yes", "on"}: @@ -127,15 +122,14 @@ def ensure_initial_admin_user() -> None: db.close() -# [/DEF:ensure_initial_admin_user:Function] +# #endregion ensure_initial_admin_user -# [DEF:startup_event:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Handles application startup tasks, such as starting the scheduler. -# @RELATION: [CALLS] ->[AppDependencies] -# @PRE: None. -# @POST: Scheduler is started. +# #region startup_event [C:3] [TYPE Function] +# @BRIEF Handles application startup tasks, such as starting the scheduler. +# @PRE None. +# @POST Scheduler is started. +# @RELATION CALLS -> [AppDependencies] # Startup event @app.on_event("startup") async def startup_event(): @@ -147,15 +141,14 @@ async def startup_event(): scheduler.start() -# [/DEF:startup_event:Function] +# #endregion startup_event -# [DEF:shutdown_event:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Handles application shutdown tasks, such as stopping the scheduler. -# @RELATION: [CALLS] ->[AppDependencies] -# @PRE: None. -# @POST: Scheduler is stopped. +# #region shutdown_event [C:3] [TYPE Function] +# @BRIEF Handles application shutdown tasks, such as stopping the scheduler. +# @PRE None. +# @POST Scheduler is stopped. +# @RELATION CALLS -> [AppDependencies] # Shutdown event @app.on_event("shutdown") async def shutdown_event(): @@ -164,10 +157,10 @@ async def shutdown_event(): scheduler.stop() -# [/DEF:shutdown_event:Function] +# #endregion shutdown_event -# [DEF:app_middleware:Block] -# @PURPOSE: Configure application-wide middleware (Session, CORS, TraceContext). +# #region app_middleware [TYPE Block] +# @BRIEF Configure application-wide middleware (Session, CORS, TraceContext). # Configure Session Middleware (required by Authlib for OAuth2 flow) from .core.auth.config import auth_config @@ -186,14 +179,13 @@ app.add_middleware( from .core.middleware.trace import TraceContextMiddleware app.add_middleware(TraceContextMiddleware) -# [/DEF:app_middleware:Block] +# #endregion app_middleware -# [DEF:network_error_handler:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Global exception handler for NetworkError. -# @PRE: request is a FastAPI Request object. -# @POST: Returns 503 HTTP Exception. +# #region network_error_handler [C:1] [TYPE Function] +# @BRIEF Global exception handler for NetworkError. +# @PRE request is a FastAPI Request object. +# @POST Returns 503 HTTP Exception. # @PARAM: request (Request) - The incoming request object. # @PARAM: exc (NetworkError) - The exception instance. eh_log = MarkerLogger("ExceptionHandler") @@ -209,15 +201,14 @@ async def network_error_handler(request: Request, exc: NetworkError): ) -# [/DEF:network_error_handler:Function] +# #endregion network_error_handler -# [DEF:log_requests:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Middleware to log incoming HTTP requests and their response status. -# @RELATION: [DEPENDS_ON] ->[LoggerModule] -# @PRE: request is a FastAPI Request object. -# @POST: Logs request and response details. +# #region log_requests [C:3] [TYPE Function] +# @BRIEF Middleware to log incoming HTTP requests and their response status. +# @PRE request is a FastAPI Request object. +# @POST Logs request and response details. +# @RELATION DEPENDS_ON -> [LoggerModule] # @PARAM: request (Request) - The incoming request object. # @PARAM: call_next (Callable) - The next middleware or route handler. @app.middleware("http") @@ -244,22 +235,21 @@ async def log_requests(request: Request, call_next): ) -# [/DEF:log_requests:Function] +# #endregion log_requests -# [DEF:API_Routes:Block] -# @COMPLEXITY: 3 -# @PURPOSE: Register all FastAPI route groups exposed by the application entrypoint. -# @RELATION: DEPENDS_ON -> [FastAPI_App] -# @RELATION: DEPENDS_ON -> [Route_Group_Contracts] -# @RELATION: DEPENDS_ON -> [AuthApi] -# @RELATION: DEPENDS_ON -> [AdminApi] -# @RELATION: DEPENDS_ON -> [PluginsRouter] -# @RELATION: DEPENDS_ON -> [TasksRouter] -# @RELATION: DEPENDS_ON -> [SettingsRouter] -# @RELATION: DEPENDS_ON -> [ConnectionsRouter] -# @RELATION: DEPENDS_ON -> [ReportsRouter] -# @RELATION: DEPENDS_ON -> [LlmRoutes] -# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api] +# #region API_Routes [C:3] [TYPE Block] +# @BRIEF Register all FastAPI route groups exposed by the application entrypoint. +# @RELATION DEPENDS_ON -> [FastAPI_App] +# @RELATION DEPENDS_ON -> [Route_Group_Contracts] +# @RELATION DEPENDS_ON -> [AuthApi] +# @RELATION DEPENDS_ON -> [AdminApi] +# @RELATION DEPENDS_ON -> [PluginsRouter] +# @RELATION DEPENDS_ON -> [TasksRouter] +# @RELATION DEPENDS_ON -> [SettingsRouter] +# @RELATION DEPENDS_ON -> [ConnectionsRouter] +# @RELATION DEPENDS_ON -> [ReportsRouter] +# @RELATION DEPENDS_ON -> [LlmRoutes] +# @RELATION DEPENDS_ON -> [CleanReleaseV2Api] # Include API routes app.include_router(auth.router) app.include_router(admin.router) @@ -285,27 +275,24 @@ app.include_router(profile.router) app.include_router(dataset_review.router) app.include_router(health.router) app.include_router(translate.router) -# [/DEF:API_Routes:Block] +# #endregion API_Routes -# [DEF:api.include_routers:Action] -# @COMPLEXITY: 1 -# @PURPOSE: Registers all API routers with the FastAPI application. -# @LAYER: API -# @SEMANTICS: routes, registration, api -# [/DEF:api.include_routers:Action] +# #region api.include_routers [C:1] [TYPE Action] [SEMANTICS routes, registration, api] +# @BRIEF Registers all API routers with the FastAPI application. +# @LAYER API +# #endregion api.include_routers -# [DEF:websocket_endpoint:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering. -# @RELATION: [CALLS] ->[TaskManagerPackage] -# @RELATION: [DEPENDS_ON] ->[LoggerModule] -# @PRE: task_id must be a valid task ID. -# @POST: WebSocket connection is managed and logs are streamed until disconnect. -# @SIDE_EFFECT: Subscribes to TaskManager log queue and broadcasts messages over network. -# @DATA_CONTRACT: [task_id: str, source: str, level: str] -> [JSON log entry objects] -# @INVARIANT: Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects. +# #region websocket_endpoint [C:5] [TYPE Function] +# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering. +# @PRE task_id must be a valid task ID. +# @POST WebSocket connection is managed and logs are streamed until disconnect. +# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network. +# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects] +# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects. +# @RELATION CALLS -> [TaskManagerPackage] +# @RELATION DEPENDS_ON -> [LoggerModule] # @UX_STATE: Connecting -> Streaming -> (Disconnected) # # @TEST_CONTRACT: WebSocketLogStreamApi -> @@ -457,23 +444,20 @@ async def websocket_endpoint( ) -# [/DEF:websocket_endpoint:Function] +# #endregion websocket_endpoint -# [DEF:StaticFiles:Mount] -# @COMPLEXITY: 1 -# @SEMANTICS: static, frontend, spa -# @PURPOSE: Mounts the frontend build directory to serve static assets. +# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa] +# @BRIEF Mounts the frontend build directory to serve static assets. frontend_path = project_root / "frontend" / "build" if frontend_path.exists(): app.mount( "/_app", StaticFiles(directory=str(frontend_path / "_app")), name="static" ) - # [DEF:serve_spa:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Serves the SPA frontend for any path not matched by API routes. - # @PRE: frontend_path exists. - # @POST: Returns the requested file or index.html. + # #region serve_spa [C:1] [TYPE Function] + # @BRIEF Serves the SPA frontend for any path not matched by API routes. + # @PRE frontend_path exists. + # @POST Returns the requested file or index.html. @app.get("/{file_path:path}", include_in_schema=False) async def serve_spa(file_path: str): with belief_scope("serve_spa"): @@ -495,13 +479,12 @@ if frontend_path.exists(): return FileResponse(str(full_path)) return FileResponse(str(frontend_path / "index.html")) - # [/DEF:serve_spa:Function] + # #endregion serve_spa else: - # [DEF:read_root:Function] - # @COMPLEXITY: 1 - # @PURPOSE: A simple root endpoint to confirm that the API is running when frontend is missing. - # @PRE: None. - # @POST: Returns a JSON message indicating API status. + # #region read_root [C:1] [TYPE Function] + # @BRIEF A simple root endpoint to confirm that the API is running when frontend is missing. + # @PRE None. + # @POST Returns a JSON message indicating API status. @app.get("/") async def read_root(): with belief_scope("read_root"): @@ -509,6 +492,5 @@ else: "message": "Superset Tools API is running (Frontend build not found)" } - # [/DEF:read_root:Function] -# [/DEF:StaticFiles:Mount] -# [/DEF:AppModule:Module] + # #endregion read_root +# #endregion StaticFiles diff --git a/backend/src/core/__init__.py b/backend/src/core/__init__.py index d50a06dc..f383a5cf 100644 --- a/backend/src/core/__init__.py +++ b/backend/src/core/__init__.py @@ -1,3 +1,3 @@ -# [DEF:src.core:Package] -# @PURPOSE: Backend core services and infrastructure package root. -# [/DEF:src.core:Package] +# #region src.core [TYPE Package] +# @BRIEF Backend core services and infrastructure package root. +# #endregion src.core diff --git a/backend/src/core/__tests__/test_config_manager_compat.py b/backend/src/core/__tests__/test_config_manager_compat.py index 74f48692..9dd908f6 100644 --- a/backend/src/core/__tests__/test_config_manager_compat.py +++ b/backend/src/core/__tests__/test_config_manager_compat.py @@ -1,9 +1,7 @@ -# [DEF:TestConfigManagerCompat:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: config-manager, compatibility, payload, tests -# @PURPOSE: Verifies ConfigManager compatibility wrappers preserve legacy payload sections. -# @LAYER: Domain -# @RELATION: VERIFIES -> ConfigManager +# #region TestConfigManagerCompat [C:3] [TYPE Module] [SEMANTICS config-manager, compatibility, payload, tests] +# @BRIEF Verifies ConfigManager compatibility wrappers preserve legacy payload sections. +# @LAYER Domain +# @RELATION VERIFIES -> [ConfigManager] from types import SimpleNamespace @@ -11,9 +9,9 @@ from src.core.config_manager import ConfigManager from src.core.config_models import AppConfig, Environment, GlobalSettings -# [DEF:test_get_payload_preserves_legacy_sections:Function] -# @RELATION: BINDS_TO -> TestConfigManagerCompat -# @PURPOSE: Ensure get_payload merges typed config into raw payload without dropping legacy sections. +# #region test_get_payload_preserves_legacy_sections [TYPE Function] +# @BRIEF Ensure get_payload merges typed config into raw payload without dropping legacy sections. +# @RELATION BINDS_TO -> [TestConfigManagerCompat] def test_get_payload_preserves_legacy_sections(): manager = ConfigManager.__new__(ConfigManager) manager.raw_payload = {"notifications": {"smtp": {"host": "mail.local"}}} @@ -25,7 +23,7 @@ def test_get_payload_preserves_legacy_sections(): assert payload["notifications"]["smtp"]["host"] == "mail.local" -# [/DEF:test_get_payload_preserves_legacy_sections:Function] +# #endregion test_get_payload_preserves_legacy_sections # [DEF:test_save_config_accepts_raw_payload_and_keeps_extras:Function] @@ -56,9 +54,9 @@ def test_save_config_accepts_raw_payload_and_keeps_extras(monkeypatch): assert persisted["payload"]["notifications"]["telegram"]["bot_token"] == "secret" -# [DEF:test_save_config_syncs_environment_records_for_fk_backed_flows:Function] -# @RELATION: BINDS_TO -> TestConfigManagerCompat -# @PURPOSE: Ensure saving config mirrors typed environments into relational records required by FK-backed session persistence. +# #region test_save_config_syncs_environment_records_for_fk_backed_flows [TYPE Function] +# @BRIEF Ensure saving config mirrors typed environments into relational records required by FK-backed session persistence. +# @RELATION BINDS_TO -> [TestConfigManagerCompat] def test_save_config_syncs_environment_records_for_fk_backed_flows(): manager = ConfigManager.__new__(ConfigManager) manager.raw_payload = {} @@ -73,22 +71,20 @@ def test_save_config_syncs_environment_records_for_fk_backed_flows(): credentials_id="legacy-user", ) - # [DEF:_FakeQuery:Class] - # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows] - # @COMPLEXITY: 1 - # @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests. - # @INVARIANT: all() always returns [existing_record]; no parameterization or filtering. + # #region _FakeQuery [C:1] [TYPE Class] + # @BRIEF Minimal query stub returning hardcoded existing environment record list for sync tests. + # @INVARIANT all() always returns [existing_record]; no parameterization or filtering. + # @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows] class _FakeQuery: def all(self): return [existing_record] - # [/DEF:_FakeQuery:Class] + # #endregion _FakeQuery - # [DEF:_FakeSession:Class] - # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows] - # @COMPLEXITY: 1 - # @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions. - # @INVARIANT: query() always returns _FakeQuery; no real DB interaction. + # #region _FakeSession [C:1] [TYPE Class] + # @BRIEF Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions. + # @INVARIANT query() always returns _FakeQuery; no real DB interaction. + # @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows] class _FakeSession: def query(self, model): return _FakeQuery() @@ -99,7 +95,7 @@ def test_save_config_syncs_environment_records_for_fk_backed_flows(): def delete(self, value): deleted_records.append(value) - # [/DEF:_FakeSession:Class] + # #endregion _FakeSession session = _FakeSession() config = AppConfig( @@ -125,12 +121,10 @@ def test_save_config_syncs_environment_records_for_fk_backed_flows(): assert deleted_records == [existing_record] -# [/DEF:test_save_config_syncs_environment_records_for_fk_backed_flows:Function] - - -# [DEF:test_load_config_syncs_environment_records_from_existing_db_payload:Function] -# @RELATION: BINDS_TO -> TestConfigManagerCompat -# @PURPOSE: Ensure loading an existing DB-backed config also mirrors environment rows required by FK-backed runtime flows. +# #endregion test_save_config_syncs_environment_records_for_fk_backed_flows +# #region test_load_config_syncs_environment_records_from_existing_db_payload [TYPE Function] +# @BRIEF Ensure loading an existing DB-backed config also mirrors environment rows required by FK-backed runtime flows. +# @RELATION BINDS_TO -> [TestConfigManagerCompat] def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypatch): manager = ConfigManager.__new__(ConfigManager) manager.config_path = None @@ -141,11 +135,10 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa closed = {"value": False} committed = {"value": False} - # [DEF:_FakeSession:Class] - # @RELATION: BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload] - # @COMPLEXITY: 1 - # @PURPOSE: Minimal session stub tracking commit/close signals for config load lifecycle assertions. - # @INVARIANT: No query or add semantics; only lifecycle signal tracking. + # #region _FakeSession [C:1] [TYPE Class] + # @BRIEF Minimal session stub tracking commit/close signals for config load lifecycle assertions. + # @INVARIANT No query or add semantics; only lifecycle signal tracking. + # @RELATION BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload] class _FakeSession: def commit(self): committed["value"] = True @@ -153,7 +146,7 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa def close(self): closed["value"] = True - # [/DEF:_FakeSession:Class] + # #endregion _FakeSession fake_session = _FakeSession() fake_record = SimpleNamespace( @@ -190,6 +183,5 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa assert closed["value"] is True -# [/DEF:test_load_config_syncs_environment_records_from_existing_db_payload:Function] - -# [/DEF:TestConfigManagerCompat:Module] +# #endregion test_load_config_syncs_environment_records_from_existing_db_payload +# #endregion TestConfigManagerCompat diff --git a/backend/src/core/__tests__/test_native_filters.py b/backend/src/core/__tests__/test_native_filters.py index ca824f44..d22a1704 100644 --- a/backend/src/core/__tests__/test_native_filters.py +++ b/backend/src/core/__tests__/test_native_filters.py @@ -1,11 +1,9 @@ -# [DEF:NativeFilterExtractionTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, superset, native, filters, permalink, filter_state -# @PURPOSE: Verify native filter extraction from permalinks and native_filters_key URLs. -# @LAYER: Domain -# @RELATION: [BINDS_TO] ->[SupersetClient] -# @RELATION: [BINDS_TO] ->[AsyncSupersetClient] -# @RELATION: [BINDS_TO] ->[FilterState, ParsedNativeFilters, ExtraFormDataMerge] +# #region NativeFilterExtractionTests [C:3] [TYPE Module] [SEMANTICS tests, superset, native, filters, permalink, filter_state] +# @BRIEF Verify native filter extraction from permalinks and native_filters_key URLs. +# @LAYER Domain +# @RELATION BINDS_TO -> [SupersetClient] +# @RELATION BINDS_TO -> [AsyncSupersetClient] +# @RELATION BINDS_TO -> [FilterState, ParsedNativeFilters, ExtraFormDataMerge] import json from unittest.mock import MagicMock @@ -28,8 +26,8 @@ from src.models.filter_state import ( ) -# [DEF:_make_environment:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests +# #region _make_environment [TYPE Function] +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def _make_environment() -> Environment: return Environment( id="env-1", @@ -40,12 +38,12 @@ def _make_environment() -> Environment: ) -# [/DEF:_make_environment:Function] +# #endregion _make_environment -# [DEF:test_extract_native_filters_from_permalink:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Extract native filters from a permalink key. +# #region test_extract_native_filters_from_permalink [TYPE Function] +# @BRIEF Extract native filters from a permalink key. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_native_filters_from_permalink(): client = SupersetClient(_make_environment()) client.get_dashboard_permalink_state = MagicMock( @@ -89,12 +87,12 @@ def test_extract_native_filters_from_permalink(): assert result["anchor"] == "SECTION1" -# [/DEF:test_extract_native_filters_from_permalink] +# #endregion test_extract_native_filters_from_permalink -# [DEF:test_extract_native_filters_from_permalink_direct_response:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Handle permalink response without result wrapper. +# #region test_extract_native_filters_from_permalink_direct_response [TYPE Function] +# @BRIEF Handle permalink response without result wrapper. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_native_filters_from_permalink_direct_response(): client = SupersetClient(_make_environment()) client.get_dashboard_permalink_state = MagicMock( @@ -117,12 +115,12 @@ def test_extract_native_filters_from_permalink_direct_response(): assert "filter_1" in result["dataMask"] -# [/DEF:test_extract_native_filters_from_permalink_direct_response] +# #endregion test_extract_native_filters_from_permalink_direct_response -# [DEF:test_extract_native_filters_from_key:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Extract native filters from a native_filters_key. +# #region test_extract_native_filters_from_key [TYPE Function] +# @BRIEF Extract native filters from a native_filters_key. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_native_filters_from_key(): client = SupersetClient(_make_environment()) client.get_native_filter_state = MagicMock( @@ -156,12 +154,12 @@ def test_extract_native_filters_from_key(): ] == ["EMEA"] -# [/DEF:test_extract_native_filters_from_key] +# #endregion test_extract_native_filters_from_key -# [DEF:test_extract_native_filters_from_key_single_filter:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Handle single filter format in native filter state. +# #region test_extract_native_filters_from_key_single_filter [TYPE Function] +# @BRIEF Handle single filter format in native filter state. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_native_filters_from_key_single_filter(): client = SupersetClient(_make_environment()) client.get_native_filter_state = MagicMock( @@ -190,12 +188,12 @@ def test_extract_native_filters_from_key_single_filter(): ) -# [/DEF:test_extract_native_filters_from_key_single_filter] +# #endregion test_extract_native_filters_from_key_single_filter -# [DEF:test_extract_native_filters_from_key_dict_value:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Handle filter state value as dict instead of JSON string. +# #region test_extract_native_filters_from_key_dict_value [TYPE Function] +# @BRIEF Handle filter state value as dict instead of JSON string. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_native_filters_from_key_dict_value(): client = SupersetClient(_make_environment()) client.get_native_filter_state = MagicMock( @@ -217,12 +215,12 @@ def test_extract_native_filters_from_key_dict_value(): assert "filter_id" in result["dataMask"] -# [/DEF:test_extract_native_filters_from_key_dict_value] +# #endregion test_extract_native_filters_from_key_dict_value -# [DEF:test_parse_dashboard_url_for_filters_permalink:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Parse permalink URL format. +# #region test_parse_dashboard_url_for_filters_permalink [TYPE Function] +# @BRIEF Parse permalink URL format. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_permalink(): client = SupersetClient(_make_environment()) client.extract_native_filters_from_permalink = MagicMock( @@ -237,12 +235,12 @@ def test_parse_dashboard_url_for_filters_permalink(): assert result["filters"]["dataMask"]["f1"] == {} -# [/DEF:test_parse_dashboard_url_for_filters_permalink] +# #endregion test_parse_dashboard_url_for_filters_permalink -# [DEF:test_parse_dashboard_url_for_filters_native_key:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Parse native_filters_key URL format with numeric dashboard ID. +# #region test_parse_dashboard_url_for_filters_native_key [TYPE Function] +# @BRIEF Parse native_filters_key URL format with numeric dashboard ID. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_native_key(): client = SupersetClient(_make_environment()) client.extract_native_filters_from_key = MagicMock( @@ -262,12 +260,12 @@ def test_parse_dashboard_url_for_filters_native_key(): assert result["filters"]["dataMask"]["f2"] == {} -# [/DEF:test_parse_dashboard_url_for_filters_native_key] +# #endregion test_parse_dashboard_url_for_filters_native_key -# [DEF:test_parse_dashboard_url_for_filters_native_key_slug:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID. +# #region test_parse_dashboard_url_for_filters_native_key_slug [TYPE Function] +# @BRIEF Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_native_key_slug(): client = SupersetClient(_make_environment()) # Simulate slug resolution: get_dashboard returns the dashboard with numeric ID @@ -295,12 +293,12 @@ def test_parse_dashboard_url_for_filters_native_key_slug(): client.extract_native_filters_from_key.assert_called_once_with(99, "abc123") -# [/DEF:test_parse_dashboard_url_for_filters_native_key_slug] +# #endregion test_parse_dashboard_url_for_filters_native_key_slug -# [DEF:test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Gracefully handle slug resolution failure for native_filters_key URL. +# #region test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails [TYPE Function] +# @BRIEF Gracefully handle slug resolution failure for native_filters_key URL. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails(): client = SupersetClient(_make_environment()) client.get_dashboard = MagicMock(side_effect=Exception("Not found")) @@ -313,12 +311,12 @@ def test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails(): assert result["dashboard_id"] is None -# [/DEF:test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails] +# #endregion test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails -# [DEF:test_parse_dashboard_url_for_filters_native_filters_direct:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Parse native_filters direct query param. +# #region test_parse_dashboard_url_for_filters_native_filters_direct [TYPE Function] +# @BRIEF Parse native_filters direct query param. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_native_filters_direct(): client = SupersetClient(_make_environment()) @@ -331,12 +329,12 @@ def test_parse_dashboard_url_for_filters_native_filters_direct(): assert "dataMask" in result["filters"] -# [/DEF:test_parse_dashboard_url_for_filters_native_filters_direct] +# #endregion test_parse_dashboard_url_for_filters_native_filters_direct -# [DEF:test_parse_dashboard_url_for_filters_no_filters:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Return empty result when no filters present. +# #region test_parse_dashboard_url_for_filters_no_filters [TYPE Function] +# @BRIEF Return empty result when no filters present. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parse_dashboard_url_for_filters_no_filters(): client = SupersetClient(_make_environment()) @@ -348,12 +346,12 @@ def test_parse_dashboard_url_for_filters_no_filters(): assert result["filters"] == {} -# [/DEF:test_parse_dashboard_url_for_filters_no_filters] +# #endregion test_parse_dashboard_url_for_filters_no_filters -# [DEF:test_extra_form_data_merge:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Test ExtraFormDataMerge correctly merges dictionaries. +# #region test_extra_form_data_merge [TYPE Function] +# @BRIEF Test ExtraFormDataMerge correctly merges dictionaries. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extra_form_data_merge(): merger = ExtraFormDataMerge() @@ -386,12 +384,12 @@ def test_extra_form_data_merge(): assert result["columns"] == ["col1", "col2"] -# [/DEF:test_extra_form_data_merge] +# #endregion test_extra_form_data_merge -# [DEF:test_filter_state_model:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Test FilterState Pydantic model. +# #region test_filter_state_model [TYPE Function] +# @BRIEF Test FilterState Pydantic model. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_filter_state_model(): state = FilterState( extraFormData={"filters": [{"col": "x", "op": "==", "val": "y"}]}, @@ -404,12 +402,12 @@ def test_filter_state_model(): assert state.ownState["selectedValues"] == ["y"] -# [/DEF:test_filter_state_model] +# #endregion test_filter_state_model -# [DEF:test_parsed_native_filters_model:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Test ParsedNativeFilters Pydantic model. +# #region test_parsed_native_filters_model [TYPE Function] +# @BRIEF Test ParsedNativeFilters Pydantic model. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parsed_native_filters_model(): filters = ParsedNativeFilters( dataMask={"f1": {"extraFormData": {}, "filterState": {}}}, @@ -423,12 +421,12 @@ def test_parsed_native_filters_model(): assert filters.filter_type == "permalink" -# [/DEF:test_parsed_native_filters_model] +# #endregion test_parsed_native_filters_model -# [DEF:test_parsed_native_filters_empty:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Test ParsedNativeFilters with no filters. +# #region test_parsed_native_filters_empty [TYPE Function] +# @BRIEF Test ParsedNativeFilters with no filters. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_parsed_native_filters_empty(): filters = ParsedNativeFilters() @@ -436,12 +434,12 @@ def test_parsed_native_filters_empty(): assert filters.get_filter_count() == 0 -# [/DEF:test_parsed_native_filters_empty] +# #endregion test_parsed_native_filters_empty -# [DEF:test_native_filter_data_mask_model:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Test NativeFilterDataMask model. +# #region test_native_filter_data_mask_model [TYPE Function] +# @BRIEF Test NativeFilterDataMask model. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_native_filter_data_mask_model(): data_mask = NativeFilterDataMask( filters={ @@ -457,12 +455,12 @@ def test_native_filter_data_mask_model(): assert data_mask.get_extra_form_data("nonexistent") == {} -# [/DEF:test_native_filter_data_mask_model] +# #endregion test_native_filter_data_mask_model -# [DEF:test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Reconcile raw native filter ids from state to canonical metadata filter names. +# #region test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names [TYPE Function] +# @BRIEF Reconcile raw native filter ids from state to canonical metadata filter names. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names(): client = MagicMock() client.get_dashboard.return_value = { @@ -524,12 +522,12 @@ def test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_n } -# [/DEF:test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names:Function] +# #endregion test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names -# [DEF:test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Collapse raw-id state entries and metadata entries into one canonical filter. +# #region test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter [TYPE Function] +# @BRIEF Collapse raw-id state entries and metadata entries into one canonical filter. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter(): client = MagicMock() client.get_dashboard.return_value = { @@ -582,12 +580,12 @@ def test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_o assert region_filter["recovery_status"] == "partial" -# [/DEF:test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter:Function] +# #endregion test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter -# [DEF:test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable. +# #region test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids [TYPE Function] +# @BRIEF Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids(): client = MagicMock() client.get_dashboard.return_value = { @@ -639,12 +637,12 @@ def test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids(): ) -# [/DEF:test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids:Function] +# #endregion test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids -# [DEF:test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation. +# #region test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview [TYPE Function] +# @BRIEF Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview(): extractor = SupersetContextExtractor(_make_environment(), client=MagicMock()) @@ -684,12 +682,12 @@ def test_extract_imported_filters_preserves_clause_level_native_filter_payload_f ] -# [/DEF:test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview:Function] +# #endregion test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview -# [DEF:test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values:Function] -# @RELATION: BINDS_TO -> NativeFilterExtractionTests -# @PURPOSE: Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata. +# #region test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values [TYPE Function] +# @BRIEF Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata. +# @RELATION BINDS_TO -> [NativeFilterExtractionTests] def test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values(): result = sanitize_imported_filter_for_assistant( { @@ -705,7 +703,7 @@ def test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values(): assert result["normalized_value"] == {"filter_clauses": []} -# [/DEF:test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values:Function] +# #endregion test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values -# [/DEF:NativeFilterExtractionTests:Module] +# #endregion NativeFilterExtractionTests diff --git a/backend/src/core/__tests__/test_superset_preview_pipeline.py b/backend/src/core/__tests__/test_superset_preview_pipeline.py index 96a33417..a3959add 100644 --- a/backend/src/core/__tests__/test_superset_preview_pipeline.py +++ b/backend/src/core/__tests__/test_superset_preview_pipeline.py @@ -1,9 +1,7 @@ -# [DEF:SupersetPreviewPipelineTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, superset, preview, chart_data, network, 404-mapping -# @PURPOSE: Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients. -# @LAYER: Domain -# @RELATION: [BINDS_TO] ->[AsyncNetworkModule] +# #region SupersetPreviewPipelineTests [C:3] [TYPE Module] [SEMANTICS tests, superset, preview, chart_data, network, 404-mapping] +# @BRIEF Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients. +# @LAYER Domain +# @RELATION BINDS_TO -> [AsyncNetworkModule] import json from unittest.mock import MagicMock @@ -18,8 +16,8 @@ from src.core.utils.async_network import AsyncAPIClient from src.core.utils.network import APIClient, DashboardNotFoundError, SupersetAPIError -# [DEF:_make_environment:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests +# #region _make_environment [TYPE Function] +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def _make_environment() -> Environment: return Environment( id="env-1", @@ -30,11 +28,11 @@ def _make_environment() -> Environment: ) -# [/DEF:_make_environment:Function] +# #endregion _make_environment -# [DEF:_make_requests_http_error:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests +# #region _make_requests_http_error [TYPE Function] +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def _make_requests_http_error( status_code: int, url: str ) -> requests.exceptions.HTTPError: @@ -47,11 +45,11 @@ def _make_requests_http_error( return requests.exceptions.HTTPError(response=response, request=request) -# [/DEF:_make_requests_http_error:Function] +# #endregion _make_requests_http_error -# [DEF:_make_httpx_status_error:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests +# #region _make_httpx_status_error [TYPE Function] +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def _make_httpx_status_error(status_code: int, url: str) -> httpx.HTTPStatusError: request = httpx.Request("GET", url) response = httpx.Response( @@ -60,12 +58,12 @@ def _make_httpx_status_error(status_code: int, url: str) -> httpx.HTTPStatusErro return httpx.HTTPStatusError("upstream error", request=request, response=response) -# [/DEF:_make_httpx_status_error:Function] +# #endregion _make_httpx_status_error -# [DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Superset preview compilation should prefer the legacy form_data transport inferred from browser traffic before falling back to chart-data. +# #region test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy [TYPE Function] +# @BRIEF Superset preview compilation should prefer the legacy form_data transport inferred from browser traffic before falling back to chart-data. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy(): client = SupersetClient(_make_environment()) client.get_dataset = MagicMock( @@ -146,12 +144,12 @@ def test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy(): ] -# [/DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function] +# #endregion test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy -# [DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Superset preview compilation should fall back to chart-data when legacy form_data strategies are rejected. +# #region test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures [TYPE Function] +# @BRIEF Superset preview compilation should fall back to chart-data when legacy form_data strategies are rejected. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures(): client = SupersetClient(_make_environment()) client.get_dataset = MagicMock( @@ -243,12 +241,12 @@ def test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures( } -# [/DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function] +# #endregion test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures -# [DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Preview query context should mirror chart-style filter transport so recovered native filters reach Superset compilation. +# #region test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data [TYPE Function] +# @BRIEF Preview query context should mirror chart-style filter transport so recovered native filters reach Superset compilation. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data(): client = SupersetClient(_make_environment()) @@ -306,12 +304,12 @@ def test_build_dataset_preview_query_context_places_recovered_filters_in_chart_s assert query_context["form_data"]["url_params"] == {"country": "DE"} -# [/DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function] +# #endregion test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data -# [DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Preview query context should merge dataset template params for parity with real dataset definitions while preserving explicit session overrides. +# #region test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values [TYPE Function] +# @BRIEF Preview query context should merge dataset template params for parity with real dataset definitions while preserving explicit session overrides. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values(): client = SupersetClient(_make_environment()) @@ -337,12 +335,12 @@ def test_build_dataset_preview_query_context_merges_dataset_template_params_and_ } -# [/DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function] +# #endregion test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values -# [DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Preview query context should preserve time-range native filter extras even when dataset defaults differ. +# #region test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload [TYPE Function] +# @BRIEF Preview query context should preserve time-range native filter extras even when dataset defaults differ. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload(): client = SupersetClient(_make_environment()) @@ -376,12 +374,12 @@ def test_build_dataset_preview_query_context_preserves_time_range_from_native_fi assert query_context["queries"][0]["filters"] == [] -# [/DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function] +# #endregion test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload -# [DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Legacy preview form_data should preserve recovered native filter clauses in browser-style fields without duplicating datasource for QueryObjectFactory. +# #region test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses [TYPE Function] +# @BRIEF Legacy preview form_data should preserve recovered native filter clauses in browser-style fields without duplicating datasource for QueryObjectFactory. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses(): client = SupersetClient(_make_environment()) @@ -430,12 +428,12 @@ def test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses( assert legacy_form_data["result_type"] == "query" -# [/DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function] +# #endregion test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses -# [DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Sync network client should reserve dashboard-not-found translation for dashboard endpoints only. +# #region test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic [TYPE Function] +# @BRIEF Sync network client should reserve dashboard-not-found translation for dashboard endpoints only. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic(): client = APIClient( config={ @@ -454,12 +452,12 @@ def test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic(): assert "API resource not found at endpoint '/chart/data'" in str(exc_info.value) -# [/DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function] +# #endregion test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic -# [DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Sync network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors. +# #region test_sync_network_404_mapping_translates_dashboard_endpoints [TYPE Function] +# @BRIEF Sync network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] def test_sync_network_404_mapping_translates_dashboard_endpoints(): client = APIClient( config={ @@ -477,12 +475,12 @@ def test_sync_network_404_mapping_translates_dashboard_endpoints(): assert "Dashboard '/dashboard/10' Dashboard not found" in str(exc_info.value) -# [/DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function] +# #endregion test_sync_network_404_mapping_translates_dashboard_endpoints -# [DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Async network client should reserve dashboard-not-found translation for dashboard endpoints only. +# #region test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic [TYPE Function] +# @BRIEF Async network client should reserve dashboard-not-found translation for dashboard endpoints only. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] @pytest.mark.asyncio async def test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic(): client = AsyncAPIClient( @@ -507,12 +505,12 @@ async def test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic() await client.aclose() -# [/DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function] +# #endregion test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic -# [DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function] -# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests -# @PURPOSE: Async network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors. +# #region test_async_network_404_mapping_translates_dashboard_endpoints [TYPE Function] +# @BRIEF Async network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors. +# @RELATION BINDS_TO -> [SupersetPreviewPipelineTests] @pytest.mark.asyncio async def test_async_network_404_mapping_translates_dashboard_endpoints(): client = AsyncAPIClient( @@ -536,7 +534,7 @@ async def test_async_network_404_mapping_translates_dashboard_endpoints(): await client.aclose() -# [/DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function] +# #endregion test_async_network_404_mapping_translates_dashboard_endpoints -# [/DEF:SupersetPreviewPipelineTests:Module] +# #endregion SupersetPreviewPipelineTests diff --git a/backend/src/core/__tests__/test_superset_profile_lookup.py b/backend/src/core/__tests__/test_superset_profile_lookup.py index 8690f5e5..ae0185d8 100644 --- a/backend/src/core/__tests__/test_superset_profile_lookup.py +++ b/backend/src/core/__tests__/test_superset_profile_lookup.py @@ -1,9 +1,7 @@ -# [DEF:TestSupersetProfileLookup:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, superset, profile, lookup, fallback, sorting -# @PURPOSE: Verifies Superset profile lookup adapter payload normalization and fallback error precedence. -# @LAYER: Domain +# #region TestSupersetProfileLookup [C:3] [TYPE Module] [SEMANTICS tests, superset, profile, lookup, fallback, sorting] +# @BRIEF Verifies Superset profile lookup adapter payload normalization and fallback error precedence. +# @LAYER Domain +# @RELATION BELONGS_TO -> [SrcRoot] # [SECTION: IMPORTS] import json @@ -22,26 +20,25 @@ from src.core.utils.network import AuthenticationError, SupersetAPIError # [/SECTION] -# [DEF:_RecordingNetworkClient:Class] -# @RELATION: BINDS_TO -> TestSupersetProfileLookup -# @COMPLEXITY: 2 -# @PURPOSE: Records request payloads and returns scripted responses for deterministic adapter tests. -# @INVARIANT: Each request consumes one scripted response in call order and persists call metadata. +# #region _RecordingNetworkClient [C:2] [TYPE Class] +# @BRIEF Records request payloads and returns scripted responses for deterministic adapter tests. +# @INVARIANT Each request consumes one scripted response in call order and persists call metadata. +# @RELATION BINDS_TO -> [TestSupersetProfileLookup] class _RecordingNetworkClient: - # [DEF:__init__:Function] - # @PURPOSE: Initializes scripted network responses. - # @PRE: scripted_responses is ordered per expected request sequence. - # @POST: Instance stores response script and captures subsequent request calls. + # #region __init__ [TYPE Function] + # @BRIEF Initializes scripted network responses. + # @PRE scripted_responses is ordered per expected request sequence. + # @POST Instance stores response script and captures subsequent request calls. def __init__(self, scripted_responses: List[Any]): self._scripted_responses = scripted_responses self.calls: List[Dict[str, Any]] = [] - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:request:Function] - # @PURPOSE: Mimics APIClient.request while capturing call arguments. - # @PRE: method and endpoint are provided. - # @POST: Returns scripted response or raises scripted exception. + # #region request [TYPE Function] + # @BRIEF Mimics APIClient.request while capturing call arguments. + # @PRE method and endpoint are provided. + # @POST Returns scripted response or raises scripted exception. def request( self, method: str, @@ -62,17 +59,17 @@ class _RecordingNetworkClient: raise response return response - # [/DEF:request:Function] + # #endregion request -# [/DEF:_RecordingNetworkClient:Class] +# #endregion _RecordingNetworkClient -# [DEF:test_get_users_page_sends_lowercase_order_direction:Function] -# @RELATION: BINDS_TO -> TestSupersetProfileLookup -# @PURPOSE: Ensures adapter sends lowercase order_direction compatible with Superset rison schema. -# @PRE: Adapter is initialized with recording network client. -# @POST: First request query payload contains order_direction='asc' for asc sort. +# #region test_get_users_page_sends_lowercase_order_direction [TYPE Function] +# @BRIEF Ensures adapter sends lowercase order_direction compatible with Superset rison schema. +# @PRE Adapter is initialized with recording network client. +# @POST First request query payload contains order_direction='asc' for asc sort. +# @RELATION BINDS_TO -> [TestSupersetProfileLookup] def test_get_users_page_sends_lowercase_order_direction(): client = _RecordingNetworkClient( scripted_responses=[{"result": [{"username": "admin"}], "count": 1}] @@ -93,14 +90,14 @@ def test_get_users_page_sends_lowercase_order_direction(): assert sent_query["order_direction"] == "asc" -# [/DEF:test_get_users_page_sends_lowercase_order_direction:Function] +# #endregion test_get_users_page_sends_lowercase_order_direction -# [DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function] -# @RELATION: BINDS_TO -> TestSupersetProfileLookup -# @PURPOSE: Ensures fallback auth error does not mask primary schema/query failure. -# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError. -# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause. +# #region test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error [TYPE Function] +# @BRIEF Ensures fallback auth error does not mask primary schema/query failure. +# @PRE Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError. +# @POST Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause. +# @RELATION BINDS_TO -> [TestSupersetProfileLookup] def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error(): client = _RecordingNetworkClient( scripted_responses=[ @@ -119,14 +116,14 @@ def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error( assert not isinstance(exc_info.value, AuthenticationError) -# [/DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function] +# #endregion test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error -# [DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function] -# @RELATION: BINDS_TO -> TestSupersetProfileLookup -# @PURPOSE: Verifies adapter retries second users endpoint and succeeds when fallback is healthy. -# @PRE: Primary endpoint fails; fallback returns valid users payload. -# @POST: Result status is success and both endpoints were attempted in order. +# #region test_get_users_page_uses_fallback_endpoint_when_primary_fails [TYPE Function] +# @BRIEF Verifies adapter retries second users endpoint and succeeds when fallback is healthy. +# @PRE Primary endpoint fails; fallback returns valid users payload. +# @POST Result status is success and both endpoints were attempted in order. +# @RELATION BINDS_TO -> [TestSupersetProfileLookup] def test_get_users_page_uses_fallback_endpoint_when_primary_fails(): client = _RecordingNetworkClient( scripted_responses=[ @@ -147,7 +144,7 @@ def test_get_users_page_uses_fallback_endpoint_when_primary_fails(): ] -# [/DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function] +# #endregion test_get_users_page_uses_fallback_endpoint_when_primary_fails -# [/DEF:TestSupersetProfileLookup:Module] +# #endregion TestSupersetProfileLookup diff --git a/backend/src/core/__tests__/test_throttled_scheduler.py b/backend/src/core/__tests__/test_throttled_scheduler.py index ef47ddfb..cd1fd799 100644 --- a/backend/src/core/__tests__/test_throttled_scheduler.py +++ b/backend/src/core/__tests__/test_throttled_scheduler.py @@ -2,15 +2,14 @@ import pytest from datetime import time, date, datetime, timedelta from src.core.scheduler import ThrottledSchedulerConfigurator -# [DEF:test_throttled_scheduler:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for ThrottledSchedulerConfigurator distribution logic. +# #region test_throttled_scheduler [C:3] [TYPE Module] +# @BRIEF Unit tests for ThrottledSchedulerConfigurator distribution logic. +# @RELATION BELONGS_TO -> [SrcRoot] -# [DEF:test_calculate_schedule_even_distribution:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate even spacing across a two-hour scheduling window for three tasks. +# #region test_calculate_schedule_even_distribution [TYPE Function] +# @BRIEF Validate even spacing across a two-hour scheduling window for three tasks. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_even_distribution(): """ @TEST_SCENARIO: 3 tasks in a 2-hour window should be spaced 1 hour apart. @@ -30,12 +29,12 @@ def test_calculate_schedule_even_distribution(): assert schedule[2] == datetime(2024, 1, 1, 3, 0) -# [/DEF:test_calculate_schedule_even_distribution:Function] +# #endregion test_calculate_schedule_even_distribution -# [DEF:test_calculate_schedule_midnight_crossing:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate scheduler correctly rolls timestamps into the next day across midnight. +# #region test_calculate_schedule_midnight_crossing [TYPE Function] +# @BRIEF Validate scheduler correctly rolls timestamps into the next day across midnight. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_midnight_crossing(): """ @TEST_SCENARIO: Window from 23:00 to 01:00 (next day). @@ -55,12 +54,12 @@ def test_calculate_schedule_midnight_crossing(): assert schedule[2] == datetime(2024, 1, 2, 1, 0) -# [/DEF:test_calculate_schedule_midnight_crossing:Function] +# #endregion test_calculate_schedule_midnight_crossing -# [DEF:test_calculate_schedule_single_task:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate single-task schedule returns only the window start timestamp. +# #region test_calculate_schedule_single_task [TYPE Function] +# @BRIEF Validate single-task schedule returns only the window start timestamp. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_single_task(): """ @TEST_SCENARIO: Single task should be scheduled at start time. @@ -78,12 +77,12 @@ def test_calculate_schedule_single_task(): assert schedule[0] == datetime(2024, 1, 1, 1, 0) -# [/DEF:test_calculate_schedule_single_task:Function] +# #endregion test_calculate_schedule_single_task -# [DEF:test_calculate_schedule_empty_list:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate empty dashboard list produces an empty schedule. +# #region test_calculate_schedule_empty_list [TYPE Function] +# @BRIEF Validate empty dashboard list produces an empty schedule. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_empty_list(): """ @TEST_SCENARIO: Empty dashboard list returns empty schedule. @@ -100,12 +99,12 @@ def test_calculate_schedule_empty_list(): assert schedule == [] -# [/DEF:test_calculate_schedule_empty_list:Function] +# #endregion test_calculate_schedule_empty_list -# [DEF:test_calculate_schedule_zero_window:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate zero-length window schedules all tasks at identical start timestamp. +# #region test_calculate_schedule_zero_window [TYPE Function] +# @BRIEF Validate zero-length window schedules all tasks at identical start timestamp. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_zero_window(): """ @TEST_SCENARIO: Window start == end. All tasks at start time. @@ -124,12 +123,12 @@ def test_calculate_schedule_zero_window(): assert schedule[1] == datetime(2024, 1, 1, 1, 0) -# [/DEF:test_calculate_schedule_zero_window:Function] +# #endregion test_calculate_schedule_zero_window -# [DEF:test_calculate_schedule_very_small_window:Function] -# @RELATION: BINDS_TO -> test_throttled_scheduler -# @PURPOSE: Validate sub-second interpolation when task count exceeds near-zero window granularity. +# #region test_calculate_schedule_very_small_window [TYPE Function] +# @BRIEF Validate sub-second interpolation when task count exceeds near-zero window granularity. +# @RELATION BINDS_TO -> [test_throttled_scheduler] def test_calculate_schedule_very_small_window(): """ @TEST_SCENARIO: Window smaller than number of tasks (in seconds). @@ -149,5 +148,5 @@ def test_calculate_schedule_very_small_window(): assert schedule[2] == datetime(2024, 1, 1, 1, 0, 1) -# [/DEF:test_calculate_schedule_very_small_window:Function] -# [/DEF:test_throttled_scheduler:Module] +# #endregion test_calculate_schedule_very_small_window +# #endregion test_throttled_scheduler diff --git a/backend/src/core/async_superset_client.py b/backend/src/core/async_superset_client.py index e45227ad..b0da74aa 100644 --- a/backend/src/core/async_superset_client.py +++ b/backend/src/core/async_superset_client.py @@ -1,8 +1,6 @@ -# [DEF:AsyncSupersetClientModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: superset, async, client, httpx, dashboards, datasets -# @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously. -# @LAYER: Core +# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, async, client, httpx, dashboards, datasets] +# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously. +# @LAYER Core # [SECTION: IMPORTS] @@ -18,20 +16,18 @@ from .utils.async_network import AsyncAPIClient # [/SECTION] -# [DEF:AsyncSupersetClient:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Async sibling of SupersetClient for dashboard read paths. -# @RELATION: [INHERITS] ->[SupersetClient] -# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient] -# @RELATION: [CALLS] ->[AsyncAPIClient.request] +# #region AsyncSupersetClient [C:3] [TYPE Class] +# @BRIEF Async sibling of SupersetClient for dashboard read paths. +# @RELATION INHERITS -> [SupersetClient] +# @RELATION DEPENDS_ON -> [AsyncAPIClient] +# @RELATION CALLS -> [AsyncAPIClient.request] class AsyncSupersetClient(SupersetClient): - # [DEF:AsyncSupersetClientInit:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport. - # @PRE: env is valid Environment instance. - # @POST: Client uses async network transport and inherited projection helpers. - # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient] - # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient] + # #region AsyncSupersetClientInit [C:3] [TYPE Function] + # @BRIEF Initialize async Superset client with AsyncAPIClient transport. + # @PRE env is valid Environment instance. + # @POST Client uses async network transport and inherited projection helpers. + # @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient] + # @RELATION DEPENDS_ON -> [AsyncAPIClient] def __init__(self, env: Environment): self.env = env auth_payload = { @@ -47,25 +43,23 @@ class AsyncSupersetClient(SupersetClient): ) self.delete_before_reimport = False - # [/DEF:AsyncSupersetClientInit:Function] + # #endregion AsyncSupersetClientInit - # [DEF:AsyncSupersetClientClose:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Close async transport resources. - # @POST: Underlying AsyncAPIClient is closed. - # @SIDE_EFFECT: Closes network sockets. - # @RELATION: [CALLS] ->[AsyncAPIClient.aclose] + # #region AsyncSupersetClientClose [C:3] [TYPE Function] + # @BRIEF Close async transport resources. + # @POST Underlying AsyncAPIClient is closed. + # @SIDE_EFFECT Closes network sockets. + # @RELATION CALLS -> [AsyncAPIClient.aclose] async def aclose(self) -> None: await self.network.aclose() - # [/DEF:AsyncSupersetClientClose:Function] + # #endregion AsyncSupersetClientClose - # [DEF:get_dashboards_page_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch one dashboards page asynchronously. - # @POST: Returns total count and page result list. - # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]] - # @RELATION: [CALLS] -> [AsyncAPIClient.request] + # #region get_dashboards_page_async [C:3] [TYPE Function] + # @BRIEF Fetch one dashboards page asynchronously. + # @POST Returns total count and page result list. + # @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]] + # @RELATION CALLS -> [AsyncAPIClient.request] async def get_dashboards_page_async( self, query: Optional[Dict] = None ) -> Tuple[int, List[Dict]]: @@ -97,14 +91,13 @@ class AsyncSupersetClient(SupersetClient): total_count = response_json.get("count", len(result)) return total_count, result - # [/DEF:get_dashboards_page_async:Function] + # #endregion get_dashboards_page_async - # [DEF:get_dashboard_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch one dashboard payload asynchronously. - # @POST: Returns raw dashboard payload from Superset API. - # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict] - # @RELATION: [CALLS] ->[AsyncAPIClient.request] + # #region get_dashboard_async [C:3] [TYPE Function] + # @BRIEF Fetch one dashboard payload asynchronously. + # @POST Returns raw dashboard payload from Superset API. + # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict] + # @RELATION CALLS -> [AsyncAPIClient.request] async def get_dashboard_async(self, dashboard_id: int) -> Dict: with belief_scope( "AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}" @@ -114,14 +107,13 @@ class AsyncSupersetClient(SupersetClient): ) return cast(Dict, response) - # [/DEF:get_dashboard_async:Function] + # #endregion get_dashboard_async - # [DEF:get_chart_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch one chart payload asynchronously. - # @POST: Returns raw chart payload from Superset API. - # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict] - # @RELATION: [CALLS] ->[AsyncAPIClient.request] + # #region get_chart_async [C:3] [TYPE Function] + # @BRIEF Fetch one chart payload asynchronously. + # @POST Returns raw chart payload from Superset API. + # @DATA_CONTRACT Input[chart_id: int] -> Output[Dict] + # @RELATION CALLS -> [AsyncAPIClient.request] async def get_chart_async(self, chart_id: int) -> Dict: with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"): response = await self.network.request( @@ -129,15 +121,14 @@ class AsyncSupersetClient(SupersetClient): ) return cast(Dict, response) - # [/DEF:get_chart_async:Function] + # #endregion get_chart_async - # [DEF:get_dashboard_detail_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests. - # @POST: Returns dashboard detail payload for overview page. - # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict] - # @RELATION: [CALLS] ->[get_dashboard_async] - # @RELATION: [CALLS] ->[get_chart_async] + # #region get_dashboard_detail_async [C:3] [TYPE Function] + # @BRIEF Fetch dashboard detail asynchronously with concurrent charts/datasets requests. + # @POST Returns dashboard detail payload for overview page. + # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict] + # @RELATION CALLS -> [get_dashboard_async] + # @RELATION CALLS -> [get_chart_async] async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict: with belief_scope( "AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}" @@ -427,13 +418,12 @@ class AsyncSupersetClient(SupersetClient): "dataset_count": len(datasets), } - # [/DEF:get_dashboard_detail_async:Function] + # #endregion get_dashboard_detail_async - # [DEF:get_dashboard_permalink_state_async:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Fetch stored dashboard permalink state asynchronously. - # @POST: Returns dashboard permalink state payload from Superset API. - # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict] + # #region get_dashboard_permalink_state_async [C:2] [TYPE Function] + # @BRIEF Fetch stored dashboard permalink state asynchronously. + # @POST Returns dashboard permalink state payload from Superset API. + # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict] async def get_dashboard_permalink_state_async(self, permalink_key: str) -> Dict: with belief_scope( "AsyncSupersetClient.get_dashboard_permalink_state_async", @@ -444,13 +434,12 @@ class AsyncSupersetClient(SupersetClient): ) return cast(Dict, response) - # [/DEF:get_dashboard_permalink_state_async:Function] + # #endregion get_dashboard_permalink_state_async - # [DEF:get_native_filter_state_async:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Fetch stored native filter state asynchronously. - # @POST: Returns native filter state payload from Superset API. - # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict] + # #region get_native_filter_state_async [C:2] [TYPE Function] + # @BRIEF Fetch stored native filter state asynchronously. + # @POST Returns native filter state payload from Superset API. + # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict] async def get_native_filter_state_async( self, dashboard_id: int, filter_state_key: str ) -> Dict: @@ -464,14 +453,13 @@ class AsyncSupersetClient(SupersetClient): ) return cast(Dict, response) - # [/DEF:get_native_filter_state_async:Function] + # #endregion get_native_filter_state_async - # [DEF:extract_native_filters_from_permalink_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously. - # @POST: Returns extracted dataMask with filter states. - # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict] - # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async] + # #region extract_native_filters_from_permalink_async [C:3] [TYPE Function] + # @BRIEF Extract native filters dataMask from a permalink key asynchronously. + # @POST Returns extracted dataMask with filter states. + # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict] + # @RELATION CALLS -> [get_dashboard_permalink_state_async] async def extract_native_filters_from_permalink_async( self, permalink_key: str ) -> Dict: @@ -505,14 +493,13 @@ class AsyncSupersetClient(SupersetClient): "permalink_key": permalink_key, } - # [/DEF:extract_native_filters_from_permalink_async:Function] + # #endregion extract_native_filters_from_permalink_async - # [DEF:extract_native_filters_from_key_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously. - # @POST: Returns extracted filter state with extraFormData. - # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict] - # @RELATION: [CALLS] ->[get_native_filter_state_async] + # #region extract_native_filters_from_key_async [C:3] [TYPE Function] + # @BRIEF Extract native filters from a native_filters_key URL parameter asynchronously. + # @POST Returns extracted filter state with extraFormData. + # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict] + # @RELATION CALLS -> [get_native_filter_state_async] async def extract_native_filters_from_key_async( self, dashboard_id: int, filter_state_key: str ) -> Dict: @@ -566,15 +553,14 @@ class AsyncSupersetClient(SupersetClient): "filter_state_key": filter_state_key, } - # [/DEF:extract_native_filters_from_key_async:Function] + # #endregion extract_native_filters_from_key_async - # [DEF:parse_dashboard_url_for_filters_async:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously. - # @POST: Returns extracted filter state or empty dict if no filters found. - # @DATA_CONTRACT: Input[url: str] -> Output[Dict] - # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async] - # @RELATION: [CALLS] ->[extract_native_filters_from_key_async] + # #region parse_dashboard_url_for_filters_async [C:3] [TYPE Function] + # @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously. + # @POST Returns extracted filter state or empty dict if no filters found. + # @DATA_CONTRACT Input[url: str] -> Output[Dict] + # @RELATION CALLS -> [extract_native_filters_from_permalink_async] + # @RELATION CALLS -> [extract_native_filters_from_key_async] async def parse_dashboard_url_for_filters_async(self, url: str) -> Dict: with belief_scope( "AsyncSupersetClient.parse_dashboard_url_for_filters_async", f"url={url}" @@ -676,9 +662,7 @@ class AsyncSupersetClient(SupersetClient): return result - # [/DEF:parse_dashboard_url_for_filters_async:Function] + # #endregion parse_dashboard_url_for_filters_async -# [/DEF:AsyncSupersetClient:Class] - -# [/DEF:AsyncSupersetClientModule:Module] +# #endregion AsyncSupersetClient diff --git a/backend/src/core/auth/__init__.py b/backend/src/core/auth/__init__.py index 29e2a055..9213310d 100644 --- a/backend/src/core/auth/__init__.py +++ b/backend/src/core/auth/__init__.py @@ -1,3 +1,3 @@ -# [DEF:AuthPackage:Package] -# @PURPOSE: Authentication and authorization package root. -# [/DEF:AuthPackage:Package] +# #region AuthPackage [TYPE Package] +# @BRIEF Authentication and authorization package root. +# #endregion AuthPackage diff --git a/backend/src/core/auth/__tests__/test_auth.py b/backend/src/core/auth/__tests__/test_auth.py index 3da432d0..89b3fc5b 100644 --- a/backend/src/core/auth/__tests__/test_auth.py +++ b/backend/src/core/auth/__tests__/test_auth.py @@ -1,8 +1,7 @@ -# [DEF:test_auth:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for authentication module -# @LAYER: Domain -# @RELATION: VERIFIES -> AuthPackage +# #region test_auth [C:3] [TYPE Module] +# @BRIEF Unit tests for authentication module +# @LAYER Domain +# @RELATION VERIFIES -> [AuthPackage] import sys from pathlib import Path @@ -58,9 +57,9 @@ def auth_repo(db_session): return AuthRepository(db_session) -# [DEF:test_create_user:Function] -# @PURPOSE: Verifies that a persisted user can be retrieved with intact credential hash. -# @RELATION: BINDS_TO -> test_auth +# #region test_create_user [TYPE Function] +# @BRIEF Verifies that a persisted user can be retrieved with intact credential hash. +# @RELATION BINDS_TO -> [test_auth] def test_create_user(auth_repo): """Test user creation""" user = User( @@ -80,12 +79,12 @@ def test_create_user(auth_repo): assert verify_password("testpassword123", retrieved_user.password_hash) -# [/DEF:test_create_user:Function] +# #endregion test_create_user -# [DEF:test_authenticate_user:Function] -# @PURPOSE: Validates authentication outcomes for valid, wrong-password, and unknown-user cases. -# @RELATION: BINDS_TO -> test_auth +# #region test_authenticate_user [TYPE Function] +# @BRIEF Validates authentication outcomes for valid, wrong-password, and unknown-user cases. +# @RELATION BINDS_TO -> [test_auth] def test_authenticate_user(auth_service, auth_repo): """Test user authentication with valid and invalid credentials""" user = User( @@ -112,12 +111,12 @@ def test_authenticate_user(auth_service, auth_repo): assert invalid_user is None -# [/DEF:test_authenticate_user:Function] +# #endregion test_authenticate_user -# [DEF:test_create_session:Function] -# @PURPOSE: Ensures session creation returns bearer token payload fields. -# @RELATION: BINDS_TO -> test_auth +# #region test_create_session [TYPE Function] +# @BRIEF Ensures session creation returns bearer token payload fields. +# @RELATION BINDS_TO -> [test_auth] def test_create_session(auth_service, auth_repo): """Test session token creation""" user = User( @@ -137,12 +136,12 @@ def test_create_session(auth_service, auth_repo): assert len(session["access_token"]) > 0 -# [/DEF:test_create_session:Function] +# #endregion test_create_session -# [DEF:test_role_permission_association:Function] -# @PURPOSE: Confirms role-permission many-to-many assignments persist and reload correctly. -# @RELATION: BINDS_TO -> test_auth +# #region test_role_permission_association [TYPE Function] +# @BRIEF Confirms role-permission many-to-many assignments persist and reload correctly. +# @RELATION BINDS_TO -> [test_auth] def test_role_permission_association(auth_repo): """Test role and permission association""" role = Role(name="Admin", description="System administrator") @@ -163,12 +162,12 @@ def test_role_permission_association(auth_repo): assert "admin:users:WRITE" in permissions -# [/DEF:test_role_permission_association:Function] +# #endregion test_role_permission_association -# [DEF:test_user_role_association:Function] -# @PURPOSE: Confirms user-role assignment persists and is queryable from repository reads. -# @RELATION: BINDS_TO -> test_auth +# #region test_user_role_association [TYPE Function] +# @BRIEF Confirms user-role assignment persists and is queryable from repository reads. +# @RELATION BINDS_TO -> [test_auth] def test_user_role_association(auth_repo): """Test user and role association""" role = Role(name="Admin", description="System administrator") @@ -191,12 +190,12 @@ def test_user_role_association(auth_repo): assert retrieved_user.roles[0].name == "Admin" -# [/DEF:test_user_role_association:Function] +# #endregion test_user_role_association -# [DEF:test_ad_group_mapping:Function] -# @PURPOSE: Verifies AD group mapping rows persist and reference the expected role. -# @RELATION: BINDS_TO -> test_auth +# #region test_ad_group_mapping [TYPE Function] +# @BRIEF Verifies AD group mapping rows persist and reference the expected role. +# @RELATION BINDS_TO -> [test_auth] def test_ad_group_mapping(auth_repo): """Test AD group mapping""" role = Role(name="ADFS_Admin", description="ADFS administrators") @@ -218,12 +217,12 @@ def test_ad_group_mapping(auth_repo): assert retrieved_mapping.role_id == role.id -# [/DEF:test_ad_group_mapping:Function] +# #endregion test_ad_group_mapping -# [DEF:test_authenticate_user_updates_last_login:Function] -# @PURPOSE: Verifies successful authentication updates last_login audit field. -# @RELATION: BINDS_TO -> test_auth +# #region test_authenticate_user_updates_last_login [TYPE Function] +# @BRIEF Verifies successful authentication updates last_login audit field. +# @RELATION BINDS_TO -> [test_auth] def test_authenticate_user_updates_last_login(auth_service, auth_repo): """@SIDE_EFFECT: authenticate_user updates last_login timestamp on success.""" user = User( @@ -242,12 +241,12 @@ def test_authenticate_user_updates_last_login(auth_service, auth_repo): assert authenticated.last_login is not None -# [/DEF:test_authenticate_user_updates_last_login:Function] +# #endregion test_authenticate_user_updates_last_login -# [DEF:test_authenticate_inactive_user:Function] -# @PURPOSE: Verifies inactive accounts are rejected during password authentication. -# @RELATION: BINDS_TO -> test_auth +# #region test_authenticate_inactive_user [TYPE Function] +# @BRIEF Verifies inactive accounts are rejected during password authentication. +# @RELATION BINDS_TO -> [test_auth] def test_authenticate_inactive_user(auth_service, auth_repo): """@PRE: User with is_active=False should not authenticate.""" user = User( @@ -264,24 +263,24 @@ def test_authenticate_inactive_user(auth_service, auth_repo): assert result is None -# [/DEF:test_authenticate_inactive_user:Function] +# #endregion test_authenticate_inactive_user -# [DEF:test_verify_password_empty_hash:Function] -# @PURPOSE: Verifies password verification safely rejects empty or null password hashes. -# @RELATION: BINDS_TO -> test_auth +# #region test_verify_password_empty_hash [TYPE Function] +# @BRIEF Verifies password verification safely rejects empty or null password hashes. +# @RELATION BINDS_TO -> [test_auth] def test_verify_password_empty_hash(): """@PRE: verify_password with empty/None hash returns False.""" assert verify_password("anypassword", "") is False assert verify_password("anypassword", None) is False -# [/DEF:test_verify_password_empty_hash:Function] +# #endregion test_verify_password_empty_hash -# [DEF:test_provision_adfs_user_new:Function] -# @PURPOSE: Verifies JIT provisioning creates a new ADFS user and maps group-derived roles. -# @RELATION: BINDS_TO -> test_auth +# #region test_provision_adfs_user_new [TYPE Function] +# @BRIEF Verifies JIT provisioning creates a new ADFS user and maps group-derived roles. +# @RELATION BINDS_TO -> [test_auth] def test_provision_adfs_user_new(auth_service, auth_repo): """@POST: provision_adfs_user creates a new ADFS user with correct roles.""" # Set up a role and AD group mapping @@ -308,7 +307,7 @@ def test_provision_adfs_user_new(auth_service, auth_repo): assert user.roles[0].name == "ADFS_Viewer" -# [/DEF:test_provision_adfs_user_new:Function] +# #endregion test_provision_adfs_user_new # [DEF:test_provision_adfs_user_existing:Function] @@ -338,5 +337,5 @@ def test_provision_adfs_user_existing(auth_service, auth_repo): assert len(user.roles) == 0 # No matching group mappings -# [/DEF:test_auth:Module] -# [/DEF:test_provision_adfs_user_existing:Function] +# #endregion test_auth +# #endregion test_provision_adfs_user_existing diff --git a/backend/src/core/auth/config.py b/backend/src/core/auth/config.py index 05f9d9de..677d3821 100644 --- a/backend/src/core/auth/config.py +++ b/backend/src/core/auth/config.py @@ -1,23 +1,21 @@ -# [DEF:AuthConfigModule:Module] +# #region AuthConfigModule [C:2] [TYPE Module] [SEMANTICS auth, config, settings, jwt, adfs] +# @BRIEF Centralized configuration for authentication and authorization. +# @LAYER Core +# @INVARIANT All sensitive configuration must have defaults or be loaded from environment. +# @RELATION DEPENDS_ON -> [pydantic] # -# @SEMANTICS: auth, config, settings, jwt, adfs -# @PURPOSE: Centralized configuration for authentication and authorization. -# @COMPLEXITY: 2 -# @LAYER: Core -# @RELATION: DEPENDS_ON -> pydantic # -# @INVARIANT: All sensitive configuration must have defaults or be loaded from environment. # [SECTION: IMPORTS] from pydantic import Field from pydantic_settings import BaseSettings # [/SECTION] -# [DEF:AuthConfig:Class] -# @PURPOSE: Holds authentication-related settings. -# @PRE: Environment variables may be provided via .env file. -# @POST: Returns a configuration object with validated settings. -# @RELATION: INHERITS -> pydantic_settings.BaseSettings +# #region AuthConfig [TYPE Class] +# @BRIEF Holds authentication-related settings. +# @PRE Environment variables may be provided via .env file. +# @POST Returns a configuration object with validated settings. +# @RELATION INHERITS -> [pydantic_settings.BaseSettings] class AuthConfig(BaseSettings): # JWT Settings SECRET_KEY: str = Field(default="super-secret-key-change-in-production", env="AUTH_SECRET_KEY") @@ -39,12 +37,12 @@ class AuthConfig(BaseSettings): class Config: env_file = ".env" extra = "ignore" -# [/DEF:AuthConfig:Class] +# #endregion AuthConfig -# [DEF:auth_config:Variable] -# @PURPOSE: Singleton instance of AuthConfig. -# @RELATION: DEPENDS_ON -> AuthConfig +# #region auth_config [TYPE Variable] +# @BRIEF Singleton instance of AuthConfig. +# @RELATION DEPENDS_ON -> [AuthConfig] auth_config = AuthConfig() -# [/DEF:auth_config:Variable] +# #endregion auth_config -# [/DEF:AuthConfigModule:Module] +# #endregion AuthConfigModule diff --git a/backend/src/core/auth/jwt.py b/backend/src/core/auth/jwt.py index fac215b5..41406869 100644 --- a/backend/src/core/auth/jwt.py +++ b/backend/src/core/auth/jwt.py @@ -1,13 +1,11 @@ -# [DEF:AuthJwtModule:Module] +# #region AuthJwtModule [C:3] [TYPE Module] [SEMANTICS jwt, token, session, auth] +# @BRIEF JWT token generation and validation logic. +# @LAYER Core +# @INVARIANT Tokens must include expiration time and user identifier. +# @RELATION DEPENDS_ON -> [auth_config] +# @RELATION USES -> [auth_config] # -# @COMPLEXITY: 3 -# @SEMANTICS: jwt, token, session, auth -# @PURPOSE: JWT token generation and validation logic. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [auth_config] -# @RELATION: USES -> [auth_config] # -# @INVARIANT: Tokens must include expiration time and user identifier. # [SECTION: IMPORTS] from datetime import datetime, timedelta @@ -18,11 +16,11 @@ from ..logger import belief_scope # [/SECTION] -# [DEF:create_access_token:Function] -# @PURPOSE: Generates a new JWT access token. -# @PRE: data dict contains 'sub' (user_id) and optional 'scopes' (roles). -# @POST: Returns a signed JWT string. -# @RELATION: DEPENDS_ON -> [auth_config] +# #region create_access_token [TYPE Function] +# @BRIEF Generates a new JWT access token. +# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles). +# @POST Returns a signed JWT string. +# @RELATION DEPENDS_ON -> [auth_config] # # @PARAM: data (dict) - Payload data for the token. # @PARAM: expires_delta (Optional[timedelta]) - Custom expiration time. @@ -44,14 +42,14 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) - return encoded_jwt -# [/DEF:create_access_token:Function] +# #endregion create_access_token -# [DEF:decode_token:Function] -# @PURPOSE: Decodes and validates a JWT token. -# @PRE: token is a signed JWT string. -# @POST: Returns the decoded payload if valid. -# @RELATION: DEPENDS_ON -> [auth_config] +# #region decode_token [TYPE Function] +# @BRIEF Decodes and validates a JWT token. +# @PRE token is a signed JWT string. +# @POST Returns the decoded payload if valid. +# @RELATION DEPENDS_ON -> [auth_config] # # @PARAM: token (str) - The JWT to decode. # @RETURN: dict - The decoded payload. @@ -64,6 +62,6 @@ def decode_token(token: str) -> dict: return payload -# [/DEF:decode_token:Function] +# #endregion decode_token -# [/DEF:AuthJwtModule:Module] +# #endregion AuthJwtModule diff --git a/backend/src/core/auth/logger.py b/backend/src/core/auth/logger.py index 9052668b..f2a37f4f 100644 --- a/backend/src/core/auth/logger.py +++ b/backend/src/core/auth/logger.py @@ -1,23 +1,21 @@ -# [DEF:AuthLoggerModule:Module] +# #region AuthLoggerModule [C:3] [TYPE Module] [SEMANTICS auth, logger, audit, security] +# @BRIEF Audit logging for security-related events. +# @LAYER Core +# @INVARIANT Must not log sensitive data like passwords or full tokens. +# @RELATION USES -> [belief_scope] # -# @COMPLEXITY: 3 -# @SEMANTICS: auth, logger, audit, security -# @PURPOSE: Audit logging for security-related events. -# @LAYER: Core -# @RELATION: USES -> belief_scope # -# @INVARIANT: Must not log sensitive data like passwords or full tokens. # [SECTION: IMPORTS] from ..logger import logger, belief_scope from datetime import datetime # [/SECTION] -# [DEF:log_security_event:Function] -# @PURPOSE: Logs a security-related event for audit trails. -# @PRE: event_type and username are strings. -# @POST: Security event is written to the application log. -# @RELATION: USES -> logger +# #region log_security_event [TYPE Function] +# @BRIEF Logs a security-related event for audit trails. +# @PRE event_type and username are strings. +# @POST Security event is written to the application log. +# @RELATION USES -> [logger] # @PARAM: event_type (str) - Type of event (e.g., LOGIN_SUCCESS, PERMISSION_DENIED). # @PARAM: username (str) - The user involved in the event. # @PARAM: details (dict) - Additional non-sensitive metadata. @@ -28,6 +26,6 @@ def log_security_event(event_type: str, username: str, details: dict = None): if details: msg += f" Details: {details}" logger.info(msg) -# [/DEF:log_security_event:Function] +# #endregion log_security_event -# [/DEF:AuthLoggerModule:Module] \ No newline at end of file +# #endregion AuthLoggerModule diff --git a/backend/src/core/auth/oauth.py b/backend/src/core/auth/oauth.py index 04d6852e..4d466644 100644 --- a/backend/src/core/auth/oauth.py +++ b/backend/src/core/auth/oauth.py @@ -1,31 +1,29 @@ -# [DEF:AuthOauthModule:Module] +# #region AuthOauthModule [C:2] [TYPE Module] [SEMANTICS auth, oauth, oidc, adfs] +# @BRIEF ADFS OIDC configuration and client using Authlib. +# @LAYER Core +# @INVARIANT Must use secure OIDC flows. +# @RELATION DEPENDS_ON -> [authlib] +# @RELATION USES -> [auth_config] # -# @SEMANTICS: auth, oauth, oidc, adfs -# @PURPOSE: ADFS OIDC configuration and client using Authlib. -# @COMPLEXITY: 2 -# @LAYER: Core -# @RELATION: DEPENDS_ON -> authlib -# @RELATION: USES -> auth_config # -# @INVARIANT: Must use secure OIDC flows. # [SECTION: IMPORTS] from authlib.integrations.starlette_client import OAuth from .config import auth_config # [/SECTION] -# [DEF:oauth:Variable] -# @PURPOSE: Global Authlib OAuth registry. -# @RELATION: DEPENDS_ON -> OAuth +# #region oauth [TYPE Variable] +# @BRIEF Global Authlib OAuth registry. +# @RELATION DEPENDS_ON -> [OAuth] oauth = OAuth() -# [/DEF:oauth:Variable] +# #endregion oauth -# [DEF:register_adfs:Function] -# @PURPOSE: Registers the ADFS OIDC client. -# @PRE: ADFS configuration is provided in auth_config. -# @POST: ADFS client is registered in oauth registry. -# @RELATION: USES -> oauth -# @RELATION: USES -> auth_config +# #region register_adfs [TYPE Function] +# @BRIEF Registers the ADFS OIDC client. +# @PRE ADFS configuration is provided in auth_config. +# @POST ADFS client is registered in oauth registry. +# @RELATION USES -> [oauth] +# @RELATION USES -> [auth_config] def register_adfs(): if auth_config.ADFS_CLIENT_ID: oauth.register( @@ -37,20 +35,20 @@ def register_adfs(): 'scope': 'openid email profile groups' } ) -# [/DEF:register_adfs:Function] +# #endregion register_adfs -# [DEF:is_adfs_configured:Function] -# @PURPOSE: Checks if ADFS is properly configured. -# @PRE: None. -# @POST: Returns True if ADFS client is registered, False otherwise. -# @RELATION: USES -> oauth -# @RETURN: bool - Configuration status. +# #region is_adfs_configured [TYPE Function] +# @BRIEF Checks if ADFS is properly configured. +# @PRE None. +# @POST Returns True if ADFS client is registered, False otherwise. +# @RETURN bool - Configuration status. +# @RELATION USES -> [oauth] def is_adfs_configured() -> bool: """Check if ADFS OAuth client is registered.""" return 'adfs' in oauth._registry -# [/DEF:is_adfs_configured:Function] +# #endregion is_adfs_configured # Initial registration register_adfs() -# [/DEF:AuthOauthModule:Module] \ No newline at end of file +# #endregion AuthOauthModule diff --git a/backend/src/core/auth/repository.py b/backend/src/core/auth/repository.py index 2f053767..eb596398 100644 --- a/backend/src/core/auth/repository.py +++ b/backend/src/core/auth/repository.py @@ -1,16 +1,14 @@ -# [DEF:AuthRepositoryModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: auth, repository, database, user, role, permission -# @PURPOSE: Data access layer for authentication and user preference entities. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [AuthModels] -# @RELATION: DEPENDS_ON -> [ProfileModels] -# @RELATION: DEPENDS_ON -> [belief_scope] -# @INVARIANT: All database read/write operations must execute via the injected SQLAlchemy session boundary. -# @DATA_CONTRACT: Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access] -# @PRE: Database connection is active. -# @POST: Provides valid access to identity data. -# @SIDE_EFFECT: Executes database read queries through the injected SQLAlchemy session boundary. +# #region AuthRepositoryModule [C:5] [TYPE Module] [SEMANTICS auth, repository, database, user, role, permission] +# @BRIEF Data access layer for authentication and user preference entities. +# @LAYER Domain +# @PRE Database connection is active. +# @POST Provides valid access to identity data. +# @SIDE_EFFECT Executes database read queries through the injected SQLAlchemy session boundary. +# @DATA_CONTRACT Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access] +# @INVARIANT All database read/write operations must execute via the injected SQLAlchemy session boundary. +# @RELATION DEPENDS_ON -> [AuthModels] +# @RELATION DEPENDS_ON -> [ProfileModels] +# @RELATION DEPENDS_ON -> [belief_scope] # [SECTION: IMPORTS] from typing import List, Optional @@ -21,21 +19,21 @@ from ..logger import belief_scope, logger # [/SECTION] -# [DEF:AuthRepository:Class] -# @PURPOSE: Provides low-level CRUD operations for identity and authorization records. -# @PRE: Database session is bound. -# @POST: Entity instances returned safely. -# @SIDE_EFFECT: Performs database reads. -# @RELATION: DEPENDS_ON -> [AuthModels] +# #region AuthRepository [TYPE Class] +# @BRIEF Provides low-level CRUD operations for identity and authorization records. +# @PRE Database session is bound. +# @POST Entity instances returned safely. +# @SIDE_EFFECT Performs database reads. +# @RELATION DEPENDS_ON -> [AuthModels] class AuthRepository: def __init__(self, db: Session): self.db = db - # [DEF:get_user_by_id:Function] - # @PURPOSE: Retrieve user by UUID. - # @PRE: user_id is a valid UUID string. - # @POST: Returns User object if found, else None. - # @RELATION: DEPENDS_ON -> [User] + # #region get_user_by_id [TYPE Function] + # @BRIEF Retrieve user by UUID. + # @PRE user_id is a valid UUID string. + # @POST Returns User object if found, else None. + # @RELATION DEPENDS_ON -> [User] def get_user_by_id(self, user_id: str) -> Optional[User]: with belief_scope("AuthRepository.get_user_by_id"): logger.reason(f"Fetching user by id: {user_id}") @@ -43,13 +41,13 @@ class AuthRepository: logger.reflect(f"User found: {result is not None}") return result - # [/DEF:get_user_by_id:Function] + # #endregion get_user_by_id - # [DEF:get_user_by_username:Function] - # @PURPOSE: Retrieve user by username. - # @PRE: username is a non-empty string. - # @POST: Returns User object if found, else None. - # @RELATION: DEPENDS_ON -> [User] + # #region get_user_by_username [TYPE Function] + # @BRIEF Retrieve user by username. + # @PRE username is a non-empty string. + # @POST Returns User object if found, else None. + # @RELATION DEPENDS_ON -> [User] def get_user_by_username(self, username: str) -> Optional[User]: with belief_scope("AuthRepository.get_user_by_username"): logger.reason(f"Fetching user by username: {username}") @@ -57,12 +55,12 @@ class AuthRepository: logger.reflect(f"User found: {result is not None}") return result - # [/DEF:get_user_by_username:Function] + # #endregion get_user_by_username - # [DEF:get_role_by_id:Function] - # @PURPOSE: Retrieve role by UUID with permissions preloaded. - # @RELATION: DEPENDS_ON -> [Role] - # @RELATION: DEPENDS_ON -> [Permission] + # #region get_role_by_id [TYPE Function] + # @BRIEF Retrieve role by UUID with permissions preloaded. + # @RELATION DEPENDS_ON -> [Role] + # @RELATION DEPENDS_ON -> [Permission] def get_role_by_id(self, role_id: str) -> Optional[Role]: with belief_scope("AuthRepository.get_role_by_id"): return ( @@ -72,31 +70,31 @@ class AuthRepository: .first() ) - # [/DEF:get_role_by_id:Function] + # #endregion get_role_by_id - # [DEF:get_role_by_name:Function] - # @PURPOSE: Retrieve role by unique name. - # @RELATION: DEPENDS_ON -> [Role] + # #region get_role_by_name [TYPE Function] + # @BRIEF Retrieve role by unique name. + # @RELATION DEPENDS_ON -> [Role] def get_role_by_name(self, name: str) -> Optional[Role]: with belief_scope("AuthRepository.get_role_by_name"): return self.db.query(Role).filter(Role.name == name).first() - # [/DEF:get_role_by_name:Function] + # #endregion get_role_by_name - # [DEF:get_permission_by_id:Function] - # @PURPOSE: Retrieve permission by UUID. - # @RELATION: DEPENDS_ON -> [Permission] + # #region get_permission_by_id [TYPE Function] + # @BRIEF Retrieve permission by UUID. + # @RELATION DEPENDS_ON -> [Permission] def get_permission_by_id(self, permission_id: str) -> Optional[Permission]: with belief_scope("AuthRepository.get_permission_by_id"): return ( self.db.query(Permission).filter(Permission.id == permission_id).first() ) - # [/DEF:get_permission_by_id:Function] + # #endregion get_permission_by_id - # [DEF:get_permission_by_resource_action:Function] - # @PURPOSE: Retrieve permission by resource and action tuple. - # @RELATION: DEPENDS_ON -> [Permission] + # #region get_permission_by_resource_action [TYPE Function] + # @BRIEF Retrieve permission by resource and action tuple. + # @RELATION DEPENDS_ON -> [Permission] def get_permission_by_resource_action( self, resource: str, action: str ) -> Optional[Permission]: @@ -107,20 +105,20 @@ class AuthRepository: .first() ) - # [/DEF:get_permission_by_resource_action:Function] + # #endregion get_permission_by_resource_action - # [DEF:list_permissions:Function] - # @PURPOSE: List all system permissions. - # @RELATION: DEPENDS_ON -> [Permission] + # #region list_permissions [TYPE Function] + # @BRIEF List all system permissions. + # @RELATION DEPENDS_ON -> [Permission] def list_permissions(self) -> List[Permission]: with belief_scope("AuthRepository.list_permissions"): return self.db.query(Permission).all() - # [/DEF:list_permissions:Function] + # #endregion list_permissions - # [DEF:get_user_dashboard_preference:Function] - # @PURPOSE: Retrieve dashboard filters/preferences for a user. - # @RELATION: DEPENDS_ON -> [UserDashboardPreference] + # #region get_user_dashboard_preference [TYPE Function] + # @BRIEF Retrieve dashboard filters/preferences for a user. + # @RELATION DEPENDS_ON -> [UserDashboardPreference] def get_user_dashboard_preference( self, user_id: str ) -> Optional[UserDashboardPreference]: @@ -131,14 +129,14 @@ class AuthRepository: .first() ) - # [/DEF:get_user_dashboard_preference:Function] + # #endregion get_user_dashboard_preference - # [DEF:get_roles_by_ad_groups:Function] - # @PURPOSE: Retrieve roles that match a list of AD group names. - # @PRE: groups is a list of strings representing AD group identifiers. - # @POST: Returns a list of Role objects mapped to the provided AD groups. - # @RELATION: DEPENDS_ON -> [Role] - # @RELATION: DEPENDS_ON -> [ADGroupMapping] + # #region get_roles_by_ad_groups [TYPE Function] + # @BRIEF Retrieve roles that match a list of AD group names. + # @PRE groups is a list of strings representing AD group identifiers. + # @POST Returns a list of Role objects mapped to the provided AD groups. + # @RELATION DEPENDS_ON -> [Role] + # @RELATION DEPENDS_ON -> [ADGroupMapping] def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]: with belief_scope("AuthRepository.get_roles_by_ad_groups"): logger.reason(f"Fetching roles for AD groups: {groups}") @@ -151,9 +149,9 @@ class AuthRepository: .all() ) - # [/DEF:get_roles_by_ad_groups:Function] + # #endregion get_roles_by_ad_groups -# [/DEF:AuthRepository:Class] +# #endregion AuthRepository -# [/DEF:AuthRepositoryModule:Module] +# #endregion AuthRepositoryModule diff --git a/backend/src/core/auth/security.py b/backend/src/core/auth/security.py index 2cd48fab..921de333 100644 --- a/backend/src/core/auth/security.py +++ b/backend/src/core/auth/security.py @@ -1,22 +1,20 @@ -# [DEF:AuthSecurityModule:Module] +# #region AuthSecurityModule [C:2] [TYPE Module] [SEMANTICS security, password, hashing, bcrypt] +# @BRIEF Utility for password hashing and verification using Passlib. +# @LAYER Core +# @INVARIANT Uses bcrypt for hashing with standard work factor. +# @RELATION DEPENDS_ON -> [bcrypt] # -# @SEMANTICS: security, password, hashing, bcrypt -# @PURPOSE: Utility for password hashing and verification using Passlib. -# @COMPLEXITY: 2 -# @LAYER: Core -# @RELATION: DEPENDS_ON -> bcrypt # -# @INVARIANT: Uses bcrypt for hashing with standard work factor. # [SECTION: IMPORTS] import bcrypt # [/SECTION] -# [DEF:verify_password:Function] -# @PURPOSE: Verifies a plain password against a hashed password. -# @PRE: plain_password is a string, hashed_password is a bcrypt hash. -# @POST: Returns True if password matches, False otherwise. -# @RELATION: DEPENDS_ON -> bcrypt +# #region verify_password [TYPE Function] +# @BRIEF Verifies a plain password against a hashed password. +# @PRE plain_password is a string, hashed_password is a bcrypt hash. +# @POST Returns True if password matches, False otherwise. +# @RELATION DEPENDS_ON -> [bcrypt] # # @PARAM: plain_password (str) - The unhashed password. # @PARAM: hashed_password (str) - The stored hash. @@ -31,18 +29,18 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: ) except Exception: return False -# [/DEF:verify_password:Function] +# #endregion verify_password -# [DEF:get_password_hash:Function] -# @PURPOSE: Generates a bcrypt hash for a plain password. -# @PRE: password is a string. -# @POST: Returns a secure bcrypt hash string. -# @RELATION: DEPENDS_ON -> bcrypt +# #region get_password_hash [TYPE Function] +# @BRIEF Generates a bcrypt hash for a plain password. +# @PRE password is a string. +# @POST Returns a secure bcrypt hash string. +# @RELATION DEPENDS_ON -> [bcrypt] # # @PARAM: password (str) - The password to hash. # @RETURN: str - The generated hash. def get_password_hash(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") -# [/DEF:get_password_hash:Function] +# #endregion get_password_hash -# [/DEF:AuthSecurityModule:Module] +# #endregion AuthSecurityModule diff --git a/backend/src/core/config_manager.py b/backend/src/core/config_manager.py index ba83089f..8ac92ddf 100644 --- a/backend/src/core/config_manager.py +++ b/backend/src/core/config_manager.py @@ -1,19 +1,17 @@ -# [DEF:ConfigManager:Module] +# #region ConfigManager [C:5] [TYPE Module] [SEMANTICS config, manager, persistence, migration, postgresql] +# @BRIEF Manages application configuration persistence in DB with one-time migration from legacy JSON. +# @LAYER Domain +# @PRE Database schema for AppConfigRecord must be initialized. +# @POST Configuration is loaded into memory and logger is configured. +# @SIDE_EFFECT Performs DB I/O and may update global logging level. +# @DATA_CONTRACT Input[json, record] -> Model[AppConfig] +# @INVARIANT Configuration must always be representable by AppConfig and persisted under global record id. +# @RELATION DEPENDS_ON -> [AppConfig] +# @RELATION DEPENDS_ON -> [SessionLocal] +# @RELATION DEPENDS_ON -> [AppConfigRecord] +# @RELATION CALLS -> [logger] +# @RELATION CALLS -> [configure_logger] # -# @COMPLEXITY: 5 -# @SEMANTICS: config, manager, persistence, migration, postgresql -# @PURPOSE: Manages application configuration persistence in DB with one-time migration from legacy JSON. -# @LAYER: Domain -# @PRE: Database schema for AppConfigRecord must be initialized. -# @POST: Configuration is loaded into memory and logger is configured. -# @SIDE_EFFECT: Performs DB I/O and may update global logging level. -# @DATA_CONTRACT: Input[json, record] -> Model[AppConfig] -# @INVARIANT: Configuration must always be representable by AppConfig and persisted under global record id. -# @RELATION: [DEPENDS_ON] ->[AppConfig] -# @RELATION: [DEPENDS_ON] ->[SessionLocal] -# @RELATION: [DEPENDS_ON] ->[AppConfigRecord] -# @RELATION: [CALLS] ->[logger] -# @RELATION: [CALLS] ->[configure_logger] # import json import os @@ -31,19 +29,18 @@ from .cot_logger import MarkerLogger log = MarkerLogger("ConfigManager") -# [DEF:ConfigManager:Class] -# @COMPLEXITY: 5 -# @PURPOSE: Handles application configuration load, validation, mutation, and persistence lifecycle. -# @PRE: Database is accessible and AppConfigRecord schema is loaded. -# @POST: Configuration state is synchronized between memory and database. -# @SIDE_EFFECT: Performs DB I/O, OS path validation, and logger reconfiguration. +# #region ConfigManager [C:5] [TYPE Class] +# @BRIEF Handles application configuration load, validation, mutation, and persistence lifecycle. +# @PRE Database is accessible and AppConfigRecord schema is loaded. +# @POST Configuration state is synchronized between memory and database. +# @SIDE_EFFECT Performs DB I/O, OS path validation, and logger reconfiguration. class ConfigManager: - # [DEF:__init__:Function] - # @PURPOSE: Initialize manager state from persisted or migrated configuration. - # @PRE: config_path is a non-empty string path. - # @POST: self.config is initialized as AppConfig and logger is configured. - # @SIDE_EFFECT: Reads config sources and updates logging configuration. - # @DATA_CONTRACT: Input(str config_path) -> Output(None; self.config: AppConfig) + # #region __init__ [TYPE Function] + # @BRIEF Initialize manager state from persisted or migrated configuration. + # @PRE config_path is a non-empty string path. + # @POST self.config is initialized as AppConfig and logger is configured. + # @SIDE_EFFECT Reads config sources and updates logging configuration. + # @DATA_CONTRACT Input(str config_path) -> Output(None; self.config: AppConfig) def __init__(self, config_path: str = "config.json"): with belief_scope("ConfigManager.__init__"): if not isinstance(config_path, str) or not config_path: @@ -64,12 +61,12 @@ class ConfigManager: log.reflect("ConfigManager initialization complete") - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:_apply_features_from_env:Function] - # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config. - # @SIDE_EFFECT: Reads os.environ; mutates settings.features in-place. - # @RATIONALE: Env vars seed the initial defaults. After first bootstrap, DB is source of truth. + # #region _apply_features_from_env [TYPE Function] + # @BRIEF Read FEATURES__* env vars and apply them to a GlobalSettings features config. + # @SIDE_EFFECT Reads os.environ; mutates settings.features in-place. + # @RATIONALE Env vars seed the initial defaults. After first bootstrap, DB is source of truth. @staticmethod def _apply_features_from_env(settings: GlobalSettings) -> None: with belief_scope("ConfigManager._apply_features_from_env"): @@ -91,10 +88,10 @@ class ConfigManager: payload={"value": parsed, "raw": health_monitor_env}, ) - # [/DEF:_apply_features_from_env:Function] + # #endregion _apply_features_from_env - # [DEF:_default_config:Function] - # @PURPOSE: Build default application configuration fallback. + # #region _default_config [TYPE Function] + # @BRIEF Build default application configuration fallback. def _default_config(self) -> AppConfig: with belief_scope("ConfigManager._default_config"): log.reason("Building default AppConfig fallback") @@ -102,10 +99,10 @@ class ConfigManager: self._apply_features_from_env(config.settings) return config - # [/DEF:_default_config:Function] + # #endregion _default_config - # [DEF:_sync_raw_payload_from_config:Function] - # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections. + # #region _sync_raw_payload_from_config [TYPE Function] + # @BRIEF Merge typed AppConfig state into raw payload while preserving unsupported legacy sections. def _sync_raw_payload_from_config(self) -> dict[str, Any]: with belief_scope("ConfigManager._sync_raw_payload_from_config"): typed_payload = self.config.model_dump() @@ -129,10 +126,10 @@ class ConfigManager: ) return merged_payload - # [/DEF:_sync_raw_payload_from_config:Function] + # #endregion _sync_raw_payload_from_config - # [DEF:_load_from_legacy_file:Function] - # @PURPOSE: Load legacy JSON configuration for migration fallback path. + # #region _load_from_legacy_file [TYPE Function] + # @BRIEF Load legacy JSON configuration for migration fallback path. def _load_from_legacy_file(self) -> dict[str, Any]: with belief_scope("ConfigManager._load_from_legacy_file"): if not self.config_path.exists(): @@ -161,10 +158,10 @@ class ConfigManager: ) return payload - # [/DEF:_load_from_legacy_file:Function] + # #endregion _load_from_legacy_file - # [DEF:_get_record:Function] - # @PURPOSE: Resolve global configuration record from DB. + # #region _get_record [TYPE Function] + # @BRIEF Resolve global configuration record from DB. def _get_record(self, session: Session) -> Optional[AppConfigRecord]: with belief_scope("ConfigManager._get_record"): record = ( @@ -177,10 +174,10 @@ class ConfigManager: ) return record - # [/DEF:_get_record:Function] + # #endregion _get_record - # [DEF:_load_config:Function] - # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON. + # #region _load_config [TYPE Function] + # @BRIEF Load configuration from DB or perform one-time migration from legacy JSON. def _load_config(self) -> AppConfig: with belief_scope("ConfigManager._load_config"): session = SessionLocal() @@ -252,10 +249,10 @@ class ConfigManager: finally: session.close() - # [/DEF:_load_config:Function] + # #endregion _load_config - # [DEF:_sync_environment_records:Function] - # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models. + # #region _sync_environment_records [TYPE Function] + # @BRIEF Mirror configured environments into the relational environments table used by FK-backed domain models. def _sync_environment_records(self, session: Session, config: AppConfig) -> None: with belief_scope("ConfigManager._sync_environment_records"): configured_envs = list(config.environments or []) @@ -300,10 +297,10 @@ class ConfigManager: record.url = normalized_url record.credentials_id = credentials_id - # [/DEF:_sync_environment_records:Function] + # #endregion _sync_environment_records - # [DEF:_save_config_to_db:Function] - # @PURPOSE: Persist provided AppConfig into the global DB configuration record. + # #region _save_config_to_db [TYPE Function] + # @BRIEF Persist provided AppConfig into the global DB configuration record. def _save_config_to_db( self, config: AppConfig, session: Optional[Session] = None ) -> None: @@ -345,35 +342,35 @@ class ConfigManager: if owns_session: db.close() - # [/DEF:_save_config_to_db:Function] + # #endregion _save_config_to_db - # [DEF:save:Function] - # @PURPOSE: Persist current in-memory configuration state. + # #region save [TYPE Function] + # @BRIEF Persist current in-memory configuration state. def save(self) -> None: with belief_scope("ConfigManager.save"): log.reason("Persisting current in-memory configuration") self._save_config_to_db(self.config) - # [/DEF:save:Function] + # #endregion save - # [DEF:get_config:Function] - # @PURPOSE: Return current in-memory configuration snapshot. + # #region get_config [TYPE Function] + # @BRIEF Return current in-memory configuration snapshot. def get_config(self) -> AppConfig: with belief_scope("ConfigManager.get_config"): return self.config - # [/DEF:get_config:Function] + # #endregion get_config - # [DEF:get_payload:Function] - # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema. + # #region get_payload [TYPE Function] + # @BRIEF Return full persisted payload including sections outside typed AppConfig schema. def get_payload(self) -> dict[str, Any]: with belief_scope("ConfigManager.get_payload"): return self._sync_raw_payload_from_config() - # [/DEF:get_payload:Function] + # #endregion get_payload - # [DEF:save_config:Function] - # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict. + # #region save_config [TYPE Function] + # @BRIEF Persist configuration provided either as typed AppConfig or raw payload dict. def save_config(self, config: Any) -> AppConfig: with belief_scope("ConfigManager.save_config"): if isinstance(config, AppConfig): @@ -402,10 +399,10 @@ class ConfigManager: log.explore("Unsupported config type supplied to save_config", error=f"Expected AppConfig or dict, got {type(config).__name__}", payload={"type": type(config).__name__}) raise TypeError("config must be AppConfig or dict") - # [/DEF:save_config:Function] + # #endregion save_config - # [DEF:update_global_settings:Function] - # @PURPOSE: Replace global settings and persist the resulting configuration. + # #region update_global_settings [TYPE Function] + # @BRIEF Replace global settings and persist the resulting configuration. def update_global_settings(self, settings: GlobalSettings) -> AppConfig: with belief_scope("ConfigManager.update_global_settings"): log.reason("Updating global settings") @@ -413,10 +410,10 @@ class ConfigManager: self.save() return self.config - # [/DEF:update_global_settings:Function] + # #endregion update_global_settings - # [DEF:validate_path:Function] - # @PURPOSE: Validate that path exists and is writable, creating it when absent. + # #region validate_path [TYPE Function] + # @BRIEF Validate that path exists and is writable, creating it when absent. def validate_path(self, path: str) -> tuple[bool, str]: with belief_scope("ConfigManager.validate_path", f"path={path}"): try: @@ -440,26 +437,26 @@ class ConfigManager: log.explore("Path validation failed", error=str(exc), payload={"path": path}) return False, str(exc) - # [/DEF:validate_path:Function] + # #endregion validate_path - # [DEF:get_environments:Function] - # @PURPOSE: Return all configured environments. + # #region get_environments [TYPE Function] + # @BRIEF Return all configured environments. def get_environments(self) -> List[Environment]: with belief_scope("ConfigManager.get_environments"): return list(self.config.environments) - # [/DEF:get_environments:Function] + # #endregion get_environments - # [DEF:has_environments:Function] - # @PURPOSE: Check whether at least one environment exists in configuration. + # #region has_environments [TYPE Function] + # @BRIEF Check whether at least one environment exists in configuration. def has_environments(self) -> bool: with belief_scope("ConfigManager.has_environments"): return len(self.config.environments) > 0 - # [/DEF:has_environments:Function] + # #endregion has_environments - # [DEF:get_environment:Function] - # @PURPOSE: Resolve a configured environment by identifier. + # #region get_environment [TYPE Function] + # @BRIEF Resolve a configured environment by identifier. def get_environment(self, env_id: str) -> Optional[Environment]: with belief_scope("ConfigManager.get_environment", f"env_id={env_id}"): normalized = str(env_id or "").strip() @@ -471,10 +468,10 @@ class ConfigManager: return env return None - # [/DEF:get_environment:Function] + # #endregion get_environment - # [DEF:add_environment:Function] - # @PURPOSE: Upsert environment by id into configuration and persist. + # #region add_environment [TYPE Function] + # @BRIEF Upsert environment by id into configuration and persist. def add_environment(self, env: Environment) -> AppConfig: with belief_scope("ConfigManager.add_environment", f"env_id={env.id}"): existing_index = next( @@ -507,10 +504,10 @@ class ConfigManager: self.save() return self.config - # [/DEF:add_environment:Function] + # #endregion add_environment - # [DEF:update_environment:Function] - # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior. + # #region update_environment [TYPE Function] + # @BRIEF Update existing environment by id and preserve masked password placeholder behavior. def update_environment(self, env_id: str, env: Environment) -> bool: with belief_scope("ConfigManager.update_environment", f"env_id={env_id}"): for index, existing in enumerate(self.config.environments): @@ -537,10 +534,10 @@ class ConfigManager: log.explore("Environment update skipped; env not found", error=f"No matching environment found for id: {env_id}", payload={"env_id": env_id}) return False - # [/DEF:update_environment:Function] + # #endregion update_environment - # [DEF:delete_environment:Function] - # @PURPOSE: Delete environment by id and persist when deletion occurs. + # #region delete_environment [TYPE Function] + # @BRIEF Delete environment by id and persist when deletion occurs. def delete_environment(self, env_id: str) -> bool: with belief_scope("ConfigManager.delete_environment", f"env_id={env_id}"): before = len(self.config.environments) @@ -569,8 +566,8 @@ class ConfigManager: self.save() return True - # [/DEF:delete_environment:Function] + # #endregion delete_environment -# [/DEF:ConfigManager:Class] -# [/DEF:ConfigManager:Module] +# #endregion ConfigManager +# #endregion ConfigManager diff --git a/backend/src/core/config_models.py b/backend/src/core/config_models.py index 49d7c0b3..32b7c9f7 100755 --- a/backend/src/core/config_models.py +++ b/backend/src/core/config_models.py @@ -1,10 +1,8 @@ -# [DEF:ConfigModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: config, models, pydantic -# @PURPOSE: Defines the data models for application configuration using Pydantic. -# @LAYER: Core -# @RELATION: IMPLEMENTS -> [CoreContracts] -# @RELATION: IMPLEMENTS -> [ConnectionContracts] +# #region ConfigModels [C:3] [TYPE Module] [SEMANTICS config, models, pydantic] +# @BRIEF Defines the data models for application configuration using Pydantic. +# @LAYER Core +# @RELATION IMPLEMENTS -> [CoreContracts] +# @RELATION IMPLEMENTS -> [ConnectionContracts] from pydantic import BaseModel, Field from typing import List, Optional @@ -16,18 +14,18 @@ from ..services.llm_prompt_templates import ( ) -# [DEF:Schedule:DataClass] -# @PURPOSE: Represents a backup schedule configuration. +# #region Schedule [TYPE DataClass] +# @BRIEF Represents a backup schedule configuration. class Schedule(BaseModel): enabled: bool = False cron_expression: str = "0 0 * * *" # Default: daily at midnight -# [/DEF:Schedule:DataClass] +# #endregion Schedule -# [DEF:Environment:DataClass] -# @PURPOSE: Represents a Superset environment configuration. +# #region Environment [TYPE DataClass] +# @BRIEF Represents a Superset environment configuration. class Environment(BaseModel): id: str name: str @@ -42,11 +40,11 @@ class Environment(BaseModel): backup_schedule: Schedule = Field(default_factory=Schedule) -# [/DEF:Environment:DataClass] +# #endregion Environment -# [DEF:LoggingConfig:DataClass] -# @PURPOSE: Defines the configuration for the application's logging system. +# #region LoggingConfig [TYPE DataClass] +# @BRIEF Defines the configuration for the application's logging system. class LoggingConfig(BaseModel): level: str = "INFO" task_log_level: str = ( @@ -58,34 +56,33 @@ class LoggingConfig(BaseModel): enable_belief_state: bool = True -# [/DEF:LoggingConfig:DataClass] +# #endregion LoggingConfig -# [DEF:CleanReleaseConfig:DataClass] -# @PURPOSE: Configuration for clean release compliance subsystem. +# #region CleanReleaseConfig [TYPE DataClass] +# @BRIEF Configuration for clean release compliance subsystem. class CleanReleaseConfig(BaseModel): active_policy_id: Optional[str] = None active_registry_id: Optional[str] = None -# [/DEF:CleanReleaseConfig:DataClass] +# #endregion CleanReleaseConfig -# [DEF:FeaturesConfig:DataClass] -# @COMPLEXITY: 1 -# @PURPOSE: Top-level feature flags that toggle entire project features on/off. -# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB. +# #region FeaturesConfig [C:1] [TYPE DataClass] +# @BRIEF Top-level feature flags that toggle entire project features on/off. +# @RATIONALE Features are read from environment variables on bootstrap and persisted in DB. # DB is source of truth after initial bootstrap; env vars only seed defaults. class FeaturesConfig(BaseModel): dataset_review: bool = True health_monitor: bool = True -# [/DEF:FeaturesConfig:DataClass] +# #endregion FeaturesConfig -# [DEF:GlobalSettings:DataClass] -# @PURPOSE: Represents global application settings. +# #region GlobalSettings [TYPE DataClass] +# @BRIEF Represents global application settings. class GlobalSettings(BaseModel): storage: StorageConfig = Field(default_factory=StorageConfig) clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig) @@ -117,16 +114,16 @@ class GlobalSettings(BaseModel): ff_dataset_execution: bool = True -# [/DEF:GlobalSettings:DataClass] +# #endregion GlobalSettings -# [DEF:AppConfig:DataClass] -# @PURPOSE: The root configuration model containing all application settings. +# #region AppConfig [TYPE DataClass] +# @BRIEF The root configuration model containing all application settings. class AppConfig(BaseModel): environments: List[Environment] = [] settings: GlobalSettings -# [/DEF:AppConfig:DataClass] +# #endregion AppConfig -# [/DEF:ConfigModels:Module] +# #endregion ConfigModels diff --git a/backend/src/core/cot_logger.py b/backend/src/core/cot_logger.py index 26a9e174..f168075d 100644 --- a/backend/src/core/cot_logger.py +++ b/backend/src/core/cot_logger.py @@ -1,7 +1,5 @@ -# [DEF:CotLoggerModule:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: logging, cot, molecular, structured, json, contextvar -# @PURPOSE: Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol. +# #region CotLoggerModule [C:4] [TYPE Module] [SEMANTICS logging, cot, molecular, structured, json, contextvar] +# @BRIEF Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol. # Uses ContextVar for trace_id and span_id propagation across async contexts. # Provides log(), MarkerLogger, seed_trace_id(), push_span(), pop_span(). # @LAYER: Core @@ -19,20 +17,16 @@ from contextvars import ContextVar from datetime import datetime, timezone from typing import Any, Dict, Optional -# [DEF:cot_trace_context:Data] -# @COMPLEXITY: 1 -# @SEMANTICS: contextvar, trace_id, span_id, propagation -# @PURPOSE: ContextVars for trace ID and span ID propagation across async boundaries. +# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation] +# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries. _trace_id: ContextVar[str] = ContextVar("_trace_id", default="") _span_id: ContextVar[str] = ContextVar("_span_id", default="") -# [/DEF:cot_trace_context:Data] +# #endregion cot_trace_context -# [DEF:cot_logger_instance:Data] -# @COMPLEXITY: 1 -# @SEMANTICS: logger, instance -# @PURPOSE: Dedicated Python logger for all CoT (molecular) log output. +# #region cot_logger_instance [C:1] [TYPE Data] [SEMANTICS logger, instance] +# @BRIEF Dedicated Python logger for all CoT (molecular) log output. cot_logger = logging.getLogger("cot") -# [/DEF:cot_logger_instance:Data] +# #endregion cot_logger_instance __all__ = [ "seed_trace_id", @@ -44,10 +38,8 @@ __all__ = [ "MarkerLogger", ] -# [DEF:seed_trace_id:Function] -# @COMPLEXITY: 1 -# @SEMANTICS: trace_id, uuid, contextvar, set -# @BRIEF: Generate a new UUID4 trace_id, set it in ContextVar, and return it. +# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set] +# @BRIEF Generate a new UUID4 trace_id, set it in ContextVar, and return it. def seed_trace_id() -> str: """Generate a new UUID4 trace ID and store it in the thread-local ContextVar. @@ -59,12 +51,10 @@ def seed_trace_id() -> str: return trace_id -# [/DEF:seed_trace_id:Function] +# #endregion seed_trace_id -# [DEF:set_trace_id:Function] -# @COMPLEXITY: 1 -# @SEMANTICS: trace_id, contextvar, set, public -# @BRIEF: Set an explicit trace_id into the ContextVar (e.g. from an incoming header). +# #region set_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, set, public] +# @BRIEF Set an explicit trace_id into the ContextVar (e.g. from an incoming header). def set_trace_id(trace_id: str) -> None: """Set a specific trace_id into the ContextVar. @@ -77,12 +67,10 @@ def set_trace_id(trace_id: str) -> None: _trace_id.set(trace_id) -# [/DEF:set_trace_id:Function] +# #endregion set_trace_id -# [DEF:get_trace_id:Function] -# @COMPLEXITY: 1 -# @SEMANTICS: trace_id, contextvar, get, public -# @BRIEF: Get the current trace_id from ContextVar. +# #region get_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, get, public] +# @BRIEF Get the current trace_id from ContextVar. def get_trace_id() -> str: """Get the current trace_id from the thread-local ContextVar. @@ -92,12 +80,10 @@ def get_trace_id() -> str: return _trace_id.get() -# [/DEF:get_trace_id:Function] +# #endregion get_trace_id -# [DEF:push_span:Function] -# @COMPLEXITY: 1 -# @SEMANTICS: span_id, contextvar, stack -# @BRIEF: Set a new span_id in ContextVar and return the previous span_id for later restoration. +# #region push_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, stack] +# @BRIEF Set a new span_id in ContextVar and return the previous span_id for later restoration. def push_span(span: str) -> str: """Push a new span ID onto the context and return the previous span ID. @@ -112,12 +98,10 @@ def push_span(span: str) -> str: return prev -# [/DEF:push_span:Function] +# #endregion push_span -# [DEF:pop_span:Function] -# @COMPLEXITY: 1 -# @SEMANTICS: span_id, contextvar, restore -# @BRIEF: Restore a previous span_id into the ContextVar. +# #region pop_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, restore] +# @BRIEF Restore a previous span_id into the ContextVar. def pop_span(prev: str) -> None: """Restore a previous span ID into the ContextVar. @@ -127,12 +111,10 @@ def pop_span(prev: str) -> None: _span_id.set(prev) -# [/DEF:pop_span:Function] +# #endregion pop_span -# [DEF:cot_log_function:Function] -# @COMPLEXITY: 2 -# @SEMANTICS: log, json, structured, marker -# @BRIEF: Core structured logging function that emits a single-line JSON record. +# #region cot_log_function [C:2] [TYPE Function] [SEMANTICS log, json, structured, marker] +# @BRIEF Core structured logging function that emits a single-line JSON record. def log( src: str, marker: str, @@ -182,12 +164,10 @@ def log( log_func(intent, extra=extra) -# [/DEF:cot_log_function:Function] +# #endregion cot_log_function -# [DEF:MarkerLogger:Class] -# @COMPLEXITY: 2 -# @SEMANTICS: logger, proxy, marker, syntactic-sugar -# @BRIEF: Thin proxy class providing .reason(), .reflect(), .explore() convenience methods. +# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar] +# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods. class MarkerLogger: """Thin proxy over the cot_logger.log() function. @@ -201,8 +181,8 @@ class MarkerLogger: Each method delegates to :func:`log` with the corresponding marker. """ - # [DEF:MarkerLogger.__init__:Function] - # @BRIEF: Store the module/component name used as the 'src' field in all log calls. + # #region MarkerLogger.__init__ [TYPE Function] + # @BRIEF Store the module/component name used as the 'src' field in all log calls. def __init__(self, module_name: str) -> None: """Initialise the MarkerLogger with a module or component name. @@ -211,30 +191,30 @@ class MarkerLogger: """ self._module_name = module_name - # [/DEF:MarkerLogger.__init__:Function] + # #endregion MarkerLogger.__init__ - # [DEF:MarkerLogger.reason:Function] - # @BRIEF: Log a REASON marker — strict deduction, core logic. + # #region MarkerLogger.reason [TYPE Function] + # @BRIEF Log a REASON marker — strict deduction, core logic. def reason( self, intent: str, *, payload: Optional[Dict[str, Any]] = None ) -> None: """Log a REASON marker (INFO level).""" log(self._module_name, "REASON", intent, payload=payload) - # [/DEF:MarkerLogger.reason:Function] + # #endregion MarkerLogger.reason - # [DEF:MarkerLogger.reflect:Function] - # @BRIEF: Log a REFLECT marker — self-check, structural validation. + # #region MarkerLogger.reflect [TYPE Function] + # @BRIEF Log a REFLECT marker — self-check, structural validation. def reflect( self, intent: str, *, payload: Optional[Dict[str, Any]] = None ) -> None: """Log a REFLECT marker (INFO level).""" log(self._module_name, "REFLECT", intent, payload=payload) - # [/DEF:MarkerLogger.reflect:Function] + # #endregion MarkerLogger.reflect - # [DEF:MarkerLogger.explore:Function] - # @BRIEF: Log an EXPLORE marker — searching, alternatives, violated assumptions. + # #region MarkerLogger.explore [TYPE Function] + # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions. def explore( self, intent: str, @@ -251,8 +231,8 @@ class MarkerLogger: """ log(self._module_name, "EXPLORE", intent, payload=payload, error=error) - # [/DEF:MarkerLogger.explore:Function] + # #endregion MarkerLogger.explore -# [/DEF:MarkerLogger:Class] -# [/DEF:CotLoggerModule:Module] +# #endregion MarkerLogger +# #endregion CotLoggerModule diff --git a/backend/src/core/database.py b/backend/src/core/database.py index 9b469431..fc6d5aad 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -1,14 +1,12 @@ -# [DEF:DatabaseModule:Module] +# #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS database, postgresql, sqlalchemy, session, persistence] +# @BRIEF Configures database connection and session management (PostgreSQL-first). +# @LAYER Core +# @INVARIANT A single engine instance is used for the entire application. +# @RELATION DEPENDS_ON -> [MappingModels] +# @RELATION DEPENDS_ON -> [auth_config] +# @RELATION DEPENDS_ON -> [ConnectionConfig] # -# @COMPLEXITY: 3 -# @SEMANTICS: database, postgresql, sqlalchemy, session, persistence -# @PURPOSE: Configures database connection and session management (PostgreSQL-first). -# @LAYER: Core -# @RELATION: [DEPENDS_ON] ->[MappingModels] -# @RELATION: [DEPENDS_ON] ->[auth_config] -# @RELATION: [DEPENDS_ON] ->[ConnectionConfig] # -# @INVARIANT: A single engine instance is used for the entire application. # [SECTION: IMPORTS] from sqlalchemy import create_engine, inspect, text @@ -32,40 +30,35 @@ import os from pathlib import Path # [/SECTION] -# [DEF:BASE_DIR:Variable] -# @COMPLEXITY: 1 -# @PURPOSE: Base directory for the backend. +# #region BASE_DIR [C:1] [TYPE Variable] +# @BRIEF Base directory for the backend. BASE_DIR = Path(__file__).resolve().parent.parent.parent -# [/DEF:BASE_DIR:Variable] +# #endregion BASE_DIR -# [DEF:DATABASE_URL:Constant] -# @COMPLEXITY: 1 -# @PURPOSE: URL for the main application database. +# #region DATABASE_URL [C:1] [TYPE Constant] +# @BRIEF URL for the main application database. DEFAULT_POSTGRES_URL = os.getenv( "POSTGRES_URL", "postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools", ) DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_POSTGRES_URL) -# [/DEF:DATABASE_URL:Constant] +# #endregion DATABASE_URL -# [DEF:TASKS_DATABASE_URL:Constant] -# @COMPLEXITY: 1 -# @PURPOSE: URL for the tasks execution database. +# #region TASKS_DATABASE_URL [C:1] [TYPE Constant] +# @BRIEF URL for the tasks execution database. # Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance. TASKS_DATABASE_URL = os.getenv("TASKS_DATABASE_URL", DATABASE_URL) -# [/DEF:TASKS_DATABASE_URL:Constant] +# #endregion TASKS_DATABASE_URL -# [DEF:AUTH_DATABASE_URL:Constant] -# @COMPLEXITY: 1 -# @PURPOSE: URL for the authentication database. +# #region AUTH_DATABASE_URL [C:1] [TYPE Constant] +# @BRIEF URL for the authentication database. AUTH_DATABASE_URL = os.getenv("AUTH_DATABASE_URL", auth_config.AUTH_DATABASE_URL) -# [/DEF:AUTH_DATABASE_URL:Constant] +# #endregion AUTH_DATABASE_URL -# [DEF:engine:Variable] -# @COMPLEXITY: 1 -# @PURPOSE: SQLAlchemy engine for mappings database. -# @SIDE_EFFECT: Creates database engine and manages connection pool. +# #region engine [C:1] [TYPE Variable] +# @BRIEF SQLAlchemy engine for mappings database. +# @SIDE_EFFECT Creates database engine and manages connection pool. def _build_engine(db_url: str): with belief_scope("_build_engine"): if db_url.startswith("sqlite"): @@ -74,48 +67,42 @@ def _build_engine(db_url: str): engine = _build_engine(DATABASE_URL) -# [/DEF:engine:Variable] +# #endregion engine -# [DEF:tasks_engine:Variable] -# @COMPLEXITY: 1 -# @PURPOSE: SQLAlchemy engine for tasks database. +# #region tasks_engine [C:1] [TYPE Variable] +# @BRIEF SQLAlchemy engine for tasks database. tasks_engine = _build_engine(TASKS_DATABASE_URL) -# [/DEF:tasks_engine:Variable] +# #endregion tasks_engine -# [DEF:auth_engine:Variable] -# @COMPLEXITY: 1 -# @PURPOSE: SQLAlchemy engine for authentication database. +# #region auth_engine [C:1] [TYPE Variable] +# @BRIEF SQLAlchemy engine for authentication database. auth_engine = _build_engine(AUTH_DATABASE_URL) -# [/DEF:auth_engine:Variable] +# #endregion auth_engine -# [DEF:SessionLocal:Class] -# @COMPLEXITY: 1 -# @PURPOSE: A session factory for the main mappings database. -# @PRE: engine is initialized. +# #region SessionLocal [C:1] [TYPE Class] +# @BRIEF A session factory for the main mappings database. +# @PRE engine is initialized. SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -# [/DEF:SessionLocal:Class] +# #endregion SessionLocal -# [DEF:TasksSessionLocal:Class] -# @COMPLEXITY: 1 -# @PURPOSE: A session factory for the tasks execution database. -# @PRE: tasks_engine is initialized. +# #region TasksSessionLocal [C:1] [TYPE Class] +# @BRIEF A session factory for the tasks execution database. +# @PRE tasks_engine is initialized. TasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine) -# [/DEF:TasksSessionLocal:Class] +# #endregion TasksSessionLocal -# [DEF:AuthSessionLocal:Class] -# @COMPLEXITY: 1 -# @PURPOSE: A session factory for the authentication database. -# @PRE: auth_engine is initialized. +# #region AuthSessionLocal [C:1] [TYPE Class] +# @BRIEF A session factory for the authentication database. +# @PRE auth_engine is initialized. AuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine) -# [/DEF:AuthSessionLocal:Class] +# #endregion AuthSessionLocal -# [DEF:_ensure_user_dashboard_preferences_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table. -# @PRE: bind_engine points to application database where profile table is stored. -# @POST: Missing columns are added without data loss. -# @RELATION: [DEPENDS_ON] ->[engine] +# #region _ensure_user_dashboard_preferences_columns [C:3] [TYPE Function] +# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table. +# @PRE bind_engine points to application database where profile table is stored. +# @POST Missing columns are added without data loss. +# @RELATION DEPENDS_ON -> [engine] def _ensure_user_dashboard_preferences_columns(bind_engine): with belief_scope("_ensure_user_dashboard_preferences_columns"): table_name = "user_dashboard_preferences" @@ -177,13 +164,12 @@ def _ensure_user_dashboard_preferences_columns(bind_engine): ) -# [/DEF:_ensure_user_dashboard_preferences_columns:Function] +# #endregion _ensure_user_dashboard_preferences_columns -# [DEF:_ensure_user_dashboard_preferences_health_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table (health fields). -# @RELATION: [DEPENDS_ON] ->[engine] +# #region _ensure_user_dashboard_preferences_health_columns [C:3] [TYPE Function] +# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table (health fields). +# @RELATION DEPENDS_ON -> [engine] def _ensure_user_dashboard_preferences_health_columns(bind_engine): with belief_scope("_ensure_user_dashboard_preferences_health_columns"): table_name = "user_dashboard_preferences" @@ -224,13 +210,12 @@ def _ensure_user_dashboard_preferences_health_columns(bind_engine): ) -# [/DEF:_ensure_user_dashboard_preferences_health_columns:Function] +# #endregion _ensure_user_dashboard_preferences_health_columns -# [DEF:_ensure_llm_validation_results_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Applies additive schema upgrades for llm_validation_results table. -# @RELATION: [DEPENDS_ON] ->[engine] +# #region _ensure_llm_validation_results_columns [C:3] [TYPE Function] +# @BRIEF Applies additive schema upgrades for llm_validation_results table. +# @RELATION DEPENDS_ON -> [engine] def _ensure_llm_validation_results_columns(bind_engine): with belief_scope("_ensure_llm_validation_results_columns"): table_name = "llm_validation_results" @@ -267,15 +252,14 @@ def _ensure_llm_validation_results_columns(bind_engine): ) -# [/DEF:_ensure_llm_validation_results_columns:Function] +# #endregion _ensure_llm_validation_results_columns -# [DEF:_ensure_git_server_configs_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Applies additive schema upgrades for git_server_configs table. -# @PRE: bind_engine points to application database. -# @POST: Missing columns are added without data loss. -# @RELATION: [DEPENDS_ON] ->[engine] +# #region _ensure_git_server_configs_columns [C:3] [TYPE Function] +# @BRIEF Applies additive schema upgrades for git_server_configs table. +# @PRE bind_engine points to application database. +# @POST Missing columns are added without data loss. +# @RELATION DEPENDS_ON -> [engine] def _ensure_git_server_configs_columns(bind_engine): with belief_scope("_ensure_git_server_configs_columns"): table_name = "git_server_configs" @@ -308,15 +292,14 @@ def _ensure_git_server_configs_columns(bind_engine): ) -# [/DEF:_ensure_git_server_configs_columns:Function] +# #endregion _ensure_git_server_configs_columns -# [DEF:_ensure_auth_users_columns:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Applies additive schema upgrades for auth users table. -# @PRE: bind_engine points to authentication database. -# @POST: Missing columns are added without data loss. -# @RELATION: [DEPENDS_ON] ->[auth_engine] +# #region _ensure_auth_users_columns [C:3] [TYPE Function] +# @BRIEF Applies additive schema upgrades for auth users table. +# @PRE bind_engine points to authentication database. +# @POST Missing columns are added without data loss. +# @RELATION DEPENDS_ON -> [auth_engine] def _ensure_auth_users_columns(bind_engine): with belief_scope("_ensure_auth_users_columns"): table_name = "users" @@ -371,15 +354,14 @@ def _ensure_auth_users_columns(bind_engine): raise -# [/DEF:_ensure_auth_users_columns:Function] +# #endregion _ensure_auth_users_columns -# [DEF:ensure_connection_configs_table:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Ensures the external connection registry table exists in the main database. -# @PRE: bind_engine points to the application database. -# @POST: connection_configs table exists without dropping existing data. -# @RELATION: [DEPENDS_ON] ->[ConnectionConfig] +# #region ensure_connection_configs_table [C:3] [TYPE Function] +# @BRIEF Ensures the external connection registry table exists in the main database. +# @PRE bind_engine points to the application database. +# @POST connection_configs table exists without dropping existing data. +# @RELATION DEPENDS_ON -> [ConnectionConfig] def ensure_connection_configs_table(bind_engine): with belief_scope("ensure_connection_configs_table"): try: @@ -392,15 +374,14 @@ def ensure_connection_configs_table(bind_engine): raise -# [/DEF:ensure_connection_configs_table:Function] +# #endregion ensure_connection_configs_table -# [DEF:_ensure_filter_source_enum_values:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Adds missing FilterSource enum values to the PostgreSQL native filtersource type. -# @PRE: bind_engine points to application database with imported_filters table. -# @POST: New enum values are available without data loss. -# @RELATION: [DEPENDS_ON] ->[engine] +# #region _ensure_filter_source_enum_values [C:3] [TYPE Function] +# @BRIEF Adds missing FilterSource enum values to the PostgreSQL native filtersource type. +# @PRE bind_engine points to application database with imported_filters table. +# @POST New enum values are available without data loss. +# @RELATION DEPENDS_ON -> [engine] def _ensure_filter_source_enum_values(bind_engine): with belief_scope("_ensure_filter_source_enum_values"): try: @@ -464,17 +445,16 @@ def _ensure_filter_source_enum_values(bind_engine): ) -# [/DEF:_ensure_filter_source_enum_values:Function] +# #endregion _ensure_filter_source_enum_values -# [DEF:_ensure_dataset_review_session_columns:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics. -# @PRE: bind_engine points to the application database where dataset review tables are stored. -# @POST: Missing additive columns across legacy dataset review tables are created without removing existing data. -# @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database. -# @RELATION: [DEPENDS_ON] ->[DatasetReviewSession] -# @RELATION: [DEPENDS_ON] ->[ImportedFilter] +# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function] +# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics. +# @PRE bind_engine points to the application database where dataset review tables are stored. +# @POST Missing additive columns across legacy dataset review tables are created without removing existing data. +# @SIDE_EFFECT Executes ALTER TABLE statements against dataset review tables in the application database. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] +# @RELATION DEPENDS_ON -> [ImportedFilter] def _ensure_translation_jobs_columns(bind_engine): with belief_scope("_ensure_translation_jobs_columns"): table_name = "translation_jobs" @@ -526,11 +506,11 @@ def _ensure_translation_jobs_columns(bind_engine): raise -# [DEF:_ensure_translation_records_columns:Function] -# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records. -# @PRE: bind_engine points to the application database. -# @POST: source_data column exists on both tables. -# @SIDE_EFFECT: Executes ALTER TABLE statements. +# #region _ensure_translation_records_columns [TYPE Function] +# @BRIEF Add source_data JSON column to translation_records and translation_preview_records. +# @PRE bind_engine points to the application database. +# @POST source_data column exists on both tables. +# @SIDE_EFFECT Executes ALTER TABLE statements. def _ensure_translation_records_columns(bind_engine): with belief_scope("_ensure_translation_records_columns"): migrations = [ @@ -560,7 +540,7 @@ def _ensure_translation_records_columns(bind_engine): extra={"error": str(migration_error)}, ) raise -# [/DEF:_ensure_translation_records_columns:Function] +# #endregion _ensure_translation_records_columns def _ensure_dataset_review_session_columns(bind_engine): @@ -636,18 +616,17 @@ def _ensure_dataset_review_session_columns(bind_engine): raise -# [/DEF:_ensure_dataset_review_session_columns:Function] +# #endregion _ensure_dataset_review_session_columns -# [DEF:init_db:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Initializes the database by creating all tables. -# @PRE: engine, tasks_engine and auth_engine are initialized. -# @POST: Database tables created in all databases. -# @SIDE_EFFECT: Creates physical database files if they don't exist. -# @RELATION: [CALLS] ->[ensure_connection_configs_table] -# @RELATION: [CALLS] ->[_ensure_filter_source_enum_values] -# @RELATION: [CALLS] ->[_ensure_dataset_review_session_columns] +# #region init_db [C:3] [TYPE Function] +# @BRIEF Initializes the database by creating all tables. +# @PRE engine, tasks_engine and auth_engine are initialized. +# @POST Database tables created in all databases. +# @SIDE_EFFECT Creates physical database files if they don't exist. +# @RELATION CALLS -> [ensure_connection_configs_table] +# @RELATION CALLS -> [_ensure_filter_source_enum_values] +# @RELATION CALLS -> [_ensure_dataset_review_session_columns] def init_db(): with belief_scope("init_db"): Base.metadata.create_all(bind=engine) @@ -665,16 +644,15 @@ def init_db(): _ensure_translation_records_columns(engine) -# [/DEF:init_db:Function] +# #endregion init_db -# [DEF:get_db:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Dependency for getting a database session. -# @PRE: SessionLocal is initialized. -# @POST: Session is closed after use. -# @RETURN: Generator[Session, None, None] -# @RELATION: [DEPENDS_ON] ->[SessionLocal] +# #region get_db [C:3] [TYPE Function] +# @BRIEF Dependency for getting a database session. +# @PRE SessionLocal is initialized. +# @POST Session is closed after use. +# @RETURN Generator[Session, None, None] +# @RELATION DEPENDS_ON -> [SessionLocal] def get_db(): with belief_scope("get_db"): db = SessionLocal() @@ -684,16 +662,15 @@ def get_db(): db.close() -# [/DEF:get_db:Function] +# #endregion get_db -# [DEF:get_tasks_db:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Dependency for getting a tasks database session. -# @PRE: TasksSessionLocal is initialized. -# @POST: Session is closed after use. -# @RETURN: Generator[Session, None, None] -# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal] +# #region get_tasks_db [C:3] [TYPE Function] +# @BRIEF Dependency for getting a tasks database session. +# @PRE TasksSessionLocal is initialized. +# @POST Session is closed after use. +# @RETURN Generator[Session, None, None] +# @RELATION DEPENDS_ON -> [TasksSessionLocal] def get_tasks_db(): with belief_scope("get_tasks_db"): db = TasksSessionLocal() @@ -703,17 +680,16 @@ def get_tasks_db(): db.close() -# [/DEF:get_tasks_db:Function] +# #endregion get_tasks_db -# [DEF:get_auth_db:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Dependency for getting an authentication database session. -# @PRE: AuthSessionLocal is initialized. -# @POST: Session is closed after use. -# @DATA_CONTRACT: None -> Output[sqlalchemy.orm.Session] -# @RETURN: Generator[Session, None, None] -# @RELATION: [DEPENDS_ON] ->[AuthSessionLocal] +# #region get_auth_db [C:3] [TYPE Function] +# @BRIEF Dependency for getting an authentication database session. +# @PRE AuthSessionLocal is initialized. +# @POST Session is closed after use. +# @DATA_CONTRACT None -> Output[sqlalchemy.orm.Session] +# @RETURN Generator[Session, None, None] +# @RELATION DEPENDS_ON -> [AuthSessionLocal] def get_auth_db(): with belief_scope("get_auth_db"): db = AuthSessionLocal() @@ -723,6 +699,6 @@ def get_auth_db(): db.close() -# [/DEF:get_auth_db:Function] +# #endregion get_auth_db -# [/DEF:DatabaseModule:Module] +# #endregion DatabaseModule diff --git a/backend/src/core/database.py.bak b/backend/src/core/database.py.bak new file mode 100644 index 00000000..e5d06ab8 --- /dev/null +++ b/backend/src/core/database.py.bak @@ -0,0 +1,737 @@ +# [DEF:DatabaseModule:Module] +# +# @COMPLEXITY: 3 +# @SEMANTICS: database, postgresql, sqlalchemy, session, persistence +# @PURPOSE: Configures database connection and session management (PostgreSQL-first). +# @LAYER: Core +# @RELATION: [DEPENDS_ON] ->[MappingModels] +# @RELATION: [DEPENDS_ON] ->[auth_config] +# @RELATION: [DEPENDS_ON] ->[ConnectionConfig] +# +# @INVARIANT: A single engine instance is used for the entire application. + +# [SECTION: IMPORTS] +from sqlalchemy import create_engine, inspect, text +from sqlalchemy.orm import sessionmaker +from ..models.mapping import Base +from ..models.connection import ConnectionConfig + +# Import models to ensure they're registered with Base +from ..models import task as _task_models # noqa: F401 +from ..models import auth as _auth_models # noqa: F401 +from ..models import config as _config_models # noqa: F401 +from ..models import llm as _llm_models # noqa: F401 +from ..models import assistant as _assistant_models # noqa: F401 +from ..models import profile as _profile_models # noqa: F401 +from ..models import clean_release as _clean_release_models # noqa: F401 +from ..models import connection as _connection_models # noqa: F401 +from ..models import dataset_review as _dataset_review_models # noqa: F401 +from .logger import belief_scope, logger +from .auth.config import auth_config +import os +from pathlib import Path +# [/SECTION] + +# [DEF:BASE_DIR:Variable] +# @COMPLEXITY: 1 +# @PURPOSE: Base directory for the backend. +BASE_DIR = Path(__file__).resolve().parent.parent.parent +# [/DEF:BASE_DIR:Variable] + +# [DEF:DATABASE_URL:Constant] +# @COMPLEXITY: 1 +# @PURPOSE: URL for the main application database. +DEFAULT_POSTGRES_URL = os.getenv( + "POSTGRES_URL", + "postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools", +) +DATABASE_URL = os.getenv("DATABASE_URL", DEFAULT_POSTGRES_URL) +# [/DEF:DATABASE_URL:Constant] + +# [DEF:TASKS_DATABASE_URL:Constant] +# @COMPLEXITY: 1 +# @PURPOSE: URL for the tasks execution database. +# Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance. +TASKS_DATABASE_URL = os.getenv("TASKS_DATABASE_URL", DATABASE_URL) +# [/DEF:TASKS_DATABASE_URL:Constant] + +# [DEF:AUTH_DATABASE_URL:Constant] +# @COMPLEXITY: 1 +# @PURPOSE: URL for the authentication database. +AUTH_DATABASE_URL = os.getenv("AUTH_DATABASE_URL", auth_config.AUTH_DATABASE_URL) +# [/DEF:AUTH_DATABASE_URL:Constant] + + +# [DEF:engine:Variable] +# @COMPLEXITY: 1 +# @PURPOSE: SQLAlchemy engine for mappings database. +# @SIDE_EFFECT: Creates database engine and manages connection pool. +def _build_engine(db_url: str): + with belief_scope("_build_engine"): + if db_url.startswith("sqlite"): + return create_engine(db_url, connect_args={"check_same_thread": False}) + return create_engine(db_url, pool_pre_ping=True) + + +engine = _build_engine(DATABASE_URL) +# [/DEF:engine:Variable] + +# [DEF:tasks_engine:Variable] +# @COMPLEXITY: 1 +# @PURPOSE: SQLAlchemy engine for tasks database. +tasks_engine = _build_engine(TASKS_DATABASE_URL) +# [/DEF:tasks_engine:Variable] + +# [DEF:auth_engine:Variable] +# @COMPLEXITY: 1 +# @PURPOSE: SQLAlchemy engine for authentication database. +auth_engine = _build_engine(AUTH_DATABASE_URL) +# [/DEF:auth_engine:Variable] + +# [DEF:SessionLocal:Class] +# @COMPLEXITY: 1 +# @PURPOSE: A session factory for the main mappings database. +# @PRE: engine is initialized. +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# [/DEF:SessionLocal:Class] + +# [DEF:TasksSessionLocal:Class] +# @COMPLEXITY: 1 +# @PURPOSE: A session factory for the tasks execution database. +# @PRE: tasks_engine is initialized. +TasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine) +# [/DEF:TasksSessionLocal:Class] + +# [DEF:AuthSessionLocal:Class] +# @COMPLEXITY: 1 +# @PURPOSE: A session factory for the authentication database. +# @PRE: auth_engine is initialized. +AuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine) +# [/DEF:AuthSessionLocal:Class] + + +# [DEF:_ensure_user_dashboard_preferences_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table. +# @PRE: bind_engine points to application database where profile table is stored. +# @POST: Missing columns are added without data loss. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_user_dashboard_preferences_columns(bind_engine): + with belief_scope("_ensure_user_dashboard_preferences_columns"): + table_name = "user_dashboard_preferences" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + alter_statements = [] + if "git_username" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR" + ) + if "git_email" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR" + ) + if "git_personal_access_token_encrypted" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences " + "ADD COLUMN git_personal_access_token_encrypted VARCHAR" + ) + if "start_page" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences " + "ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'" + ) + if "auto_open_task_drawer" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences " + "ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE" + ) + if "dashboards_table_density" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences " + "ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'" + ) + if "show_only_slug_dashboards" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences " + "ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE" + ) + + if not alter_statements: + return + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] Profile preference additive migration failed: %s", + migration_error, + ) + + +# [/DEF:_ensure_user_dashboard_preferences_columns:Function] + + +# [DEF:_ensure_user_dashboard_preferences_health_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Applies additive schema upgrades for user_dashboard_preferences table (health fields). +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_user_dashboard_preferences_health_columns(bind_engine): + with belief_scope("_ensure_user_dashboard_preferences_health_columns"): + table_name = "user_dashboard_preferences" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + alter_statements = [] + if "telegram_id" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR" + ) + if "email_address" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR" + ) + if "notify_on_fail" not in existing_columns: + alter_statements.append( + "ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE" + ) + + if not alter_statements: + return + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] Profile health preference additive migration failed: %s", + migration_error, + ) + + +# [/DEF:_ensure_user_dashboard_preferences_health_columns:Function] + + +# [DEF:_ensure_llm_validation_results_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Applies additive schema upgrades for llm_validation_results table. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_llm_validation_results_columns(bind_engine): + with belief_scope("_ensure_llm_validation_results_columns"): + table_name = "llm_validation_results" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + alter_statements = [] + if "task_id" not in existing_columns: + alter_statements.append( + "ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR" + ) + if "environment_id" not in existing_columns: + alter_statements.append( + "ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR" + ) + + if not alter_statements: + return + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] ValidationRecord additive migration failed: %s", + migration_error, + ) + + +# [/DEF:_ensure_llm_validation_results_columns:Function] + + +# [DEF:_ensure_git_server_configs_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Applies additive schema upgrades for git_server_configs table. +# @PRE: bind_engine points to application database. +# @POST: Missing columns are added without data loss. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_git_server_configs_columns(bind_engine): + with belief_scope("_ensure_git_server_configs_columns"): + table_name = "git_server_configs" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + alter_statements = [] + if "default_branch" not in existing_columns: + alter_statements.append( + "ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'" + ) + + if not alter_statements: + return + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] GitServerConfig preference additive migration failed: %s", + migration_error, + ) + + +# [/DEF:_ensure_git_server_configs_columns:Function] + + +# [DEF:_ensure_auth_users_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Applies additive schema upgrades for auth users table. +# @PRE: bind_engine points to authentication database. +# @POST: Missing columns are added without data loss. +# @RELATION: [DEPENDS_ON] ->[auth_engine] +def _ensure_auth_users_columns(bind_engine): + with belief_scope("_ensure_auth_users_columns"): + table_name = "users" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + alter_statements = [] + if "full_name" not in existing_columns: + alter_statements.append("ALTER TABLE users ADD COLUMN full_name VARCHAR") + if "is_ad_user" not in existing_columns: + alter_statements.append( + "ALTER TABLE users ADD COLUMN is_ad_user BOOLEAN NOT NULL DEFAULT FALSE" + ) + + if not alter_statements: + logger.reason( + "Auth users schema already up to date", + extra={"table": table_name, "columns": sorted(existing_columns)}, + ) + return + + logger.reason( + "Applying additive auth users schema migration", + extra={"table": table_name, "statements": alter_statements}, + ) + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + logger.reason( + "Auth users schema migration completed", + extra={ + "table": table_name, + "added_columns": [ + stmt.split(" ADD COLUMN ", 1)[1].split()[0] + for stmt in alter_statements + ], + }, + ) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] Auth users additive migration failed: %s", + migration_error, + ) + raise + + +# [/DEF:_ensure_auth_users_columns:Function] + + +# [DEF:ensure_connection_configs_table:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Ensures the external connection registry table exists in the main database. +# @PRE: bind_engine points to the application database. +# @POST: connection_configs table exists without dropping existing data. +# @RELATION: [DEPENDS_ON] ->[ConnectionConfig] +def ensure_connection_configs_table(bind_engine): + with belief_scope("ensure_connection_configs_table"): + try: + ConnectionConfig.__table__.create(bind=bind_engine, checkfirst=True) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] ConnectionConfig table ensure failed: %s", + migration_error, + ) + raise + + +# [/DEF:ensure_connection_configs_table:Function] + + +# [DEF:_ensure_filter_source_enum_values:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Adds missing FilterSource enum values to the PostgreSQL native filtersource type. +# @PRE: bind_engine points to application database with imported_filters table. +# @POST: New enum values are available without data loss. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_filter_source_enum_values(bind_engine): + with belief_scope("_ensure_filter_source_enum_values"): + try: + with bind_engine.connect() as connection: + # Check if the native enum type exists + result = connection.execute( + text( + "SELECT t.typname FROM pg_type t " + "JOIN pg_namespace n ON t.typnamespace = n.oid " + "WHERE t.typname = 'filtersource' AND n.nspname = 'public'" + ) + ) + if result.fetchone() is None: + logger.reason( + "filtersource enum type does not exist yet; skipping migration" + ) + return + + # Get existing enum values + result = connection.execute( + text( + "SELECT e.enumlabel FROM pg_enum e " + "JOIN pg_type t ON e.enumtypid = t.oid " + "WHERE t.typname = 'filtersource' " + "ORDER BY e.enumsortorder" + ) + ) + existing_values = {row[0] for row in result.fetchall()} + + required_values = ["SUPERSET_PERMALINK", "SUPERSET_NATIVE_FILTERS_KEY"] + missing_values = [ + v for v in required_values if v not in existing_values + ] + + if not missing_values: + logger.reason( + "filtersource enum already up to date", + extra={"existing": sorted(existing_values)}, + ) + return + + logger.reason( + "Adding missing values to filtersource enum", + extra={"missing": missing_values}, + ) + for value in missing_values: + connection.execute( + text( + f"ALTER TYPE filtersource ADD VALUE IF NOT EXISTS '{value}'" + ) + ) + connection.commit() + logger.reason( + "filtersource enum migration completed", + extra={"added": missing_values}, + ) + except Exception as migration_error: + logger.warning( + "[database][EXPLORE] FilterSource enum additive migration failed: %s", + migration_error, + ) + + +# [/DEF:_ensure_filter_source_enum_values:Function] + + +# [DEF:_ensure_translation_jobs_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Apply additive schema upgrades for translation_jobs table. +# @PRE: bind_engine points to the application database where translation_jobs table is stored. +# @POST: Missing additive columns (environment_id, target_database_id) across translation_jobs are created without removing existing data. +# @SIDE_EFFECT: Executes ALTER TABLE statements against translation_jobs table. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_translation_jobs_columns(bind_engine): + with belief_scope("_ensure_translation_jobs_columns"): + table_name = "translation_jobs" + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + return + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + + if "environment_id" not in existing_columns: + try: + with bind_engine.begin() as connection: + connection.execute( + text( + "ALTER TABLE translation_jobs " + "ADD COLUMN environment_id VARCHAR" + ) + ) + logger.reflect( + "Added environment_id column to translation_jobs", + ) + except Exception as migration_error: + logger.explore( + "Failed to add environment_id to translation_jobs", + extra={"error": str(migration_error)}, + ) + raise + + if "target_database_id" not in existing_columns: + try: + with bind_engine.begin() as connection: + connection.execute( + text( + "ALTER TABLE translation_jobs " + "ADD COLUMN target_database_id VARCHAR" + ) + ) + logger.reflect( + "Added target_database_id column to translation_jobs", + ) + except Exception as migration_error: + logger.explore( + "Failed to add target_database_id to translation_jobs", + extra={"error": str(migration_error)}, + ) + raise + +# [/DEF:_ensure_translation_jobs_columns:Function] + +# [DEF:_ensure_translation_records_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records. +# @PRE: bind_engine points to the application database. +# @POST: source_data column exists on both tables. +# @SIDE_EFFECT: Executes ALTER TABLE statements. +# @RELATION: [DEPENDS_ON] ->[engine] +def _ensure_translation_records_columns(bind_engine): + with belief_scope("_ensure_translation_records_columns"): + migrations = [ + ("translation_records", "source_data", + "ALTER TABLE translation_records ADD COLUMN source_data JSON"), + ("translation_preview_records", "source_data", + "ALTER TABLE translation_preview_records ADD COLUMN source_data JSON"), + ] + for table_name, column_name, alter_sql in migrations: + inspector = inspect(bind_engine) + if table_name not in inspector.get_table_names(): + continue + existing_columns = { + str(c.get("name") or "").strip() + for c in inspector.get_columns(table_name) + } + if column_name not in existing_columns: + try: + with bind_engine.begin() as connection: + connection.execute(text(alter_sql)) + logger.reflect( + f"Added {column_name} column to {table_name}", + ) + except Exception as migration_error: + logger.explore( + f"Failed to add {column_name} to {table_name}", + extra={"error": str(migration_error)}, + ) + raise +# [/DEF:_ensure_translation_records_columns:Function] + +# [DEF:_ensure_dataset_review_session_columns:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics. +# @PRE: bind_engine points to the application database where dataset review tables are stored. +# @POST: Missing additive columns across legacy dataset review tables are created without removing existing data. +# @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database. +# @RELATION: [DEPENDS_ON] ->[DatasetReviewSession] +# @RELATION: [DEPENDS_ON] ->[ImportedFilter] +def _ensure_dataset_review_session_columns(bind_engine): + with belief_scope("_ensure_dataset_review_session_columns"): + inspector = inspect(bind_engine) + existing_tables = set(inspector.get_table_names()) + migration_plan = { + "dataset_review_sessions": [ + ( + "version", + "ALTER TABLE dataset_review_sessions " + "ADD COLUMN version INTEGER NOT NULL DEFAULT 0", + ) + ], + "imported_filters": [ + ( + "raw_value_masked", + "ALTER TABLE imported_filters " + "ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE", + ) + ], + } + + for table_name, planned_columns in migration_plan.items(): + if table_name not in existing_tables: + logger.reason( + "Dataset review table does not exist yet; skipping additive schema migration", + extra={"table": table_name}, + ) + continue + + existing_columns = { + str(column.get("name") or "").strip() + for column in inspector.get_columns(table_name) + } + alter_statements = [ + statement + for column_name, statement in planned_columns + if column_name not in existing_columns + ] + + if not alter_statements: + logger.reason( + "Dataset review table schema already up to date", + extra={"table": table_name, "columns": sorted(existing_columns)}, + ) + continue + + logger.reason( + "Applying additive dataset review schema migration", + extra={"table": table_name, "statements": alter_statements}, + ) + + try: + with bind_engine.begin() as connection: + for statement in alter_statements: + connection.execute(text(statement)) + logger.reflect( + "Dataset review additive schema migration completed", + extra={ + "table": table_name, + "added_columns": [ + stmt.split(" ADD COLUMN ", 1)[1].split()[0] + for stmt in alter_statements + ], + }, + ) + except Exception as migration_error: + logger.explore( + "Dataset review additive schema migration failed", + extra={"table": table_name, "error": str(migration_error)}, + ) + raise + + +# [/DEF:_ensure_dataset_review_session_columns:Function] + + +# [DEF:init_db:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Initializes the database by creating all tables. +# @PRE: engine, tasks_engine and auth_engine are initialized. +# @POST: Database tables created in all databases. +# @SIDE_EFFECT: Creates physical database files if they don't exist. +# @RELATION: [CALLS] ->[ensure_connection_configs_table] +# @RELATION: [CALLS] ->[_ensure_filter_source_enum_values] +# @RELATION: [CALLS] ->[_ensure_dataset_review_session_columns] +def init_db(): + with belief_scope("init_db"): + Base.metadata.create_all(bind=engine) + Base.metadata.create_all(bind=tasks_engine) + Base.metadata.create_all(bind=auth_engine) + _ensure_user_dashboard_preferences_columns(engine) + _ensure_llm_validation_results_columns(engine) + _ensure_user_dashboard_preferences_health_columns(engine) + _ensure_git_server_configs_columns(engine) + _ensure_auth_users_columns(auth_engine) + ensure_connection_configs_table(engine) + _ensure_filter_source_enum_values(engine) + _ensure_dataset_review_session_columns(engine) + _ensure_translation_jobs_columns(engine) + _ensure_translation_records_columns(engine) + + +# [/DEF:init_db:Function] + + +# [DEF:get_db:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Dependency for getting a database session. +# @PRE: SessionLocal is initialized. +# @POST: Session is closed after use. +# @RETURN: Generator[Session, None, None] +# @RELATION: [DEPENDS_ON] ->[SessionLocal] +def get_db(): + with belief_scope("get_db"): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# [/DEF:get_db:Function] + + +# [DEF:get_tasks_db:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Dependency for getting a tasks database session. +# @PRE: TasksSessionLocal is initialized. +# @POST: Session is closed after use. +# @RETURN: Generator[Session, None, None] +# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal] +def get_tasks_db(): + with belief_scope("get_tasks_db"): + db = TasksSessionLocal() + try: + yield db + finally: + db.close() + + +# [/DEF:get_tasks_db:Function] + + +# [DEF:get_auth_db:Function] +# @COMPLEXITY: 3 +# @PURPOSE: Dependency for getting an authentication database session. +# @PRE: AuthSessionLocal is initialized. +# @POST: Session is closed after use. +# @DATA_CONTRACT: None -> Output[sqlalchemy.orm.Session] +# @RETURN: Generator[Session, None, None] +# @RELATION: [DEPENDS_ON] ->[AuthSessionLocal] +def get_auth_db(): + with belief_scope("get_auth_db"): + db = AuthSessionLocal() + try: + yield db + finally: + db.close() + + +# [/DEF:get_auth_db:Function] + +# [/DEF:DatabaseModule:Module] diff --git a/backend/src/core/encryption_key.py b/backend/src/core/encryption_key.py index 425d665c..b82b320d 100644 --- a/backend/src/core/encryption_key.py +++ b/backend/src/core/encryption_key.py @@ -1,14 +1,12 @@ -# [DEF:EncryptionKeyModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: encryption, key, bootstrap, environment, startup -# @PURPOSE: Resolve and persist the Fernet encryption key required by runtime services. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> [LoggerModule] -# @INVARIANT: Runtime key resolution never falls back to an ephemeral secret. -# @PRE: Runtime environment can read process variables and target .env path is writable when key generation is required. -# @POST: A valid Fernet key is available to runtime services via ENCRYPTION_KEY. -# @SIDE_EFFECT: May append ENCRYPTION_KEY entry into backend .env file and set process environment variable. -# @DATA_CONTRACT: Input[env_file_path] -> Output[encryption_key] +# #region EncryptionKeyModule [C:5] [TYPE Module] [SEMANTICS encryption, key, bootstrap, environment, startup] +# @BRIEF Resolve and persist the Fernet encryption key required by runtime services. +# @LAYER Infra +# @PRE Runtime environment can read process variables and target .env path is writable when key generation is required. +# @POST A valid Fernet key is available to runtime services via ENCRYPTION_KEY. +# @SIDE_EFFECT May append ENCRYPTION_KEY entry into backend .env file and set process environment variable. +# @DATA_CONTRACT Input[env_file_path] -> Output[encryption_key] +# @INVARIANT Runtime key resolution never falls back to an ephemeral secret. +# @RELATION DEPENDS_ON -> [LoggerModule] from __future__ import annotations @@ -22,11 +20,11 @@ from .logger import logger, belief_scope DEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / ".env" -# [DEF:ensure_encryption_key:Function] -# @PURPOSE: Ensure backend runtime has a persistent valid Fernet key. -# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment. -# @POST: Returns a valid Fernet key and guarantees it is present in process environment. -# @SIDE_EFFECT: May create or append backend/.env when key is missing. +# #region ensure_encryption_key [TYPE Function] +# @BRIEF Ensure backend runtime has a persistent valid Fernet key. +# @PRE env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment. +# @POST Returns a valid Fernet key and guarantees it is present in process environment. +# @SIDE_EFFECT May create or append backend/.env when key is missing. def ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str: with belief_scope("ensure_encryption_key", f"env_file_path={env_file_path}"): existing_key = os.getenv("ENCRYPTION_KEY", "").strip() @@ -57,6 +55,6 @@ def ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str: return generated_key -# [/DEF:ensure_encryption_key:Function] +# #endregion ensure_encryption_key -# [/DEF:EncryptionKeyModule:Module] +# #endregion EncryptionKeyModule diff --git a/backend/src/core/logger/__tests__/test_logger.py b/backend/src/core/logger/__tests__/test_logger.py index 974e1724..7554e2fc 100644 --- a/backend/src/core/logger/__tests__/test_logger.py +++ b/backend/src/core/logger/__tests__/test_logger.py @@ -1,8 +1,7 @@ -# [DEF:test_logger:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for logger module — CoT JSON format markers. -# @LAYER: Infra -# @RELATION: VERIFIES -> src.core.logger +# #region test_logger [C:3] [TYPE Module] +# @BRIEF Unit tests for logger module — CoT JSON format markers. +# @LAYER Infra +# @RELATION VERIFIES -> [src.core.logger] import sys from pathlib import Path @@ -45,11 +44,11 @@ def reset_logger_state(): configure_logger(config) -# [DEF:test_belief_scope_logs_reason_reflect_at_debug:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level. -# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG. -# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers. +# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function] +# @BRIEF Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level. +# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG. +# @POST Logs are verified to contain REASON (entry) and REFLECT (coherence) markers. +# @RELATION BINDS_TO -> [test_logger] def test_belief_scope_logs_reason_reflect_at_debug(caplog): """Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.""" # Configure logger to DEBUG level @@ -86,14 +85,14 @@ def test_belief_scope_logs_reason_reflect_at_debug(caplog): # Reset to INFO config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True) configure_logger(config) -# [/DEF:test_belief_scope_logs_reason_reflect_at_debug:Function] +# #endregion test_belief_scope_logs_reason_reflect_at_debug -# [DEF:test_belief_scope_error_handling:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception. -# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG. -# @POST: Logs are verified to contain EXPLORE marker with error info. +# #region test_belief_scope_error_handling [TYPE Function] +# @BRIEF Test that belief_scope logs EXPLORE marker on exception. +# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG. +# @POST Logs are verified to contain EXPLORE marker with error info. +# @RELATION BINDS_TO -> [test_logger] def test_belief_scope_error_handling(caplog): """Test that belief_scope logs EXPLORE marker on exception.""" # Configure logger to DEBUG level @@ -124,14 +123,14 @@ def test_belief_scope_error_handling(caplog): # Reset to INFO config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True) configure_logger(config) -# [/DEF:test_belief_scope_error_handling:Function] +# #endregion test_belief_scope_error_handling -# [DEF:test_belief_scope_success_coherence:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that belief_scope logs REFLECT marker on success. -# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG. -# @POST: Logs are verified to contain REFLECT marker. +# #region test_belief_scope_success_coherence [TYPE Function] +# @BRIEF Test that belief_scope logs REFLECT marker on success. +# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG. +# @POST Logs are verified to contain REFLECT marker. +# @RELATION BINDS_TO -> [test_logger] def test_belief_scope_success_coherence(caplog): """Test that belief_scope logs REFLECT marker on success.""" # Configure logger to DEBUG level @@ -158,14 +157,14 @@ def test_belief_scope_success_coherence(caplog): getattr(reflect_records[0], 'intent', '') == 'Coherence OK' -# [/DEF:test_belief_scope_success_coherence:Function] +# #endregion test_belief_scope_success_coherence -# [DEF:test_belief_scope_reason_not_visible_at_info:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level. -# @PRE: belief_scope is available. caplog fixture is used. -# @POST: REASON/REFLECT markers are not captured at INFO level. +# #region test_belief_scope_reason_not_visible_at_info [TYPE Function] +# @BRIEF Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level. +# @PRE belief_scope is available. caplog fixture is used. +# @POST REASON/REFLECT markers are not captured at INFO level. +# @RELATION BINDS_TO -> [test_logger] def test_belief_scope_reason_not_visible_at_info(caplog): """Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.""" caplog.set_level("INFO") @@ -192,26 +191,26 @@ def test_belief_scope_reason_not_visible_at_info(caplog): if r.levelname == 'INFO' and 'Doing something important' in r.getMessage() ] assert len(info_records) >= 1, "INFO log 'Doing something important' should be visible" -# [/DEF:test_belief_scope_reason_not_visible_at_info:Function] +# #endregion test_belief_scope_reason_not_visible_at_info -# [DEF:test_task_log_level_default:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that default task log level is INFO. -# @PRE: None. -# @POST: Default level is INFO. +# #region test_task_log_level_default [TYPE Function] +# @BRIEF Test that default task log level is INFO. +# @PRE None. +# @POST Default level is INFO. +# @RELATION BINDS_TO -> [test_logger] def test_task_log_level_default(): """Test that default task log level is INFO (after reset fixture).""" level = get_task_log_level() assert level == "INFO" -# [/DEF:test_task_log_level_default:Function] +# #endregion test_task_log_level_default -# [DEF:test_should_log_task_level:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that should_log_task_level correctly filters log levels. -# @PRE: None. -# @POST: Filtering works correctly for all level combinations. +# #region test_should_log_task_level [TYPE Function] +# @BRIEF Test that should_log_task_level correctly filters log levels. +# @PRE None. +# @POST Filtering works correctly for all level combinations. +# @RELATION BINDS_TO -> [test_logger] def test_should_log_task_level(): """Test that should_log_task_level correctly filters log levels.""" # Default level is INFO @@ -219,14 +218,14 @@ def test_should_log_task_level(): assert should_log_task_level("WARNING") is True, "WARNING should be logged at INFO threshold" assert should_log_task_level("INFO") is True, "INFO should be logged at INFO threshold" assert should_log_task_level("DEBUG") is False, "DEBUG should NOT be logged at INFO threshold" -# [/DEF:test_should_log_task_level:Function] +# #endregion test_should_log_task_level -# [DEF:test_configure_logger_task_log_level:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that configure_logger updates task_log_level. -# @PRE: LoggingConfig is available. -# @POST: task_log_level is updated correctly. +# #region test_configure_logger_task_log_level [TYPE Function] +# @BRIEF Test that configure_logger updates task_log_level. +# @PRE LoggingConfig is available. +# @POST task_log_level is updated correctly. +# @RELATION BINDS_TO -> [test_logger] def test_configure_logger_task_log_level(): """Test that configure_logger updates task_log_level.""" config = LoggingConfig( @@ -238,14 +237,14 @@ def test_configure_logger_task_log_level(): assert get_task_log_level() == "DEBUG", "task_log_level should be DEBUG" assert should_log_task_level("DEBUG") is True, "DEBUG should be logged at DEBUG threshold" -# [/DEF:test_configure_logger_task_log_level:Function] +# #endregion test_configure_logger_task_log_level -# [DEF:test_enable_belief_state_flag:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging. -# @PRE: LoggingConfig is available. caplog fixture is used. -# @POST: REASON entry marker is suppressed when disabled; REFLECT coherence still logged. +# #region test_enable_belief_state_flag [TYPE Function] +# @BRIEF Test that enable_belief_state flag controls belief_scope entry logging. +# @PRE LoggingConfig is available. caplog fixture is used. +# @POST REASON entry marker is suppressed when disabled; REFLECT coherence still logged. +# @RELATION BINDS_TO -> [test_logger] def test_enable_belief_state_flag(caplog): """Test that enable_belief_state flag controls belief_scope REASON entry logging.""" # Disable belief state @@ -274,12 +273,12 @@ def test_enable_belief_state_flag(caplog): if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction' ] assert len(reflect_records) >= 1, "REFLECT coherence should still be logged" -# [/DEF:test_enable_belief_state_flag:Function] +# #endregion test_enable_belief_state_flag -# [DEF:test_belief_scope_missing_anchor:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test @PRE condition: anchor_id must be provided +# #region test_belief_scope_missing_anchor [TYPE Function] +# @BRIEF Test @PRE condition: anchor_id must be provided +# @RELATION BINDS_TO -> [test_logger] def test_belief_scope_missing_anchor(): """Test that belief_scope enforces anchor_id to be provided.""" import pytest @@ -288,11 +287,11 @@ def test_belief_scope_missing_anchor(): # Missing required positional argument 'anchor_id' with belief_scope(): pass -# [/DEF:test_belief_scope_missing_anchor:Function] +# #endregion test_belief_scope_missing_anchor -# [DEF:test_configure_logger_post_conditions:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated. +# #region test_configure_logger_post_conditions [TYPE Function] +# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated. +# @RELATION BINDS_TO -> [test_logger] def test_configure_logger_post_conditions(tmp_path): """Test that configure_logger satisfies all @POST conditions.""" import logging @@ -327,11 +326,11 @@ def test_configure_logger_post_conditions(tmp_path): # 4. Global states assert getattr(logger_module, '_enable_belief_state') is False assert get_task_log_level() == "DEBUG" -# [/DEF:test_configure_logger_post_conditions:Function] +# #endregion test_configure_logger_post_conditions -# [DEF:test_cot_json_formatter_output:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields. +# #region test_cot_json_formatter_output [TYPE Function] +# @BRIEF Test that CotJsonFormatter produces valid JSON with expected fields. +# @RELATION BINDS_TO -> [test_logger] def test_cot_json_formatter_output(): """Test that CotJsonFormatter produces valid JSON with expected fields.""" import json @@ -364,11 +363,11 @@ def test_cot_json_formatter_output(): assert parsed["payload"] == {"key": "value"} assert "ts" in parsed assert "trace_id" in parsed -# [/DEF:test_cot_json_formatter_output:Function] +# #endregion test_cot_json_formatter_output -# [DEF:test_cot_json_formatter_plain_message:Function] -# @RELATION: BINDS_TO -> test_logger -# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker. +# #region test_cot_json_formatter_plain_message [TYPE Function] +# @BRIEF Test that CotJsonFormatter wraps plain messages (no extra) with default marker. +# @RELATION BINDS_TO -> [test_logger] def test_cot_json_formatter_plain_message(): """Test that CotJsonFormatter wraps plain messages with default marker.""" import json @@ -395,6 +394,6 @@ def test_cot_json_formatter_plain_message(): assert parsed["src"] == "test.module" assert "ts" in parsed assert "trace_id" in parsed -# [/DEF:test_cot_json_formatter_plain_message:Function] +# #endregion test_cot_json_formatter_plain_message -# [/DEF:test_logger:Module] +# #endregion test_logger diff --git a/backend/src/core/mapping_service.py b/backend/src/core/mapping_service.py index 96bf86fe..050f3541 100644 --- a/backend/src/core/mapping_service.py +++ b/backend/src/core/mapping_service.py @@ -1,15 +1,13 @@ -# [DEF:IdMappingServiceModule:Module] +# #region IdMappingServiceModule [C:5] [TYPE Module] [SEMANTICS mapping, ids, synchronization, environments, cross-filters] +# @BRIEF Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID) +# @LAYER Core +# @PRE Database session is valid and Superset client factory returns authenticated clients for requested environments. +# @POST Mapping synchronization and lookup APIs are available for environment-scoped UUID-to-integer resolution. +# @SIDE_EFFECT Reads/writes ResourceMapping rows, emits logs, and schedules periodic sync jobs. +# @DATA_CONTRACT Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None] +# @RELATION DEPENDS_ON -> [MappingModels] +# @RELATION DEPENDS_ON -> [LoggerModule] # -# @COMPLEXITY: 5 -# @SEMANTICS: mapping, ids, synchronization, environments, cross-filters -# @PURPOSE: Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID) -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [MappingModels] -# @RELATION: DEPENDS_ON -> [LoggerModule] -# @PRE: Database session is valid and Superset client factory returns authenticated clients for requested environments. -# @POST: Mapping synchronization and lookup APIs are available for environment-scoped UUID-to-integer resolution. -# @SIDE_EFFECT: Reads/writes ResourceMapping rows, emits logs, and schedules periodic sync jobs. -# @DATA_CONTRACT: Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None] # @TEST_DATA: mock_superset_resources -> {'chart': [{'id': 42, 'uuid': '1234', 'slice_name': 'test'}], 'dataset': [{'id': 99, 'uuid': '5678', 'table_name': 'data'}]} # # @INVARIANT: sync_environment must handle remote API failures gracefully. @@ -28,16 +26,15 @@ log = MarkerLogger("IdMapping") # [/SECTION] -# [DEF:IdMappingService:Class] -# @COMPLEXITY: 5 -# @PURPOSE: Service handling the cataloging and retrieval of remote Superset Integer IDs. -# @PRE: db_session is an active SQLAlchemy Session bound to mapping tables. -# @POST: Service instance provides scheduler control and environment-scoped mapping synchronization APIs. -# @RELATION: DEPENDS_ON -> [MappingModels] -# @RELATION: DEPENDS_ON -> [LoggerModule] -# @INVARIANT: self.db remains the authoritative session for all mapping operations. -# @SIDE_EFFECT: Instantiates an in-process scheduler and performs database writes during sync cycles. -# @DATA_CONTRACT: Input[db_session] -> Output[IdMappingService] +# #region IdMappingService [C:5] [TYPE Class] +# @BRIEF Service handling the cataloging and retrieval of remote Superset Integer IDs. +# @PRE db_session is an active SQLAlchemy Session bound to mapping tables. +# @POST Service instance provides scheduler control and environment-scoped mapping synchronization APIs. +# @SIDE_EFFECT Instantiates an in-process scheduler and performs database writes during sync cycles. +# @DATA_CONTRACT Input[db_session] -> Output[IdMappingService] +# @INVARIANT self.db remains the authoritative session for all mapping operations. +# @RELATION DEPENDS_ON -> [MappingModels] +# @RELATION DEPENDS_ON -> [LoggerModule] # # @TEST_CONTRACT: IdMappingServiceModel -> # { @@ -54,17 +51,17 @@ log = MarkerLogger("IdMapping") # @TEST_EDGE: get_batch_empty_list -> returns empty dict # @TEST_INVARIANT: resilient_fetching -> verifies: [sync_api_failure] class IdMappingService: - # [DEF:__init__:Function] - # @PURPOSE: Initializes the mapping service. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the mapping service. def __init__(self, db_session: Session): self.db = db_session self.scheduler = BackgroundScheduler() self._sync_job = None - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:start_scheduler:Function] - # @PURPOSE: Starts the background scheduler with a given cron string. + # #region start_scheduler [TYPE Function] + # @BRIEF Starts the background scheduler with a given cron string. # @PARAM: cron_string (str) - Cron expression for the sync interval. # @PARAM: environments (List[str]) - List of environment IDs to sync. # @PARAM: superset_client_factory - Function to get a client for an environment. @@ -95,10 +92,10 @@ class IdMappingService: else: log.reason(f"Updated background scheduler with cron: {cron_string}") - # [/DEF:start_scheduler:Function] + # #endregion start_scheduler - # [DEF:sync_environment:Function] - # @PURPOSE: Fully synchronizes mapping for a specific environment. + # #region sync_environment [TYPE Function] + # @BRIEF Fully synchronizes mapping for a specific environment. # @PARAM: environment_id (str) - Target environment ID. # @PARAM: superset_client - Instance capable of hitting the Superset API. # @PRE: environment_id exists in the database. @@ -235,10 +232,10 @@ class IdMappingService: log.explore("Critical sync failure", error=str(e)) raise - # [/DEF:sync_environment:Function] + # #endregion sync_environment - # [DEF:get_remote_id:Function] - # @PURPOSE: Retrieves the remote integer ID for a given universal UUID. + # #region get_remote_id [TYPE Function] + # @BRIEF Retrieves the remote integer ID for a given universal UUID. # @PARAM: environment_id (str) # @PARAM: resource_type (ResourceType) # @PARAM: uuid (str) @@ -261,10 +258,10 @@ class IdMappingService: return None return None - # [/DEF:get_remote_id:Function] + # #endregion get_remote_id - # [DEF:get_remote_ids_batch:Function] - # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently. + # #region get_remote_ids_batch [TYPE Function] + # @BRIEF Retrieves remote integer IDs for a list of universal UUIDs efficiently. # @PARAM: environment_id (str) # @PARAM: resource_type (ResourceType) # @PARAM: uuids (List[str]) @@ -294,8 +291,8 @@ class IdMappingService: return result - # [/DEF:get_remote_ids_batch:Function] + # #endregion get_remote_ids_batch -# [/DEF:IdMappingService:Class] -# [/DEF:IdMappingServiceModule:Module] +# #endregion IdMappingService +# #endregion IdMappingServiceModule diff --git a/backend/src/core/middleware/__init__.py b/backend/src/core/middleware/__init__.py index 60c85c06..772e5ca1 100644 --- a/backend/src/core/middleware/__init__.py +++ b/backend/src/core/middleware/__init__.py @@ -1,5 +1,3 @@ -# [DEF:middleware_package:Package] -# @COMPLEXITY: 1 -# @SEMANTICS: middleware, package, starlette, fastapi -# @PURPOSE: FastAPI/Starlette middleware package for request-level context and tracing. -# [/DEF:middleware_package:Package] +# #region middleware_package [C:1] [TYPE Package] [SEMANTICS middleware, package, starlette, fastapi] +# @BRIEF FastAPI/Starlette middleware package for request-level context and tracing. +# #endregion middleware_package diff --git a/backend/src/core/middleware/trace.py b/backend/src/core/middleware/trace.py index 7fff9aa9..369f47c7 100644 --- a/backend/src/core/middleware/trace.py +++ b/backend/src/core/middleware/trace.py @@ -1,7 +1,5 @@ -# [DEF:TraceContextMiddlewareModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: middleware, trace, context, starlette, fastapi -# @PURPOSE: FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request. +# #region TraceContextMiddlewareModule [C:3] [TYPE Module] [SEMANTICS middleware, trace, context, starlette, fastapi] +# @BRIEF FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request. # Optionally extracts X-Trace-ID header for cross-service trace propagation. # @LAYER: Core # @RELATION: [DEPENDS_ON] -> [CotLoggerModule] @@ -18,10 +16,8 @@ from starlette.types import ASGIApp from ..cot_logger import seed_trace_id, set_trace_id -# [DEF:TraceContextMiddleware:Class] -# @COMPLEXITY: 2 -# @SEMANTICS: middleware, trace, context, dispatch -# @BRIEF: Starlette BaseHTTPMiddleware that seeds a trace_id per request. +# #region TraceContextMiddleware [C:2] [TYPE Class] [SEMANTICS middleware, trace, context, dispatch] +# @BRIEF Starlette BaseHTTPMiddleware that seeds a trace_id per request. class TraceContextMiddleware(BaseHTTPMiddleware): """FastAPI/Starlette middleware that seeds a trace_id for every request. @@ -35,17 +31,15 @@ class TraceContextMiddleware(BaseHTTPMiddleware): app.add_middleware(TraceContextMiddleware) """ - # [DEF:TraceContextMiddleware.__init__:Function] - # @BRIEF: Standard BaseHTTPMiddleware initialiser. + # #region TraceContextMiddleware.__init__ [TYPE Function] + # @BRIEF Standard BaseHTTPMiddleware initialiser. def __init__(self, app: ASGIApp) -> None: super().__init__(app) - # [/DEF:TraceContextMiddleware.__init__:Function] + # #endregion TraceContextMiddleware.__init__ - # [DEF:TraceContextMiddleware.dispatch:Function] - # @COMPLEXITY: 2 - # @SEMANTICS: dispatch, trace, seed, header, call_next - # @BRIEF: Dispatch handler that seeds trace_id before passing to the next middleware. + # #region TraceContextMiddleware.dispatch [C:2] [TYPE Function] [SEMANTICS dispatch, trace, seed, header, call_next] + # @BRIEF Dispatch handler that seeds trace_id before passing to the next middleware. async def dispatch( self, request: Request, call_next ) -> Response: @@ -70,8 +64,7 @@ class TraceContextMiddleware(BaseHTTPMiddleware): response = await call_next(request) return response - # [/DEF:TraceContextMiddleware.dispatch:Function] + # #endregion TraceContextMiddleware.dispatch -# [/DEF:TraceContextMiddleware:Class] -# [/DEF:TraceContextMiddlewareModule:Module] +# #endregion TraceContextMiddleware diff --git a/backend/src/core/migration/__init__.py b/backend/src/core/migration/__init__.py index 2d7b3d8b..974bd749 100644 --- a/backend/src/core/migration/__init__.py +++ b/backend/src/core/migration/__init__.py @@ -1,8 +1,8 @@ -# [DEF:MigrationPackage:Module] +# #region MigrationPackage [TYPE Module] from .dry_run_orchestrator import MigrationDryRunService from .archive_parser import MigrationArchiveParser __all__ = ["MigrationDryRunService", "MigrationArchiveParser"] -# [/DEF:MigrationPackage:Module] +# #endregion MigrationPackage diff --git a/backend/src/core/migration/archive_parser.py b/backend/src/core/migration/archive_parser.py index 1d4fbb4e..98dcb845 100644 --- a/backend/src/core/migration/archive_parser.py +++ b/backend/src/core/migration/archive_parser.py @@ -1,10 +1,8 @@ -# [DEF:MigrationArchiveParserModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: migration, zip, parser, yaml, metadata -# @PURPOSE: Parse Superset export ZIP archives into normalized object catalogs for diffing. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [LoggerModule] -# @INVARIANT: Parsing is read-only and never mutates archive files. +# #region MigrationArchiveParserModule [C:3] [TYPE Module] [SEMANTICS migration, zip, parser, yaml, metadata] +# @BRIEF Parse Superset export ZIP archives into normalized object catalogs for diffing. +# @LAYER Core +# @INVARIANT Parsing is read-only and never mutates archive files. +# @RELATION DEPENDS_ON -> [LoggerModule] import json import tempfile @@ -17,18 +15,18 @@ import yaml from ..logger import logger, belief_scope -# [DEF:MigrationArchiveParser:Class] -# @PURPOSE: Extract normalized dashboards/charts/datasets metadata from ZIP archives. -# @RELATION: CONTAINS -> [extract_objects_from_zip] -# @RELATION: CONTAINS -> [_collect_yaml_objects] -# @RELATION: CONTAINS -> [_normalize_object_payload] +# #region MigrationArchiveParser [TYPE Class] +# @BRIEF Extract normalized dashboards/charts/datasets metadata from ZIP archives. +# @RELATION CONTAINS -> [extract_objects_from_zip] +# @RELATION CONTAINS -> [_collect_yaml_objects] +# @RELATION CONTAINS -> [_normalize_object_payload] class MigrationArchiveParser: - # [DEF:extract_objects_from_zip:Function] - # @PURPOSE: Extract object catalogs from Superset archive. - # @RELATION: DEPENDS_ON -> [_collect_yaml_objects] - # @PRE: zip_path points to a valid readable ZIP. - # @POST: Returns object lists grouped by resource type. - # @RETURN: Dict[str, List[Dict[str, Any]]] + # #region extract_objects_from_zip [TYPE Function] + # @BRIEF Extract object catalogs from Superset archive. + # @PRE zip_path points to a valid readable ZIP. + # @POST Returns object lists grouped by resource type. + # @RETURN Dict[str, List[Dict[str, Any]]] + # @RELATION DEPENDS_ON -> [_collect_yaml_objects] def extract_objects_from_zip( self, zip_path: str ) -> Dict[str, List[Dict[str, Any]]]: @@ -51,13 +49,13 @@ class MigrationArchiveParser: return result - # [/DEF:extract_objects_from_zip:Function] + # #endregion extract_objects_from_zip - # [DEF:_collect_yaml_objects:Function] - # @PURPOSE: Read and normalize YAML manifests for one object type. - # @RELATION: DEPENDS_ON -> [_normalize_object_payload] - # @PRE: object_type is one of dashboards/charts/datasets. - # @POST: Returns only valid normalized objects. + # #region _collect_yaml_objects [TYPE Function] + # @BRIEF Read and normalize YAML manifests for one object type. + # @PRE object_type is one of dashboards/charts/datasets. + # @POST Returns only valid normalized objects. + # @RELATION DEPENDS_ON -> [_normalize_object_payload] def _collect_yaml_objects( self, root_dir: Path, object_type: str ) -> List[Dict[str, Any]]: @@ -81,12 +79,12 @@ class MigrationArchiveParser: ) return objects - # [/DEF:_collect_yaml_objects:Function] + # #endregion _collect_yaml_objects - # [DEF:_normalize_object_payload:Function] - # @PURPOSE: Convert raw YAML payload to stable diff signature shape. - # @PRE: payload is parsed YAML mapping. - # @POST: Returns normalized descriptor with `uuid`, `title`, and `signature`. + # #region _normalize_object_payload [TYPE Function] + # @BRIEF Convert raw YAML payload to stable diff signature shape. + # @PRE payload is parsed YAML mapping. + # @POST Returns normalized descriptor with `uuid`, `title`, and `signature`. def _normalize_object_payload( self, payload: Dict[str, Any], object_type: str ) -> Optional[Dict[str, Any]]: @@ -151,8 +149,8 @@ class MigrationArchiveParser: return None - # [/DEF:_normalize_object_payload:Function] + # #endregion _normalize_object_payload -# [/DEF:MigrationArchiveParser:Class] -# [/DEF:MigrationArchiveParserModule:Module] +# #endregion MigrationArchiveParser +# #endregion MigrationArchiveParserModule diff --git a/backend/src/core/migration/dry_run_orchestrator.py b/backend/src/core/migration/dry_run_orchestrator.py index 21e79777..10cc67cc 100644 --- a/backend/src/core/migration/dry_run_orchestrator.py +++ b/backend/src/core/migration/dry_run_orchestrator.py @@ -1,13 +1,11 @@ -# [DEF:MigrationDryRunOrchestratorModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: migration, dry_run, diff, risk, superset -# @PURPOSE: Compute pre-flight migration diff and risk scoring without apply. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @RELATION: DEPENDS_ON -> [MigrationEngine] -# @RELATION: DEPENDS_ON -> [MigrationArchiveParser] -# @RELATION: DEPENDS_ON -> [RiskAssessorModule] -# @INVARIANT: Dry run is informative only and must not mutate target environment. +# #region MigrationDryRunOrchestratorModule [C:3] [TYPE Module] [SEMANTICS migration, dry_run, diff, risk, superset] +# @BRIEF Compute pre-flight migration diff and risk scoring without apply. +# @LAYER Core +# @INVARIANT Dry run is informative only and must not mutate target environment. +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [MigrationEngine] +# @RELATION DEPENDS_ON -> [MigrationArchiveParser] +# @RELATION DEPENDS_ON -> [RiskAssessorModule] from datetime import datetime, timezone import json @@ -25,36 +23,36 @@ from ..superset_client import SupersetClient from ..utils.fileio import create_temp_file -# [DEF:MigrationDryRunService:Class] -# @PURPOSE: Build deterministic diff/risk payload for migration pre-flight. -# @RELATION: CONTAINS -> [__init__] -# @RELATION: CONTAINS -> [run] -# @RELATION: CONTAINS -> [_load_db_mapping] -# @RELATION: CONTAINS -> [_accumulate_objects] -# @RELATION: CONTAINS -> [_index_by_uuid] -# @RELATION: CONTAINS -> [_build_object_diff] -# @RELATION: CONTAINS -> [_build_target_signatures] -# @RELATION: CONTAINS -> [_build_risks] +# #region MigrationDryRunService [TYPE Class] +# @BRIEF Build deterministic diff/risk payload for migration pre-flight. +# @RELATION CONTAINS -> [__init__] +# @RELATION CONTAINS -> [run] +# @RELATION CONTAINS -> [_load_db_mapping] +# @RELATION CONTAINS -> [_accumulate_objects] +# @RELATION CONTAINS -> [_index_by_uuid] +# @RELATION CONTAINS -> [_build_object_diff] +# @RELATION CONTAINS -> [_build_target_signatures] +# @RELATION CONTAINS -> [_build_risks] class MigrationDryRunService: - # [DEF:__init__:Function] - # @PURPOSE: Wire parser dependency for archive object extraction. - # @PRE: parser can be omitted to use default implementation. - # @POST: Service is ready to calculate dry-run payload. + # #region __init__ [TYPE Function] + # @BRIEF Wire parser dependency for archive object extraction. + # @PRE parser can be omitted to use default implementation. + # @POST Service is ready to calculate dry-run payload. def __init__(self, parser: MigrationArchiveParser | None = None): self.parser = parser or MigrationArchiveParser() - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:run:Function] - # @PURPOSE: Execute full dry-run computation for selected dashboards. - # @RELATION: DEPENDS_ON -> [_load_db_mapping] - # @RELATION: DEPENDS_ON -> [_accumulate_objects] - # @RELATION: DEPENDS_ON -> [_build_target_signatures] - # @RELATION: DEPENDS_ON -> [_build_object_diff] - # @RELATION: DEPENDS_ON -> [_build_risks] - # @PRE: source/target clients are authenticated and selection validated by caller. - # @POST: Returns JSON-serializable pre-flight payload with summary, diff and risk. - # @SIDE_EFFECT: Reads source export archives and target metadata via network. + # #region run [TYPE Function] + # @BRIEF Execute full dry-run computation for selected dashboards. + # @PRE source/target clients are authenticated and selection validated by caller. + # @POST Returns JSON-serializable pre-flight payload with summary, diff and risk. + # @SIDE_EFFECT Reads source export archives and target metadata via network. + # @RELATION DEPENDS_ON -> [_load_db_mapping] + # @RELATION DEPENDS_ON -> [_accumulate_objects] + # @RELATION DEPENDS_ON -> [_build_target_signatures] + # @RELATION DEPENDS_ON -> [_build_object_diff] + # @RELATION DEPENDS_ON -> [_build_risks] def run( self, selection: DashboardSelection, @@ -156,10 +154,10 @@ class MigrationDryRunService: "risk": score_risks(risk), } - # [/DEF:run:Function] + # #endregion run - # [DEF:_load_db_mapping:Function] - # @PURPOSE: Resolve UUID mapping for optional DB config replacement. + # #region _load_db_mapping [TYPE Function] + # @BRIEF Resolve UUID mapping for optional DB config replacement. def _load_db_mapping( self, db: Session, selection: DashboardSelection ) -> Dict[str, str]: @@ -173,10 +171,10 @@ class MigrationDryRunService: ) return {row.source_db_uuid: row.target_db_uuid for row in rows} - # [/DEF:_load_db_mapping:Function] + # #endregion _load_db_mapping - # [DEF:_accumulate_objects:Function] - # @PURPOSE: Merge extracted resources by UUID to avoid duplicates. + # #region _accumulate_objects [TYPE Function] + # @BRIEF Merge extracted resources by UUID to avoid duplicates. def _accumulate_objects( self, target: Dict[str, Dict[str, Dict[str, Any]]], @@ -188,10 +186,10 @@ class MigrationDryRunService: if uuid: target[object_type][str(uuid)] = item - # [/DEF:_accumulate_objects:Function] + # #endregion _accumulate_objects - # [DEF:_index_by_uuid:Function] - # @PURPOSE: Build UUID-index map for normalized resources. + # #region _index_by_uuid [TYPE Function] + # @BRIEF Build UUID-index map for normalized resources. def _index_by_uuid( self, objects: List[Dict[str, Any]] ) -> Dict[str, Dict[str, Any]]: @@ -202,11 +200,11 @@ class MigrationDryRunService: indexed[str(uuid)] = obj return indexed - # [/DEF:_index_by_uuid:Function] + # #endregion _index_by_uuid - # [DEF:_build_object_diff:Function] - # @PURPOSE: Compute create/update/delete buckets by UUID+signature. - # @RELATION: DEPENDS_ON -> [_index_by_uuid] + # #region _build_object_diff [TYPE Function] + # @BRIEF Compute create/update/delete buckets by UUID+signature. + # @RELATION DEPENDS_ON -> [_index_by_uuid] def _build_object_diff( self, source_objects: List[Dict[str, Any]], target_objects: List[Dict[str, Any]] ) -> Dict[str, List[Dict[str, Any]]]: @@ -230,10 +228,10 @@ class MigrationDryRunService: ) return {"create": created, "update": updated, "delete": deleted} - # [/DEF:_build_object_diff:Function] + # #endregion _build_object_diff - # [DEF:_build_target_signatures:Function] - # @PURPOSE: Pull target metadata and normalize it into comparable signatures. + # #region _build_target_signatures [TYPE Function] + # @BRIEF Pull target metadata and normalize it into comparable signatures. def _build_target_signatures( self, client: SupersetClient ) -> Dict[str, List[Dict[str, Any]]]: @@ -343,10 +341,10 @@ class MigrationDryRunService: ], } - # [/DEF:_build_target_signatures:Function] + # #endregion _build_target_signatures - # [DEF:_build_risks:Function] - # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch. + # #region _build_risks [TYPE Function] + # @BRIEF Build risk items for missing datasource, broken refs, overwrite, owner mismatch. def _build_risks( self, source_objects: Dict[str, List[Dict[str, Any]]], @@ -356,8 +354,8 @@ class MigrationDryRunService: ) -> List[Dict[str, Any]]: return build_risks(source_objects, target_objects, diff, target_client) - # [/DEF:_build_risks:Function] + # #endregion _build_risks -# [/DEF:MigrationDryRunService:Class] -# [/DEF:MigrationDryRunOrchestratorModule:Module] +# #endregion MigrationDryRunService +# #endregion MigrationDryRunOrchestratorModule diff --git a/backend/src/core/migration/risk_assessor.py b/backend/src/core/migration/risk_assessor.py index 3823b9ef..e1c69ea6 100644 --- a/backend/src/core/migration/risk_assessor.py +++ b/backend/src/core/migration/risk_assessor.py @@ -1,18 +1,16 @@ -# [DEF:RiskAssessorModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: migration, dry_run, risk, scoring, preflight -# @PURPOSE: Compute deterministic migration risk items and aggregate score for dry-run reporting. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @RELATION: CONTAINS -> [index_by_uuid] -# @RELATION: CONTAINS -> [extract_owner_identifiers] -# @RELATION: CONTAINS -> [build_risks] -# @RELATION: CONTAINS -> [score_risks] -# @INVARIANT: Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping. -# @PRE: Risk assessor functions receive normalized migration object collections from dry-run orchestration. -# @POST: Risk scoring output preserves item list and provides bounded score with derived level. -# @SIDE_EFFECT: Emits diagnostic logs and performs read-only metadata requests via Superset client. -# @DATA_CONTRACT: Module[build_risks, score_risks] +# #region RiskAssessorModule [C:5] [TYPE Module] [SEMANTICS migration, dry_run, risk, scoring, preflight] +# @BRIEF Compute deterministic migration risk items and aggregate score for dry-run reporting. +# @LAYER Domain +# @PRE Risk assessor functions receive normalized migration object collections from dry-run orchestration. +# @POST Risk scoring output preserves item list and provides bounded score with derived level. +# @SIDE_EFFECT Emits diagnostic logs and performs read-only metadata requests via Superset client. +# @DATA_CONTRACT Module[build_risks, score_risks] +# @INVARIANT Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping. +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION CONTAINS -> [index_by_uuid] +# @RELATION CONTAINS -> [extract_owner_identifiers] +# @RELATION CONTAINS -> [build_risks] +# @RELATION CONTAINS -> [score_risks] # @TEST_CONTRACT: [source_objects,target_objects,diff,target_client] -> [List[RiskItem]] # @TEST_SCENARIO: [overwrite_update_objects] -> [medium overwrite_existing risk is emitted for each update diff item] # @TEST_SCENARIO: [missing_datasource_dataset] -> [high missing_datasource risk is emitted] @@ -35,12 +33,12 @@ from ..superset_client import SupersetClient log = MarkerLogger("RiskAssessor") -# [DEF:index_by_uuid:Function] -# @PURPOSE: Build UUID-index from normalized objects. -# @PRE: Input list items are dict-like payloads potentially containing "uuid". -# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included. -# @SIDE_EFFECT: Emits reasoning/reflective logs only. -# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]] +# #region index_by_uuid [TYPE Function] +# @BRIEF Build UUID-index from normalized objects. +# @PRE Input list items are dict-like payloads potentially containing "uuid". +# @POST Returns mapping keyed by string uuid; only truthy uuid values are included. +# @SIDE_EFFECT Emits reasoning/reflective logs only. +# @DATA_CONTRACT List[Dict[str, Any]] -> Dict[str, Dict[str, Any]] def index_by_uuid(objects: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: with belief_scope("risk_assessor.index_by_uuid"): log.reason("Building UUID index", payload={"objects_count": len(objects)}) @@ -53,15 +51,15 @@ def index_by_uuid(objects: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: return indexed -# [/DEF:index_by_uuid:Function] +# #endregion index_by_uuid -# [DEF:extract_owner_identifiers:Function] -# @PURPOSE: Normalize owner payloads for stable comparison. -# @PRE: Owners may be list payload, scalar values, or None. -# @POST: Returns sorted unique owner identifiers as strings. -# @SIDE_EFFECT: Emits reasoning/reflective logs only. -# @DATA_CONTRACT: Any -> List[str] +# #region extract_owner_identifiers [TYPE Function] +# @BRIEF Normalize owner payloads for stable comparison. +# @PRE Owners may be list payload, scalar values, or None. +# @POST Returns sorted unique owner identifiers as strings. +# @SIDE_EFFECT Emits reasoning/reflective logs only. +# @DATA_CONTRACT Any -> List[str] def extract_owner_identifiers(owners: Any) -> List[str]: with belief_scope("risk_assessor.extract_owner_identifiers"): log.reason("Normalizing owner identifiers") @@ -84,23 +82,23 @@ def extract_owner_identifiers(owners: Any) -> List[str]: return normalized_ids -# [/DEF:extract_owner_identifiers:Function] +# #endregion extract_owner_identifiers -# [DEF:build_risks:Function] -# @PURPOSE: Build risk list from computed diffs and target catalog state. -# @RELATION: DEPENDS_ON -> [index_by_uuid] -# @RELATION: DEPENDS_ON -> [extract_owner_identifiers] -# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures. -# @PRE: target_client is authenticated/usable for database list retrieval. -# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks. -# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs. -# @DATA_CONTRACT: ( -# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]], -# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]], -# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]], -# @DATA_CONTRACT: SupersetClient -# @DATA_CONTRACT: ) -> List[Dict[str, Any]] +# #region build_risks [TYPE Function] +# @BRIEF Build risk list from computed diffs and target catalog state. +# @PRE source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures. +# @PRE target_client is authenticated/usable for database list retrieval. +# @POST Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks. +# @SIDE_EFFECT Calls target Superset API for databases metadata and emits logs. +# @DATA_CONTRACT ( +# @DATA_CONTRACT Dict[str, List[Dict[str, Any]]], +# @DATA_CONTRACT Dict[str, List[Dict[str, Any]]], +# @DATA_CONTRACT Dict[str, Dict[str, List[Dict[str, Any]]]], +# @DATA_CONTRACT SupersetClient +# @DATA_CONTRACT ) -> List[Dict[str, Any]] +# @RELATION DEPENDS_ON -> [index_by_uuid] +# @RELATION DEPENDS_ON -> [extract_owner_identifiers] def build_risks( source_objects: Dict[str, List[Dict[str, Any]]], target_objects: Dict[str, List[Dict[str, Any]]], @@ -177,15 +175,15 @@ def build_risks( return risks -# [/DEF:build_risks:Function] +# #endregion build_risks -# [DEF:score_risks:Function] -# @PURPOSE: Aggregate risk list into score and level. -# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight. -# @POST: Returns dict with score in [0,100], derived level, and original items. -# @SIDE_EFFECT: Emits reasoning/reflective logs only. -# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any] +# #region score_risks [TYPE Function] +# @BRIEF Aggregate risk list into score and level. +# @PRE risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight. +# @POST Returns dict with score in [0,100], derived level, and original items. +# @SIDE_EFFECT Emits reasoning/reflective logs only. +# @DATA_CONTRACT List[Dict[str, Any]] -> Dict[str, Any] def score_risks(risk_items: List[Dict[str, Any]]) -> Dict[str, Any]: with belief_scope("risk_assessor.score_risks"): log.reason("Scoring risk items", payload={"risk_items_count": len(risk_items)}) @@ -199,7 +197,7 @@ def score_risks(risk_items: List[Dict[str, Any]]) -> Dict[str, Any]: return result -# [/DEF:score_risks:Function] +# #endregion score_risks -# [/DEF:RiskAssessorModule:Module] +# #endregion RiskAssessorModule diff --git a/backend/src/core/migration_engine.py b/backend/src/core/migration_engine.py index 9b2d199d..d2024dda 100644 --- a/backend/src/core/migration_engine.py +++ b/backend/src/core/migration_engine.py @@ -1,18 +1,16 @@ -# [DEF:MigrationEngineModule:Module] +# #region MigrationEngineModule [C:5] [TYPE Module] [SEMANTICS migration, engine, zip, yaml, transformation, cross-filter, id-mapping] +# @BRIEF Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers. +# @LAYER Domain +# @PRE Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs. +# @POST Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines. +# @SIDE_EFFECT Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs. +# @DATA_CONTRACT Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive] +# @INVARIANT ZIP structure and non-targeted metadata must remain valid after transformation. +# @RELATION DEPENDS_ON -> [LoggerModule] +# @RELATION DEPENDS_ON -> [IdMappingService] +# @RELATION DEPENDS_ON -> [ResourceType] +# @RELATION DEPENDS_ON -> [yaml] # -# @COMPLEXITY: 5 -# @SEMANTICS: migration, engine, zip, yaml, transformation, cross-filter, id-mapping -# @PURPOSE: Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[LoggerModule] -# @RELATION: [DEPENDS_ON] ->[IdMappingService] -# @RELATION: [DEPENDS_ON] ->[ResourceType] -# @RELATION: [DEPENDS_ON] ->[yaml] -# @PRE: Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs. -# @POST: Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines. -# @SIDE_EFFECT: Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs. -# @DATA_CONTRACT: Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive] -# @INVARIANT: ZIP structure and non-targeted metadata must remain valid after transformation. # [SECTION: IMPORTS] import zipfile @@ -29,16 +27,16 @@ from src.models.mapping import ResourceType # [/SECTION] -# [DEF:MigrationEngine:Class] -# @PURPOSE: Engine for transforming Superset export ZIPs. -# @RELATION: CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata] +# #region MigrationEngine [TYPE Class] +# @BRIEF Engine for transforming Superset export ZIPs. +# @RELATION CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata] class MigrationEngine: - # [DEF:__init__:Function] - # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations. - # @PRE: mapping_service is None or implements batch remote ID lookup for ResourceType.CHART. - # @POST: self.mapping_service is assigned and available for optional cross-filter patching flows. - # @SIDE_EFFECT: Mutates in-memory engine state by storing dependency reference. - # @DATA_CONTRACT: Input[Optional[IdMappingService]] -> Output[MigrationEngine] + # #region __init__ [TYPE Function] + # @BRIEF Initializes migration orchestration dependencies for ZIP/YAML metadata transformations. + # @PRE mapping_service is None or implements batch remote ID lookup for ResourceType.CHART. + # @POST self.mapping_service is assigned and available for optional cross-filter patching flows. + # @SIDE_EFFECT Mutates in-memory engine state by storing dependency reference. + # @DATA_CONTRACT Input[Optional[IdMappingService]] -> Output[MigrationEngine] # @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs. def __init__(self, mapping_service: Optional[IdMappingService] = None): with belief_scope("MigrationEngine.__init__"): @@ -46,11 +44,11 @@ class MigrationEngine: self.mapping_service = mapping_service logger.reflect("MigrationEngine initialized") - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:transform_zip:Function] - # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages. - # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata] + # #region transform_zip [TYPE Function] + # @BRIEF Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages. + # @RELATION DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata] # @PARAM: zip_path (str) - Path to the source ZIP file. # @PARAM: output_path (str) - Path where the transformed ZIP will be saved. # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID. @@ -146,10 +144,10 @@ class MigrationEngine: logger.explore(f"Error transforming ZIP: {e}") return False - # [/DEF:transform_zip:Function] + # #endregion transform_zip - # [DEF:_transform_yaml:Function] - # @PURPOSE: Replaces database_uuid in a single YAML file. + # #region _transform_yaml [TYPE Function] + # @BRIEF Replaces database_uuid in a single YAML file. # @PARAM: file_path (Path) - Path to the YAML file. # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary. # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs. @@ -176,14 +174,14 @@ class MigrationEngine: yaml.dump(data, f) logger.reflect(f"Database UUID patched in {file_path.name}") - # [/DEF:_transform_yaml:Function] + # #endregion _transform_yaml - # [DEF:_extract_chart_uuids_from_archive:Function] - # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map. - # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources. - # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs. - # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures. - # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]] + # #region _extract_chart_uuids_from_archive [TYPE Function] + # @BRIEF Scans extracted chart YAML files and builds a source chart ID to UUID lookup map. + # @PRE temp_dir exists and points to extracted archive root with optional chart YAML resources. + # @POST Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs. + # @SIDE_EFFECT Reads chart YAML files from filesystem; suppresses per-file parsing failures. + # @DATA_CONTRACT Input[Path] -> Output[Dict[int,str]] # @PARAM: temp_dir (Path) - Root dir of unpacked archive. # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID. def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> Dict[int, str]: @@ -206,14 +204,14 @@ class MigrationEngine: pass return mapping - # [/DEF:_extract_chart_uuids_from_archive:Function] + # #endregion _extract_chart_uuids_from_archive - # [DEF:_patch_dashboard_metadata:Function] - # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings. - # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid. - # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged. - # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures. - # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None] + # #region _patch_dashboard_metadata [TYPE Function] + # @BRIEF Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings. + # @PRE file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid. + # @POST json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged. + # @SIDE_EFFECT Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures. + # @DATA_CONTRACT Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None] # @PARAM: file_path (Path) # @PARAM: target_env_id (str) # @PARAM: source_map (Dict[int, str]) @@ -297,8 +295,8 @@ class MigrationEngine: except Exception as e: logger.explore(f"Metadata patch failed for {file_path.name}: {e}") - # [/DEF:_patch_dashboard_metadata:Function] + # #endregion _patch_dashboard_metadata -# [/DEF:MigrationEngine:Class] +# #endregion MigrationEngine -# [/DEF:MigrationEngineModule:Module] \ No newline at end of file +# #endregion MigrationEngineModule diff --git a/backend/src/core/plugin_base.py b/backend/src/core/plugin_base.py index bda35651..a21fda2c 100755 --- a/backend/src/core/plugin_base.py +++ b/backend/src/core/plugin_base.py @@ -1,143 +1,141 @@ -from abc import ABC, abstractmethod -from typing import Dict, Any, Optional -from .logger import belief_scope - -from pydantic import BaseModel, Field - -# [DEF:PluginBase:Class] -# @SEMANTICS: plugin, interface, base, abstract -# @PURPOSE: Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution. -# @LAYER: Core -# @RELATION: Used by PluginLoader to identify valid plugins. -# @INVARIANT: All plugins MUST inherit from this class. -class PluginBase(ABC): - """ - Base class for all plugins. - Plugins must inherit from this class and implement the abstract methods. - """ - - @property - @abstractmethod - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string ID. - # @RETURN: str - Plugin ID. - def id(self) -> str: - """A unique identifier for the plugin.""" - with belief_scope("id"): - pass - # [/DEF:id:Function] - - @property - @abstractmethod - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string name. - # @RETURN: str - Plugin name. - def name(self) -> str: - """A human-readable name for the plugin.""" - with belief_scope("name"): - pass - # [/DEF:name:Function] - - @property - @abstractmethod - # [DEF:description:Function] - # @PURPOSE: Returns a brief description of the plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string description. - # @RETURN: str - Plugin description. - def description(self) -> str: - """A brief description of what the plugin does.""" - with belief_scope("description"): - pass - # [/DEF:description:Function] - - @property - @abstractmethod - # [DEF:version:Function] - # @PURPOSE: Returns the version of the plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string version. - # @RETURN: str - Plugin version. - def version(self) -> str: - """The version of the plugin.""" - with belief_scope("version"): - pass - # [/DEF:version:Function] - - @property - # [DEF:required_permission:Function] - # @PURPOSE: Returns the required permission string to execute this plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string permission. - # @RETURN: str - Required permission (e.g., "plugin:backup:execute"). - def required_permission(self) -> str: - """The permission string required to execute this plugin.""" - with belief_scope("required_permission"): - return f"plugin:{self.id}:execute" - # [/DEF:required_permission:Function] - - @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable. - # @PRE: Plugin instance exists. - # @POST: Returns string route or None. - # @RETURN: Optional[str] - Frontend route. - def ui_route(self) -> Optional[str]: - """ - The frontend route for the plugin's UI. - Returns None if the plugin does not have a dedicated UI page. - """ - with belief_scope("ui_route"): - return None - # [/DEF:ui_route:Function] - - @abstractmethod - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for the plugin's input parameters. - # @PRE: Plugin instance exists. - # @POST: Returns dict schema. - # @RETURN: Dict[str, Any] - JSON schema. - def get_schema(self) -> Dict[str, Any]: - """ - Returns the JSON schema for the plugin's input parameters. - This schema will be used to generate the frontend form. - """ - with belief_scope("get_schema"): - pass - # [/DEF:get_schema:Function] - - @abstractmethod - # [DEF:execute:Function] - # @PURPOSE: Executes the plugin's core logic. - # @PARAM: params (Dict[str, Any]) - Validated input parameters. - # @PRE: params must be a dictionary. - # @POST: Plugin execution is completed. - async def execute(self, params: Dict[str, Any]): - with belief_scope("execute"): - pass - """ - Executes the plugin's logic. - The `params` argument will be validated against the schema returned by `get_schema()`. - """ - pass - # [/DEF:execute:Function] -# [/DEF:PluginBase:Class] - -# [DEF:PluginConfig:Class] -# @SEMANTICS: plugin, config, schema, pydantic -# @PURPOSE: A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer. -# @LAYER: Core -# @RELATION: Instantiated by PluginLoader after validating a PluginBase instance. -class PluginConfig(BaseModel): - """Pydantic model for plugin configuration.""" - id: str = Field(..., description="Unique identifier for the plugin") - name: str = Field(..., description="Human-readable name for the plugin") - description: str = Field(..., description="Brief description of what the plugin does") - version: str = Field(..., description="Version of the plugin") - ui_route: Optional[str] = Field(None, description="Frontend route for the plugin UI") - input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema") -# [/DEF:PluginConfig:Class] \ No newline at end of file +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional +from .logger import belief_scope + +from pydantic import BaseModel, Field + +# #region PluginBase [TYPE Class] [SEMANTICS plugin, interface, base, abstract] +# @BRIEF Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution. +# @LAYER Core +# @INVARIANT All plugins MUST inherit from this class. +# @RELATION Used by PluginLoader to identify valid plugins. +class PluginBase(ABC): + """ + Base class for all plugins. + Plugins must inherit from this class and implement the abstract methods. + """ + + @property + @abstractmethod + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the plugin. + # @PRE Plugin instance exists. + # @POST Returns string ID. + # @RETURN str - Plugin ID. + def id(self) -> str: + """A unique identifier for the plugin.""" + with belief_scope("id"): + pass + # #endregion id + + @property + @abstractmethod + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the plugin. + # @PRE Plugin instance exists. + # @POST Returns string name. + # @RETURN str - Plugin name. + def name(self) -> str: + """A human-readable name for the plugin.""" + with belief_scope("name"): + pass + # #endregion name + + @property + @abstractmethod + # #region description [TYPE Function] + # @BRIEF Returns a brief description of the plugin. + # @PRE Plugin instance exists. + # @POST Returns string description. + # @RETURN str - Plugin description. + def description(self) -> str: + """A brief description of what the plugin does.""" + with belief_scope("description"): + pass + # #endregion description + + @property + @abstractmethod + # #region version [TYPE Function] + # @BRIEF Returns the version of the plugin. + # @PRE Plugin instance exists. + # @POST Returns string version. + # @RETURN str - Plugin version. + def version(self) -> str: + """The version of the plugin.""" + with belief_scope("version"): + pass + # #endregion version + + @property + # #region required_permission [TYPE Function] + # @BRIEF Returns the required permission string to execute this plugin. + # @PRE Plugin instance exists. + # @POST Returns string permission. + # @RETURN str - Required permission (e.g., "plugin:backup:execute"). + def required_permission(self) -> str: + """The permission string required to execute this plugin.""" + with belief_scope("required_permission"): + return f"plugin:{self.id}:execute" + # #endregion required_permission + + @property + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the plugin's UI, if applicable. + # @PRE Plugin instance exists. + # @POST Returns string route or None. + # @RETURN Optional[str] - Frontend route. + def ui_route(self) -> Optional[str]: + """ + The frontend route for the plugin's UI. + Returns None if the plugin does not have a dedicated UI page. + """ + with belief_scope("ui_route"): + return None + # #endregion ui_route + + @abstractmethod + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for the plugin's input parameters. + # @PRE Plugin instance exists. + # @POST Returns dict schema. + # @RETURN Dict[str, Any] - JSON schema. + def get_schema(self) -> Dict[str, Any]: + """ + Returns the JSON schema for the plugin's input parameters. + This schema will be used to generate the frontend form. + """ + with belief_scope("get_schema"): + pass + # #endregion get_schema + + @abstractmethod + # #region execute [TYPE Function] + # @BRIEF Executes the plugin's core logic. + # @PARAM: params (Dict[str, Any]) - Validated input parameters. + # @PRE: params must be a dictionary. + # @POST: Plugin execution is completed. + async def execute(self, params: Dict[str, Any]): + with belief_scope("execute"): + pass + """ + Executes the plugin's logic. + The `params` argument will be validated against the schema returned by `get_schema()`. + """ + pass + # #endregion execute +# #endregion PluginBase + +# #region PluginConfig [TYPE Class] [SEMANTICS plugin, config, schema, pydantic] +# @BRIEF A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer. +# @LAYER Core +# @RELATION Instantiated by PluginLoader after validating a PluginBase instance. +class PluginConfig(BaseModel): + """Pydantic model for plugin configuration.""" + id: str = Field(..., description="Unique identifier for the plugin") + name: str = Field(..., description="Human-readable name for the plugin") + description: str = Field(..., description="Brief description of what the plugin does") + version: str = Field(..., description="Version of the plugin") + ui_route: Optional[str] = Field(None, description="Frontend route for the plugin UI") + input_schema: Dict[str, Any] = Field(..., description="JSON schema for input parameters", alias="schema") +# #endregion PluginConfig diff --git a/backend/src/core/plugin_loader.py b/backend/src/core/plugin_loader.py index b614c7ab..bbd9f07a 100755 --- a/backend/src/core/plugin_loader.py +++ b/backend/src/core/plugin_loader.py @@ -5,22 +5,20 @@ from typing import Dict, List, Optional from .plugin_base import PluginBase, PluginConfig from .logger import belief_scope -# [DEF:PluginLoader:Class] -# @COMPLEXITY: 3 -# @SEMANTICS: plugin, loader, dynamic, import -# @PURPOSE: Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface. -# @LAYER: Core -# @RELATION: Depends on PluginBase. It is used by the main application to discover and manage available plugins. +# #region PluginLoader [C:3] [TYPE Class] [SEMANTICS plugin, loader, dynamic, import] +# @BRIEF Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface. +# @LAYER Core +# @RELATION Depends on PluginBase. It is used by the main application to discover and manage available plugins. class PluginLoader: """ Scans a directory for Python modules, loads them, and identifies classes that inherit from PluginBase. """ - # [DEF:__init__:Function] - # @PURPOSE: Initializes the PluginLoader with a directory to scan. - # @PRE: plugin_dir is a valid directory path. - # @POST: Plugins are loaded and registered. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the PluginLoader with a directory to scan. + # @PRE plugin_dir is a valid directory path. + # @POST Plugins are loaded and registered. # @PARAM: plugin_dir (str) - The directory containing plugin modules. def __init__(self, plugin_dir: str): with belief_scope("__init__"): @@ -28,12 +26,12 @@ class PluginLoader: self._plugins: Dict[str, PluginBase] = {} self._plugin_configs: Dict[str, PluginConfig] = {} self._load_plugins() - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:_load_plugins:Function] - # @PURPOSE: Scans the plugin directory and loads all valid plugins. - # @PRE: plugin_dir exists or can be created. - # @POST: _load_module is called for each .py file. + # #region _load_plugins [TYPE Function] + # @BRIEF Scans the plugin directory and loads all valid plugins. + # @PRE plugin_dir exists or can be created. + # @POST _load_module is called for each .py file. def _load_plugins(self): with belief_scope("_load_plugins"): """ @@ -63,12 +61,12 @@ class PluginLoader: if filename.endswith(".py") and filename != "__init__.py": module_name = filename[:-3] self._load_module(module_name, file_path) - # [/DEF:_load_plugins:Function] + # #endregion _load_plugins - # [DEF:_load_module:Function] - # @PURPOSE: Loads a single Python module and discovers PluginBase implementations. - # @PRE: module_name and file_path are valid. - # @POST: Plugin classes are instantiated and registered. + # #region _load_module [TYPE Function] + # @BRIEF Loads a single Python module and discovers PluginBase implementations. + # @PRE module_name and file_path are valid. + # @POST Plugin classes are instantiated and registered. # @PARAM: module_name (str) - The name of the module. # @PARAM: file_path (str) - The path to the module file. def _load_module(self, module_name: str, file_path: str): @@ -104,12 +102,12 @@ class PluginLoader: self._register_plugin(plugin_instance) except Exception as e: print(f"Error instantiating plugin {attribute_name} in {module_name}: {e}") # Replace with proper logging - # [/DEF:_load_module:Function] + # #endregion _load_module - # [DEF:_register_plugin:Function] - # @PURPOSE: Registers a PluginBase instance and its configuration. - # @PRE: plugin_instance is a valid implementation of PluginBase. - # @POST: Plugin is added to _plugins and _plugin_configs. + # #region _register_plugin [TYPE Function] + # @BRIEF Registers a PluginBase instance and its configuration. + # @PRE plugin_instance is a valid implementation of PluginBase. + # @POST Plugin is added to _plugins and _plugin_configs. # @PARAM: plugin_instance (PluginBase) - The plugin instance to register. def _register_plugin(self, plugin_instance: PluginBase): with belief_scope("_register_plugin"): @@ -145,13 +143,13 @@ class PluginLoader: except Exception as e: from ..core.logger import logger logger.error(f"Error validating plugin '{plugin_instance.name}' (ID: {plugin_id}): {e}") - # [/DEF:_register_plugin:Function] + # #endregion _register_plugin - # [DEF:get_plugin:Function] - # @PURPOSE: Retrieves a loaded plugin instance by its ID. - # @PRE: plugin_id is a string. - # @POST: Returns plugin instance or None. + # #region get_plugin [TYPE Function] + # @BRIEF Retrieves a loaded plugin instance by its ID. + # @PRE plugin_id is a string. + # @POST Returns plugin instance or None. # @PARAM: plugin_id (str) - The unique identifier of the plugin. # @RETURN: Optional[PluginBase] - The plugin instance if found, otherwise None. def get_plugin(self, plugin_id: str) -> Optional[PluginBase]: @@ -160,25 +158,25 @@ class PluginLoader: Returns a loaded plugin instance by its ID. """ return self._plugins.get(plugin_id) - # [/DEF:get_plugin:Function] + # #endregion get_plugin - # [DEF:get_all_plugin_configs:Function] - # @PURPOSE: Returns a list of all registered plugin configurations. - # @PRE: None. - # @POST: Returns list of all PluginConfig objects. - # @RETURN: List[PluginConfig] - A list of plugin configurations. + # #region get_all_plugin_configs [TYPE Function] + # @BRIEF Returns a list of all registered plugin configurations. + # @PRE None. + # @POST Returns list of all PluginConfig objects. + # @RETURN List[PluginConfig] - A list of plugin configurations. def get_all_plugin_configs(self) -> List[PluginConfig]: with belief_scope("get_all_plugin_configs"): """ Returns a list of all loaded plugin configurations. """ return list(self._plugin_configs.values()) - # [/DEF:get_all_plugin_configs:Function] + # #endregion get_all_plugin_configs - # [DEF:has_plugin:Function] - # @PURPOSE: Checks if a plugin with the given ID is registered. - # @PRE: plugin_id is a string. - # @POST: Returns True if plugin exists. + # #region has_plugin [TYPE Function] + # @BRIEF Checks if a plugin with the given ID is registered. + # @PRE plugin_id is a string. + # @POST Returns True if plugin exists. # @PARAM: plugin_id (str) - The unique identifier of the plugin. # @RETURN: bool - True if the plugin is registered, False otherwise. def has_plugin(self, plugin_id: str) -> bool: @@ -187,6 +185,6 @@ class PluginLoader: Checks if a plugin with the given ID is loaded. """ return plugin_id in self._plugins - # [/DEF:has_plugin:Function] + # #endregion has_plugin -# [/DEF:PluginLoader:Class] +# #endregion PluginLoader diff --git a/backend/src/core/scheduler.py b/backend/src/core/scheduler.py index d089b7c5..05cf3602 100644 --- a/backend/src/core/scheduler.py +++ b/backend/src/core/scheduler.py @@ -1,9 +1,7 @@ -# [DEF:SchedulerModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: scheduler, apscheduler, cron, backup -# @PURPOSE: Manages scheduled tasks using APScheduler. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> TaskManager +# #region SchedulerModule [C:3] [TYPE Module] [SEMANTICS scheduler, apscheduler, cron, backup] +# @BRIEF Manages scheduled tasks using APScheduler. +# @LAYER Core +# @RELATION DEPENDS_ON -> [TaskManager] # [SECTION: IMPORTS] from apscheduler.schedulers.background import BackgroundScheduler @@ -19,16 +17,14 @@ from datetime import datetime, time, timedelta, date # [/SECTION] -# [DEF:SchedulerService:Class] -# @COMPLEXITY: 3 -# @SEMANTICS: scheduler, service, apscheduler -# @PURPOSE: Provides a service to manage scheduled backup tasks. -# @RELATION: DEPENDS_ON -> [ThrottledSchedulerConfigurator] +# #region SchedulerService [C:3] [TYPE Class] [SEMANTICS scheduler, service, apscheduler] +# @BRIEF Provides a service to manage scheduled backup tasks. +# @RELATION DEPENDS_ON -> [ThrottledSchedulerConfigurator] class SchedulerService: - # [DEF:__init__:Function] - # @PURPOSE: Initializes the scheduler service with task and config managers. - # @PRE: task_manager and config_manager must be provided. - # @POST: Scheduler instance is created but not started. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the scheduler service with task and config managers. + # @PRE task_manager and config_manager must be provided. + # @POST Scheduler instance is created but not started. def __init__(self, task_manager, config_manager: ConfigManager): with belief_scope("SchedulerService.__init__"): self.task_manager = task_manager @@ -36,12 +32,12 @@ class SchedulerService: self.scheduler = BackgroundScheduler() self.loop = asyncio.get_event_loop() - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:start:Function] - # @PURPOSE: Starts the background scheduler and loads initial schedules. - # @PRE: Scheduler should be initialized. - # @POST: Scheduler is running and schedules are loaded. + # #region start [TYPE Function] + # @BRIEF Starts the background scheduler and loads initial schedules. + # @PRE Scheduler should be initialized. + # @POST Scheduler is running and schedules are loaded. def start(self): with belief_scope("SchedulerService.start"): if not self.scheduler.running: @@ -49,24 +45,24 @@ class SchedulerService: log.reflect("Scheduler started") self.load_schedules() - # [/DEF:start:Function] + # #endregion start - # [DEF:stop:Function] - # @PURPOSE: Stops the background scheduler. - # @PRE: Scheduler should be running. - # @POST: Scheduler is shut down. + # #region stop [TYPE Function] + # @BRIEF Stops the background scheduler. + # @PRE Scheduler should be running. + # @POST Scheduler is shut down. def stop(self): with belief_scope("SchedulerService.stop"): if self.scheduler.running: self.scheduler.shutdown() log.reflect("Scheduler stopped") - # [/DEF:stop:Function] + # #endregion stop - # [DEF:load_schedules:Function] - # @PURPOSE: Loads backup schedules from configuration and registers them. - # @PRE: config_manager must have valid configuration. - # @POST: All enabled backup jobs are added to the scheduler. + # #region load_schedules [TYPE Function] + # @BRIEF Loads backup schedules from configuration and registers them. + # @PRE config_manager must have valid configuration. + # @POST All enabled backup jobs are added to the scheduler. def load_schedules(self): with belief_scope("SchedulerService.load_schedules"): # Clear existing jobs @@ -77,12 +73,12 @@ class SchedulerService: if env.backup_schedule and env.backup_schedule.enabled: self.add_backup_job(env.id, env.backup_schedule.cron_expression) - # [/DEF:load_schedules:Function] + # #endregion load_schedules - # [DEF:add_backup_job:Function] - # @PURPOSE: Adds a scheduled backup job for an environment. - # @PRE: env_id and cron_expression must be valid strings. - # @POST: A new job is added to the scheduler or replaced if it already exists. + # #region add_backup_job [TYPE Function] + # @BRIEF Adds a scheduled backup job for an environment. + # @PRE env_id and cron_expression must be valid strings. + # @POST A new job is added to the scheduler or replaced if it already exists. # @PARAM: env_id (str) - The ID of the environment. # @PARAM: cron_expression (str) - The cron expression for the schedule. def add_backup_job(self, env_id: str, cron_expression: str): @@ -107,12 +103,12 @@ class SchedulerService: log.explore("Failed to add backup job", error=str(e), payload={"env_id": env_id, "cron": cron_expression}) logger.error(f"Failed to add backup job for environment {env_id}: {e}") - # [/DEF:add_backup_job:Function] + # #endregion add_backup_job - # [DEF:_trigger_backup:Function] - # @PURPOSE: Triggered by the scheduler to start a backup task. - # @PRE: env_id must be a valid environment ID. - # @POST: A new backup task is created in the task manager if not already running. + # #region _trigger_backup [TYPE Function] + # @BRIEF Triggered by the scheduler to start a backup task. + # @PRE env_id must be a valid environment ID. + # @POST A new backup task is created in the task manager if not already running. # @PARAM: env_id (str) - The ID of the environment. def _trigger_backup(self, env_id: str): with belief_scope("SchedulerService._trigger_backup", f"env_id={env_id}"): @@ -142,28 +138,26 @@ class SchedulerService: asyncio.run_coroutine_threadsafe(_backup_task(), self.loop) - # [/DEF:_trigger_backup:Function] + # #endregion _trigger_backup -# [/DEF:SchedulerService:Class] +# #endregion SchedulerService -# [DEF:ThrottledSchedulerConfigurator:Class] -# @COMPLEXITY: 5 -# @SEMANTICS: scheduler, throttling, distribution -# @PURPOSE: Distributes validation tasks evenly within an execution window. -# @PRE: Validation policies provide a finite dashboard list and a valid execution window. -# @POST: Produces deterministic per-dashboard run timestamps within the configured window. -# @RELATION: DEPENDS_ON -> SchedulerModule -# @INVARIANT: Returned schedule size always matches number of dashboard IDs. -# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows. -# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]] +# #region ThrottledSchedulerConfigurator [C:5] [TYPE Class] [SEMANTICS scheduler, throttling, distribution] +# @BRIEF Distributes validation tasks evenly within an execution window. +# @PRE Validation policies provide a finite dashboard list and a valid execution window. +# @POST Produces deterministic per-dashboard run timestamps within the configured window. +# @SIDE_EFFECT Emits warning logs for degenerate or near-zero scheduling windows. +# @DATA_CONTRACT Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]] +# @INVARIANT Returned schedule size always matches number of dashboard IDs. +# @RELATION DEPENDS_ON -> [SchedulerModule] class ThrottledSchedulerConfigurator: - # [DEF:calculate_schedule:Function] - # @PURPOSE: Calculates execution times for N tasks within a window. - # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date). - # @POST: Returns List[datetime] of scheduled times. - # @INVARIANT: Tasks are distributed with near-even spacing. + # #region calculate_schedule [TYPE Function] + # @BRIEF Calculates execution times for N tasks within a window. + # @PRE window_start, window_end (time), dashboard_ids (List), current_date (date). + # @POST Returns List[datetime] of scheduled times. + # @INVARIANT Tasks are distributed with near-even spacing. @staticmethod def calculate_schedule( window_start: time, window_end: time, dashboard_ids: list, current_date: date @@ -207,9 +201,9 @@ class ThrottledSchedulerConfigurator: return scheduled_times - # [/DEF:calculate_schedule:Function] + # #endregion calculate_schedule -# [/DEF:ThrottledSchedulerConfigurator:Class] +# #endregion ThrottledSchedulerConfigurator -# [/DEF:SchedulerModule:Module] +# #endregion SchedulerModule diff --git a/backend/src/core/scheduler.py.bak b/backend/src/core/scheduler.py.bak new file mode 100644 index 00000000..ea653ded --- /dev/null +++ b/backend/src/core/scheduler.py.bak @@ -0,0 +1,215 @@ +# [DEF:SchedulerModule:Module] +# @COMPLEXITY: 3 +# @SEMANTICS: scheduler, apscheduler, cron, backup +# @PURPOSE: Manages scheduled tasks using APScheduler. +# @LAYER: Core +# @RELATION: [DEPENDS_ON] ->[TaskManager] + +# [SECTION: IMPORTS] +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from .logger import logger, belief_scope +from .config_manager import ConfigManager +from .cot_logger import MarkerLogger, get_trace_id, set_trace_id + +log = MarkerLogger("SchedulerService") +log_tsc = MarkerLogger("ThrottledSchedulerConfigurator") +import asyncio +from datetime import datetime, time, timedelta, date +# [/SECTION] + + +# [DEF:SchedulerService:Class] +# @COMPLEXITY: 3 +# @SEMANTICS: scheduler, service, apscheduler +# @PURPOSE: Provides a service to manage scheduled backup tasks. +# @RELATION: DEPENDS_ON -> [ThrottledSchedulerConfigurator] +class SchedulerService: + # [DEF:__init__:Function] + # @PURPOSE: Initializes the scheduler service with task and config managers. + # @PRE: task_manager and config_manager must be provided. + # @POST: Scheduler instance is created but not started. + def __init__(self, task_manager, config_manager: ConfigManager): + with belief_scope("SchedulerService.__init__"): + self.task_manager = task_manager + self.config_manager = config_manager + self.scheduler = BackgroundScheduler() + self.loop = asyncio.get_event_loop() + + # [/DEF:__init__:Function] + + # [DEF:start:Function] + # @PURPOSE: Starts the background scheduler and loads initial schedules. + # @PRE: Scheduler should be initialized. + # @POST: Scheduler is running and schedules are loaded. + def start(self): + with belief_scope("SchedulerService.start"): + if not self.scheduler.running: + self.scheduler.start() + log.reflect("Scheduler started") + self.load_schedules() + + # [/DEF:start:Function] + + # [DEF:stop:Function] + # @PURPOSE: Stops the background scheduler. + # @PRE: Scheduler should be running. + # @POST: Scheduler is shut down. + def stop(self): + with belief_scope("SchedulerService.stop"): + if self.scheduler.running: + self.scheduler.shutdown() + log.reflect("Scheduler stopped") + + # [/DEF:stop:Function] + + # [DEF:load_schedules:Function] + # @PURPOSE: Loads backup schedules from configuration and registers them. + # @PRE: config_manager must have valid configuration. + # @POST: All enabled backup jobs are added to the scheduler. + def load_schedules(self): + with belief_scope("SchedulerService.load_schedules"): + # Clear existing jobs + self.scheduler.remove_all_jobs() + + config = self.config_manager.get_config() + for env in config.environments: + if env.backup_schedule and env.backup_schedule.enabled: + self.add_backup_job(env.id, env.backup_schedule.cron_expression) + + # [/DEF:load_schedules:Function] + + # [DEF:add_backup_job:Function] + # @PURPOSE: Adds a scheduled backup job for an environment. + # @PRE: env_id and cron_expression must be valid strings. + # @POST: A new job is added to the scheduler or replaced if it already exists. + # @PARAM: env_id (str) - The ID of the environment. + # @PARAM: cron_expression (str) - The cron expression for the schedule. + def add_backup_job(self, env_id: str, cron_expression: str): + with belief_scope( + "SchedulerService.add_backup_job", + f"env_id={env_id}, cron={cron_expression}", + ): + job_id = f"backup_{env_id}" + try: + self.scheduler.add_job( + self._trigger_backup, + CronTrigger.from_crontab(cron_expression), + id=job_id, + args=[env_id], + replace_existing=True, + ) + log.reflect( + "Scheduled backup job added", + payload={"env_id": env_id, "cron": cron_expression}, + ) + except Exception as e: + log.explore("Failed to add backup job", error=str(e), payload={"env_id": env_id, "cron": cron_expression}) + logger.error(f"Failed to add backup job for environment {env_id}: {e}") + + # [/DEF:add_backup_job:Function] + + # [DEF:_trigger_backup:Function] + # @PURPOSE: Triggered by the scheduler to start a backup task. + # @PRE: env_id must be a valid environment ID. + # @POST: A new backup task is created in the task manager if not already running. + # @PARAM: env_id (str) - The ID of the environment. + def _trigger_backup(self, env_id: str): + with belief_scope("SchedulerService._trigger_backup", f"env_id={env_id}"): + log.reason("Triggering scheduled backup", payload={"env_id": env_id}) + + # Check if a backup is already running for this environment + active_tasks = self.task_manager.get_tasks(limit=100) + for task in active_tasks: + if ( + task.plugin_id == "superset-backup" + and task.status in ["PENDING", "RUNNING"] + and task.params.get("environment_id") == env_id + ): + log.explore("Backup already running, skipping scheduled run", error="Duplicate backup prevented", payload={"env_id": env_id}) + return + + # Run the backup task + # We need to run this in the event loop since create_task is async + trace_id = get_trace_id() + + async def _backup_task(): + if trace_id: + set_trace_id(trace_id) + await self.task_manager.create_task( + "superset-backup", {"environment_id": env_id} + ) + + asyncio.run_coroutine_threadsafe(_backup_task(), self.loop) + + # [/DEF:_trigger_backup:Function] + + +# [/DEF:SchedulerService:Class] + + +# [DEF:ThrottledSchedulerConfigurator:Class] +# @COMPLEXITY: 5 +# @SEMANTICS: scheduler, throttling, distribution +# @PURPOSE: Distributes validation tasks evenly within an execution window. +# @PRE: Validation policies provide a finite dashboard list and a valid execution window. +# @POST: Produces deterministic per-dashboard run timestamps within the configured window. +# @RELATION: [DEPENDS_ON] ->[SchedulerModule] +# @INVARIANT: Returned schedule size always matches number of dashboard IDs. +# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows. +# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]] +class ThrottledSchedulerConfigurator: + # [DEF:calculate_schedule:Function] + # @PURPOSE: Calculates execution times for N tasks within a window. + # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date). + # @POST: Returns List[datetime] of scheduled times. + # @INVARIANT: Tasks are distributed with near-even spacing. + @staticmethod + def calculate_schedule( + window_start: time, window_end: time, dashboard_ids: list, current_date: date + ) -> list: + with belief_scope("ThrottledSchedulerConfigurator.calculate_schedule"): + n = len(dashboard_ids) + if n == 0: + return [] + + start_dt = datetime.combine(current_date, window_start) + end_dt = datetime.combine(current_date, window_end) + + # Handle window crossing midnight + if end_dt < start_dt: + end_dt += timedelta(days=1) + + total_seconds = (end_dt - start_dt).total_seconds() + + # Minimum interval of 1 second to avoid division by zero or negative + if total_seconds <= 0: + log_tsc.explore("Window size is zero or negative, falling back to start time", error=f"total_seconds={total_seconds}", payload={"n": n}) + return [start_dt] * n + + # If window is too small for even distribution (e.g. 10 tasks in 5 seconds), + # we still distribute them but they might be very close. + # The requirement says "near-even spacing". + + if n == 1: + return [start_dt] + + interval = total_seconds / (n - 1) if n > 1 else 0 + + # If interval is too small (e.g. < 1s), we might want a fallback, + # but the spec says "handle too-small windows with explicit fallback/warning". + if interval < 1: + log_tsc.explore("Window too small for task distribution", error=f"interval={interval:.2f}s", payload={"n": n}) + + scheduled_times = [] + for i in range(n): + scheduled_times.append(start_dt + timedelta(seconds=i * interval)) + + return scheduled_times + + # [/DEF:calculate_schedule:Function] + + +# [/DEF:ThrottledSchedulerConfigurator:Class] + +# [/DEF:SchedulerModule:Module] diff --git a/backend/src/core/superset_client/__init__.py b/backend/src/core/superset_client/__init__.py index d06e07ae..49c825c6 100644 --- a/backend/src/core/superset_client/__init__.py +++ b/backend/src/core/superset_client/__init__.py @@ -1,15 +1,13 @@ -# [DEF:SupersetClientModule:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, api, client, rest, http, dashboard, dataset, import, export -# @PURPOSE: Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию. -# @RELATION: DEPENDS_ON -> [ConfigModels] -# @RELATION: DEPENDS_ON -> [APIClient] -# @RELATION: DEPENDS_ON -> [SupersetAPIError] -# @RELATION: DEPENDS_ON -> [get_filename_from_headers] -# @RELATION: DEPENDS_ON -> [SupersetDatasetsPreviewFiltersMixin] +# #region SupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, api, client, rest, http, dashboard, dataset, import, export] +# @BRIEF Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию. +# @LAYER Infra +# @INVARIANT All network operations must use the internal APIClient instance. +# @RELATION DEPENDS_ON -> [ConfigModels] +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetAPIError] +# @RELATION DEPENDS_ON -> [get_filename_from_headers] +# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewFiltersMixin] # -# @INVARIANT: All network operations must use the internal APIClient instance. # @PUBLIC_API: SupersetClient # # @RATIONALE: Decomposed from monolithic superset_client.py (2145 lines) into @@ -30,22 +28,21 @@ from ._datasets_preview_filters import SupersetDatasetsPreviewFiltersMixin from ._databases import SupersetDatabasesMixin -# [DEF:SupersetClient:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами. -# @RELATION: DEPENDS_ON -> [ConfigModels] -# @RELATION: DEPENDS_ON -> [APIClient] -# @RELATION: DEPENDS_ON -> [SupersetAPIError] -# @RELATION: INHERITS -> [SupersetClientBase] -# @RELATION: INHERITS -> [SupersetUserProjectionMixin] -# @RELATION: INHERITS -> [SupersetDashboardsListMixin] -# @RELATION: INHERITS -> [SupersetDashboardsFiltersMixin] -# @RELATION: INHERITS -> [SupersetDashboardsCrudMixin] -# @RELATION: INHERITS -> [SupersetChartsMixin] -# @RELATION: INHERITS -> [SupersetDatasetsMixin] -# @RELATION: INHERITS -> [SupersetDatasetsPreviewMixin] -# @RELATION: INHERITS -> [SupersetDatasetsPreviewFiltersMixin] -# @RELATION: INHERITS -> [SupersetDatabasesMixin] +# #region SupersetClient [C:3] [TYPE Class] +# @BRIEF Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами. +# @RELATION DEPENDS_ON -> [ConfigModels] +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetAPIError] +# @RELATION INHERITS -> [SupersetClientBase] +# @RELATION INHERITS -> [SupersetUserProjectionMixin] +# @RELATION INHERITS -> [SupersetDashboardsListMixin] +# @RELATION INHERITS -> [SupersetDashboardsFiltersMixin] +# @RELATION INHERITS -> [SupersetDashboardsCrudMixin] +# @RELATION INHERITS -> [SupersetChartsMixin] +# @RELATION INHERITS -> [SupersetDatasetsMixin] +# @RELATION INHERITS -> [SupersetDatasetsPreviewMixin] +# @RELATION INHERITS -> [SupersetDatasetsPreviewFiltersMixin] +# @RELATION INHERITS -> [SupersetDatabasesMixin] class SupersetClient( SupersetDatabasesMixin, SupersetDatasetsPreviewFiltersMixin, @@ -66,6 +63,6 @@ class SupersetClient( pass -# [/DEF:SupersetClient:Class] +# #endregion SupersetClient -# [/DEF:SupersetClientModule:Module] +# #endregion SupersetClientModule diff --git a/backend/src/core/superset_client/_base.py b/backend/src/core/superset_client/_base.py index 9cffc1bf..eaf33407 100644 --- a/backend/src/core/superset_client/_base.py +++ b/backend/src/core/superset_client/_base.py @@ -1,12 +1,10 @@ -# [DEF:SupersetClientBase:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, api, client, base, pagination, auth, import, export -# @PURPOSE: Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers. -# @RELATION: DEPENDS_ON -> [ConfigModels] -# @RELATION: DEPENDS_ON -> [APIClient] -# @RELATION: DEPENDS_ON -> [SupersetAPIError] -# @RELATION: DEPENDS_ON -> [get_filename_from_headers] +# #region SupersetClientBase [C:3] [TYPE Module] [SEMANTICS superset, api, client, base, pagination, auth, import, export] +# @BRIEF Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [ConfigModels] +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetAPIError] +# @RELATION DEPENDS_ON -> [get_filename_from_headers] import json import zipfile @@ -23,18 +21,16 @@ from ..config_models import Environment log = MarkerLogger("SupersetClientBase") -# [DEF:SupersetClientBase:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Base class providing Superset client initialization, auth, pagination, and import/export plumbing. -# @RELATION: DEPENDS_ON -> [ConfigModels] -# @RELATION: DEPENDS_ON -> [APIClient] -# @RELATION: DEPENDS_ON -> [SupersetAPIError] +# #region SupersetClientBase [C:3] [TYPE Class] +# @BRIEF Base class providing Superset client initialization, auth, pagination, and import/export plumbing. +# @RELATION DEPENDS_ON -> [ConfigModels] +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetAPIError] class SupersetClientBase: - # [DEF:SupersetClientInit:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент. - # @RELATION: DEPENDS_ON -> [Environment] - # @RELATION: DEPENDS_ON -> [APIClient] + # #region SupersetClientInit [C:3] [TYPE Function] + # @BRIEF Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент. + # @RELATION DEPENDS_ON -> [Environment] + # @RELATION DEPENDS_ON -> [APIClient] def __init__(self, env: Environment): with belief_scope("SupersetClientInit"): log.reason( @@ -60,12 +56,11 @@ class SupersetClientBase: payload={"environment": getattr(self.env, "id", None)}, ) - # [/DEF:SupersetClientInit:Function] + # #endregion SupersetClientInit - # [DEF:SupersetClientAuthenticate:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Authenticates the client using the configured credentials. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientAuthenticate [C:3] [TYPE Function] + # @BRIEF Authenticates the client using the configured credentials. + # @RELATION CALLS -> [APIClient] def authenticate(self) -> Dict[str, str]: with belief_scope("SupersetClientAuthenticate"): log.reason( @@ -81,23 +76,21 @@ class SupersetClientBase: }, ) return tokens - # [/DEF:SupersetClientAuthenticate:Function] + # #endregion SupersetClientAuthenticate @property - # [DEF:SupersetClientHeaders:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом. + # #region SupersetClientHeaders [C:1] [TYPE Function] + # @BRIEF Возвращает базовые HTTP-заголовки, используемые сетевым клиентом. def headers(self) -> dict: with belief_scope("headers"): return self.network.headers - # [/DEF:SupersetClientHeaders:Function] + # #endregion SupersetClientHeaders # --- Pagination helpers --- - # [DEF:SupersetClientValidateQueryParams:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Ensures query parameters have default page and page_size. + # #region SupersetClientValidateQueryParams [C:1] [TYPE Function] + # @BRIEF Ensures query parameters have default page and page_size. def _validate_query_params(self, query: Optional[Dict]) -> Dict: with belief_scope("_validate_query_params"): # Superset list endpoints commonly cap page_size at 100. @@ -105,12 +98,11 @@ class SupersetClientBase: base_query = {"page": 0, "page_size": 100} return {**base_query, **(query or {})} - # [/DEF:SupersetClientValidateQueryParams:Function] + # #endregion SupersetClientValidateQueryParams - # [DEF:SupersetClientFetchTotalObjectCount:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Fetches the total number of items for a given endpoint. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientFetchTotalObjectCount [C:1] [TYPE Function] + # @BRIEF Fetches the total number of items for a given endpoint. + # @RELATION CALLS -> [APIClient] def _fetch_total_object_count(self, endpoint: str) -> int: with belief_scope("_fetch_total_object_count"): return self.network.fetch_paginated_count( @@ -119,26 +111,24 @@ class SupersetClientBase: count_field="count", ) - # [/DEF:SupersetClientFetchTotalObjectCount:Function] + # #endregion SupersetClientFetchTotalObjectCount - # [DEF:SupersetClientFetchAllPages:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Iterates through all pages to collect all data items. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientFetchAllPages [C:1] [TYPE Function] + # @BRIEF Iterates through all pages to collect all data items. + # @RELATION CALLS -> [APIClient] def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]: with belief_scope("_fetch_all_pages"): return self.network.fetch_paginated_data( endpoint=endpoint, pagination_options=pagination_options ) - # [/DEF:SupersetClientFetchAllPages:Function] + # #endregion SupersetClientFetchAllPages # --- Import/Export helpers --- - # [DEF:SupersetClientDoImport:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Performs the actual multipart upload for import. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientDoImport [C:1] [TYPE Function] + # @BRIEF Performs the actual multipart upload for import. + # @RELATION CALLS -> [APIClient] def _do_import(self, file_name: Union[str, Path]) -> Dict: with belief_scope("_do_import"): log.reason("Uploading file for import", payload={"file_name": str(file_name)}) @@ -158,11 +148,10 @@ class SupersetClientBase: timeout=self.env.timeout * 2, ) - # [/DEF:SupersetClientDoImport:Function] + # #endregion SupersetClientDoImport - # [DEF:SupersetClientValidateExportResponse:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Validates that the export response is a non-empty ZIP archive. + # #region SupersetClientValidateExportResponse [C:1] [TYPE Function] + # @BRIEF Validates that the export response is a non-empty ZIP archive. def _validate_export_response(self, response: Response, dashboard_id: int) -> None: with belief_scope("_validate_export_response"): content_type = response.headers.get("Content-Type", "") @@ -173,11 +162,10 @@ class SupersetClientBase: if not response.content: raise SupersetAPIError("Получены пустые данные при экспорте") - # [/DEF:SupersetClientValidateExportResponse:Function] + # #endregion SupersetClientValidateExportResponse - # [DEF:SupersetClientResolveExportFilename:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Determines the filename for an exported dashboard. + # #region SupersetClientResolveExportFilename [C:1] [TYPE Function] + # @BRIEF Determines the filename for an exported dashboard. def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str: with belief_scope("_resolve_export_filename"): filename = get_filename_from_headers(dict(response.headers)) @@ -192,11 +180,10 @@ class SupersetClientBase: ) return filename - # [/DEF:SupersetClientResolveExportFilename:Function] + # #endregion SupersetClientResolveExportFilename - # [DEF:SupersetClientValidateImportFile:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml. + # #region SupersetClientValidateImportFile [C:1] [TYPE Function] + # @BRIEF Validates that the file to be imported is a valid ZIP with metadata.yaml. def _validate_import_file(self, zip_path: Union[str, Path]) -> None: with belief_scope("_validate_import_file"): path = Path(zip_path) @@ -210,12 +197,11 @@ class SupersetClientBase: f"Архив {zip_path} не содержит 'metadata.yaml'" ) - # [/DEF:SupersetClientValidateImportFile:Function] + # #endregion SupersetClientValidateImportFile - # [DEF:SupersetClientResolveTargetIdForDelete:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Resolves a dashboard ID from either an ID or a slug. - # @RELATION: CALLS -> [SupersetClientGetDashboards] + # #region SupersetClientResolveTargetIdForDelete [C:1] [TYPE Function] + # @BRIEF Resolves a dashboard ID from either an ID or a slug. + # @RELATION CALLS -> [SupersetClientGetDashboards] def _resolve_target_id_for_delete( self, dash_id: Optional[int], dash_slug: Optional[str] ) -> Optional[int]: @@ -238,12 +224,11 @@ class SupersetClientBase: log.explore("Could not resolve slug to dashboard ID", error=str(e), payload={"slug": dash_slug}) return None - # [/DEF:SupersetClientResolveTargetIdForDelete:Function] + # #endregion SupersetClientResolveTargetIdForDelete - # [DEF:SupersetClientGetAllResources:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches all resources of a given type with id, uuid, and name columns. - # @RELATION: CALLS -> [SupersetClientFetchAllPages] + # #region SupersetClientGetAllResources [C:3] [TYPE Function] + # @BRIEF Fetches all resources of a given type with id, uuid, and name columns. + # @RELATION CALLS -> [SupersetClientFetchAllPages] def get_all_resources( self, resource_type: str, since_dttm: Optional["datetime"] = None ) -> List[Dict]: @@ -293,8 +278,7 @@ class SupersetClientBase: ) return data - # [/DEF:SupersetClientGetAllResources:Function] + # #endregion SupersetClientGetAllResources -# [/DEF:SupersetClientBase:Class] -# [/DEF:SupersetClientBase:Module] +# #endregion SupersetClientBase diff --git a/backend/src/core/superset_client/_charts.py b/backend/src/core/superset_client/_charts.py index 6114adbd..058ed7dd 100644 --- a/backend/src/core/superset_client/_charts.py +++ b/backend/src/core/superset_client/_charts.py @@ -1,9 +1,7 @@ -# [DEF:SupersetChartsMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, charts, list, get, layout -# @PURPOSE: Chart domain mixin for SupersetClient — list, get, extract IDs from layout. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetChartsMixin [C:3] [TYPE Module] [SEMANTICS superset, charts, list, get, layout] +# @BRIEF Chart domain mixin for SupersetClient — list, get, extract IDs from layout. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] import json import re @@ -14,26 +12,23 @@ from ..logger import logger as app_logger, belief_scope app_logger = cast(Any, app_logger) -# [DEF:SupersetChartsMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing all chart-related Superset API operations. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetChartsMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing all chart-related Superset API operations. +# @RELATION DEPENDS_ON -> [SupersetClientBase] class SupersetChartsMixin: - # [DEF:SupersetClientGetChart:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches a single chart by ID. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetChart [C:3] [TYPE Function] + # @BRIEF Fetches a single chart by ID. + # @RELATION CALLS -> [APIClient] def get_chart(self, chart_id: int) -> Dict: with belief_scope("SupersetClient.get_chart", f"id={chart_id}"): response = self.network.request(method="GET", endpoint=f"/chart/{chart_id}") return cast(Dict, response) - # [/DEF:SupersetClientGetChart:Function] + # #endregion SupersetClientGetChart - # [DEF:SupersetClientGetCharts:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches all charts with pagination support. - # @RELATION: CALLS -> [SupersetClientFetchAllPages] + # #region SupersetClientGetCharts [C:3] [TYPE Function] + # @BRIEF Fetches all charts with pagination support. + # @RELATION CALLS -> [SupersetClientFetchAllPages] def get_charts(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: with belief_scope("get_charts"): validated_query = self._validate_query_params(query or {}) @@ -49,11 +44,10 @@ class SupersetChartsMixin: ) return len(paginated_data), paginated_data - # [/DEF:SupersetClientGetCharts:Function] + # #endregion SupersetClientGetCharts - # [DEF:SupersetClientExtractChartIdsFromLayout:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys. + # #region SupersetClientExtractChartIdsFromLayout [C:1] [TYPE Function] + # @BRIEF Traverses dashboard layout metadata and extracts chart IDs from common keys. def _extract_chart_ids_from_layout( self, payload: Any ) -> set: @@ -83,8 +77,7 @@ class SupersetChartsMixin: walk(payload) return found - # [/DEF:SupersetClientExtractChartIdsFromLayout:Function] + # #endregion SupersetClientExtractChartIdsFromLayout -# [/DEF:SupersetChartsMixin:Class] -# [/DEF:SupersetChartsMixin:Module] +# #endregion SupersetChartsMixin diff --git a/backend/src/core/superset_client/_dashboards_crud.py b/backend/src/core/superset_client/_dashboards_crud.py index 81580d93..9235b516 100644 --- a/backend/src/core/superset_client/_dashboards_crud.py +++ b/backend/src/core/superset_client/_dashboards_crud.py @@ -1,11 +1,9 @@ -# [DEF:SupersetDashboardsCrudMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, dashboards, crud, detail, export, import, delete -# @PURPOSE: Dashboard CRUD mixin for SupersetClient — detail, export, import, delete. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetDashboardsFiltersMixin] -# @RELATION: DEPENDS_ON -> [SupersetChartsMixin] +# #region SupersetDashboardsCrudMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboards, crud, detail, export, import, delete] +# @BRIEF Dashboard CRUD mixin for SupersetClient — detail, export, import, delete. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetDashboardsFiltersMixin] +# @RELATION DEPENDS_ON -> [SupersetChartsMixin] import json import re @@ -19,18 +17,16 @@ from ..logger import belief_scope log = MarkerLogger("SupersetDashboardsCrud") -# [DEF:SupersetDashboardsCrudMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing dashboard detail resolution, export, import, and delete operations. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetDashboardsFiltersMixin] -# @RELATION: DEPENDS_ON -> [SupersetChartsMixin] +# #region SupersetDashboardsCrudMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing dashboard detail resolution, export, import, and delete operations. +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetDashboardsFiltersMixin] +# @RELATION DEPENDS_ON -> [SupersetChartsMixin] class SupersetDashboardsCrudMixin: - # [DEF:SupersetClientGetDashboardDetail:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches detailed dashboard information including related charts and datasets. - # @RELATION: CALLS -> [SupersetClientGetDashboard] - # @RELATION: CALLS -> [SupersetClientGetChart] + # #region SupersetClientGetDashboardDetail [C:3] [TYPE Function] + # @BRIEF Fetches detailed dashboard information including related charts and datasets. + # @RELATION CALLS -> [SupersetClientGetDashboard] + # @RELATION CALLS -> [SupersetClientGetChart] def get_dashboard_detail(self, dashboard_ref: Union[int, str]) -> Dict: with belief_scope( "SupersetClient.get_dashboard_detail", f"ref={dashboard_ref}" @@ -266,13 +262,12 @@ class SupersetDashboardsCrudMixin: "dataset_count": len(unique_datasets), } - # [/DEF:SupersetClientGetDashboardDetail:Function] + # #endregion SupersetClientGetDashboardDetail - # [DEF:SupersetClientExportDashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Экспортирует дашборд в виде ZIP-архива. - # @SIDE_EFFECT: Performs network I/O to download archive. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientExportDashboard [C:3] [TYPE Function] + # @BRIEF Экспортирует дашборд в виде ZIP-архива. + # @SIDE_EFFECT Performs network I/O to download archive. + # @RELATION CALLS -> [APIClient] def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]: with belief_scope("export_dashboard"): log.reason("Exporting dashboard", payload={"dashboard_id": dashboard_id}) @@ -289,14 +284,13 @@ class SupersetDashboardsCrudMixin: log.reflect("Dashboard exported", payload={"dashboard_id": dashboard_id, "filename": filename}) return response.content, filename - # [/DEF:SupersetClientExportDashboard:Function] + # #endregion SupersetClientExportDashboard - # [DEF:SupersetClientImportDashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Импортирует дашборд из ZIP-файла. - # @SIDE_EFFECT: Performs network I/O to upload archive. - # @RELATION: CALLS -> [SupersetClientDoImport] - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientImportDashboard [C:3] [TYPE Function] + # @BRIEF Импортирует дашборд из ZIP-файла. + # @SIDE_EFFECT Performs network I/O to upload archive. + # @RELATION CALLS -> [SupersetClientDoImport] + # @RELATION CALLS -> [APIClient] def import_dashboard( self, file_name: Union[str, Path], @@ -327,13 +321,12 @@ class SupersetDashboardsCrudMixin: ) return self._do_import(file_path) - # [/DEF:SupersetClientImportDashboard:Function] + # #endregion SupersetClientImportDashboard - # [DEF:SupersetClientDeleteDashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Удаляет дашборд по его ID или slug. - # @SIDE_EFFECT: Deletes resource from upstream Superset environment. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientDeleteDashboard [C:3] [TYPE Function] + # @BRIEF Удаляет дашборд по его ID или slug. + # @SIDE_EFFECT Deletes resource from upstream Superset environment. + # @RELATION CALLS -> [APIClient] def delete_dashboard(self, dashboard_id: Union[int, str]) -> None: with belief_scope("delete_dashboard"): log.reason("Deleting dashboard", payload={"dashboard_id": dashboard_id}) @@ -346,8 +339,7 @@ class SupersetDashboardsCrudMixin: else: log.explore("Unexpected response while deleting dashboard", error=f"Unexpected API response: {response}", payload={"dashboard_id": dashboard_id, "response": response}) - # [/DEF:SupersetClientDeleteDashboard:Function] + # #endregion SupersetClientDeleteDashboard -# [/DEF:SupersetDashboardsCrudMixin:Class] -# [/DEF:SupersetDashboardsCrudMixin:Module] +# #endregion SupersetDashboardsCrudMixin diff --git a/backend/src/core/superset_client/_dashboards_filters.py b/backend/src/core/superset_client/_dashboards_filters.py index 5d733b21..cdff3fab 100644 --- a/backend/src/core/superset_client/_dashboards_filters.py +++ b/backend/src/core/superset_client/_dashboards_filters.py @@ -1,9 +1,7 @@ -# [DEF:SupersetDashboardsFiltersMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, dashboards, filters, permalink, native-filters -# @PURPOSE: Dashboard native filter extraction mixin for SupersetClient. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDashboardsFiltersMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboards, filters, permalink, native-filters] +# @BRIEF Dashboard native filter extraction mixin for SupersetClient. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] import json from typing import Any, Dict, Optional, Union, cast @@ -14,15 +12,13 @@ from ..logger import belief_scope log = MarkerLogger("SupersetDashboardsFilters") -# [DEF:SupersetDashboardsFiltersMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing dashboard native filter extraction from permalink and URL state. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDashboardsFiltersMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing dashboard native filter extraction from permalink and URL state. +# @RELATION DEPENDS_ON -> [SupersetClientBase] class SupersetDashboardsFiltersMixin: - # [DEF:SupersetClientGetDashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches a single dashboard by ID or slug. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDashboard [C:3] [TYPE Function] + # @BRIEF Fetches a single dashboard by ID or slug. + # @RELATION CALLS -> [APIClient] def get_dashboard(self, dashboard_ref: Union[int, str]) -> Dict: with belief_scope("SupersetClient.get_dashboard", f"ref={dashboard_ref}"): response = self.network.request( @@ -30,12 +26,11 @@ class SupersetDashboardsFiltersMixin: ) return cast(Dict, response) - # [/DEF:SupersetClientGetDashboard:Function] + # #endregion SupersetClientGetDashboard - # [DEF:SupersetClientGetDashboardPermalinkState:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Fetches stored dashboard permalink state by permalink key. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDashboardPermalinkState [C:2] [TYPE Function] + # @BRIEF Fetches stored dashboard permalink state by permalink key. + # @RELATION CALLS -> [APIClient] def get_dashboard_permalink_state(self, permalink_key: str) -> Dict: with belief_scope( "SupersetClient.get_dashboard_permalink_state", f"key={permalink_key}" @@ -45,12 +40,11 @@ class SupersetDashboardsFiltersMixin: ) return cast(Dict, response) - # [/DEF:SupersetClientGetDashboardPermalinkState:Function] + # #endregion SupersetClientGetDashboardPermalinkState - # [DEF:SupersetClientGetNativeFilterState:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Fetches stored native filter state by filter state key. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetNativeFilterState [C:2] [TYPE Function] + # @BRIEF Fetches stored native filter state by filter state key. + # @RELATION CALLS -> [APIClient] def get_native_filter_state( self, dashboard_id: Union[int, str], filter_state_key: str ) -> Dict: @@ -64,12 +58,11 @@ class SupersetDashboardsFiltersMixin: ) return cast(Dict, response) - # [/DEF:SupersetClientGetNativeFilterState:Function] + # #endregion SupersetClientGetNativeFilterState - # [DEF:SupersetClientExtractNativeFiltersFromPermalink:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract native filters dataMask from a permalink key. - # @RELATION: CALLS -> [SupersetClientGetDashboardPermalinkState] + # #region SupersetClientExtractNativeFiltersFromPermalink [C:3] [TYPE Function] + # @BRIEF Extract native filters dataMask from a permalink key. + # @RELATION CALLS -> [SupersetClientGetDashboardPermalinkState] def extract_native_filters_from_permalink(self, permalink_key: str) -> Dict: with belief_scope( "SupersetClient.extract_native_filters_from_permalink", @@ -99,12 +92,11 @@ class SupersetDashboardsFiltersMixin: "permalink_key": permalink_key, } - # [/DEF:SupersetClientExtractNativeFiltersFromPermalink:Function] + # #endregion SupersetClientExtractNativeFiltersFromPermalink - # [DEF:SupersetClientExtractNativeFiltersFromKey:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract native filters from a native_filters_key URL parameter. - # @RELATION: CALLS -> [SupersetClientGetNativeFilterState] + # #region SupersetClientExtractNativeFiltersFromKey [C:3] [TYPE Function] + # @BRIEF Extract native filters from a native_filters_key URL parameter. + # @RELATION CALLS -> [SupersetClientGetNativeFilterState] def extract_native_filters_from_key( self, dashboard_id: Union[int, str], filter_state_key: str ) -> Dict: @@ -155,13 +147,12 @@ class SupersetDashboardsFiltersMixin: "filter_state_key": filter_state_key, } - # [/DEF:SupersetClientExtractNativeFiltersFromKey:Function] + # #endregion SupersetClientExtractNativeFiltersFromKey - # [DEF:SupersetClientParseDashboardUrlForFilters:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Parse a Superset dashboard URL and extract native filter state if present. - # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromPermalink] - # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromKey] + # #region SupersetClientParseDashboardUrlForFilters [C:3] [TYPE Function] + # @BRIEF Parse a Superset dashboard URL and extract native filter state if present. + # @RELATION CALLS -> [SupersetClientExtractNativeFiltersFromPermalink] + # @RELATION CALLS -> [SupersetClientExtractNativeFiltersFromKey] def parse_dashboard_url_for_filters(self, url: str) -> Dict: with belief_scope( "SupersetClient.parse_dashboard_url_for_filters", f"url={url}" @@ -250,8 +241,7 @@ class SupersetDashboardsFiltersMixin: return result - # [/DEF:SupersetClientParseDashboardUrlForFilters:Function] + # #endregion SupersetClientParseDashboardUrlForFilters -# [/DEF:SupersetDashboardsFiltersMixin:Class] -# [/DEF:SupersetDashboardsFiltersMixin:Module] +# #endregion SupersetDashboardsFiltersMixin diff --git a/backend/src/core/superset_client/_dashboards_list.py b/backend/src/core/superset_client/_dashboards_list.py index 50c4c407..487d7f6c 100644 --- a/backend/src/core/superset_client/_dashboards_list.py +++ b/backend/src/core/superset_client/_dashboards_list.py @@ -1,10 +1,8 @@ -# [DEF:SupersetDashboardsListMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, dashboards, list, pagination, summary -# @PURPOSE: Dashboard listing mixin for SupersetClient — paginated list, summary projection. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetUserProjectionMixin] +# #region SupersetDashboardsListMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboards, list, pagination, summary] +# @BRIEF Dashboard listing mixin for SupersetClient — paginated list, summary projection. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetUserProjectionMixin] import json from typing import Any, Dict, List, Optional, Tuple, cast @@ -15,16 +13,14 @@ from ..logger import belief_scope log = MarkerLogger("SupersetDashboardsList") -# [DEF:SupersetDashboardsListMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing dashboard listing and summary projection operations. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetUserProjectionMixin] +# #region SupersetDashboardsListMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing dashboard listing and summary projection operations. +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetUserProjectionMixin] class SupersetDashboardsListMixin: - # [DEF:SupersetClientGetDashboards:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию. - # @RELATION: CALLS -> [SupersetClientFetchAllPages] + # #region SupersetClientGetDashboards [C:3] [TYPE Function] + # @BRIEF Получает полный список дашбордов, автоматически обрабатывая пагинацию. + # @RELATION CALLS -> [SupersetClientFetchAllPages] def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: with belief_scope("get_dashboards"): log.reason("Fetching dashboards", payload={"has_query": query is not None}) @@ -46,12 +42,11 @@ class SupersetDashboardsListMixin: log.reflect("Dashboards fetched", payload={"total_count": total_count}) return total_count, paginated_data - # [/DEF:SupersetClientGetDashboards:Function] + # #endregion SupersetClientGetDashboards - # [DEF:SupersetClientGetDashboardsPage:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDashboardsPage [C:3] [TYPE Function] + # @BRIEF Fetches a single dashboards page from Superset without iterating all pages. + # @RELATION CALLS -> [APIClient] def get_dashboards_page( self, query: Optional[Dict] = None ) -> Tuple[int, List[Dict]]: @@ -75,12 +70,11 @@ class SupersetDashboardsListMixin: total_count = response_json.get("count", len(result)) return total_count, result - # [/DEF:SupersetClientGetDashboardsPage:Function] + # #endregion SupersetClientGetDashboardsPage - # [DEF:SupersetClientGetDashboardsSummary:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches dashboard metadata optimized for the grid. - # @RELATION: CALLS -> [SupersetClientGetDashboards] + # #region SupersetClientGetDashboardsSummary [C:3] [TYPE Function] + # @BRIEF Fetches dashboard metadata optimized for the grid. + # @RELATION CALLS -> [SupersetClientGetDashboards] def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]: with belief_scope("SupersetClient.get_dashboards_summary"): query: Dict[str, Any] = {} @@ -154,12 +148,11 @@ class SupersetDashboardsListMixin: ) return result - # [/DEF:SupersetClientGetDashboardsSummary:Function] + # #endregion SupersetClientGetDashboardsSummary - # [DEF:SupersetClientGetDashboardsSummaryPage:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches one page of dashboard metadata optimized for the grid. - # @RELATION: CALLS -> [SupersetClientGetDashboardsPage] + # #region SupersetClientGetDashboardsSummaryPage [C:3] [TYPE Function] + # @BRIEF Fetches one page of dashboard metadata optimized for the grid. + # @RELATION CALLS -> [SupersetClientGetDashboardsPage] def get_dashboards_summary_page( self, page: int, @@ -211,8 +204,7 @@ class SupersetDashboardsListMixin: return total_count, result - # [/DEF:SupersetClientGetDashboardsSummaryPage:Function] + # #endregion SupersetClientGetDashboardsSummaryPage -# [/DEF:SupersetDashboardsListMixin:Class] -# [/DEF:SupersetDashboardsListMixin:Module] +# #endregion SupersetDashboardsListMixin diff --git a/backend/src/core/superset_client/_databases.py b/backend/src/core/superset_client/_databases.py index 3687d154..6f0c5b2e 100644 --- a/backend/src/core/superset_client/_databases.py +++ b/backend/src/core/superset_client/_databases.py @@ -1,9 +1,7 @@ -# [DEF:SupersetDatabasesMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, databases, list, get, summary, uuid -# @PURPOSE: Database domain mixin for SupersetClient — list, get, summary, by_uuid. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDatabasesMixin [C:3] [TYPE Module] [SEMANTICS superset, databases, list, get, summary, uuid] +# @BRIEF Database domain mixin for SupersetClient — list, get, summary, by_uuid. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] from typing import Any, Dict, List, Optional, Tuple, cast @@ -13,15 +11,13 @@ from ..logger import belief_scope log = MarkerLogger("SupersetDatabases") -# [DEF:SupersetDatabasesMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing all database-related Superset API operations. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDatabasesMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing all database-related Superset API operations. +# @RELATION DEPENDS_ON -> [SupersetClientBase] class SupersetDatabasesMixin: - # [DEF:SupersetClientGetDatabases:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Получает полный список баз данных. - # @RELATION: CALLS -> [SupersetClientFetchAllPages] + # #region SupersetClientGetDatabases [C:3] [TYPE Function] + # @BRIEF Получает полный список баз данных. + # @RELATION CALLS -> [SupersetClientFetchAllPages] def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: with belief_scope("get_databases"): log.reason("Fetching databases", payload={"has_query": query is not None}) @@ -40,12 +36,11 @@ class SupersetDatabasesMixin: log.reflect("Databases fetched", payload={"total_count": total_count}) return total_count, paginated_data - # [/DEF:SupersetClientGetDatabases:Function] + # #endregion SupersetClientGetDatabases - # [DEF:SupersetClientGetDatabase:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Получает информацию о конкретной базе данных по её ID. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDatabase [C:3] [TYPE Function] + # @BRIEF Получает информацию о конкретной базе данных по её ID. + # @RELATION CALLS -> [APIClient] def get_database(self, database_id: int) -> Dict: with belief_scope("get_database"): log.reason("Fetching database", payload={"database_id": database_id}) @@ -56,12 +51,11 @@ class SupersetDatabasesMixin: log.reflect("Database fetched", payload={"database_id": database_id}) return response - # [/DEF:SupersetClientGetDatabase:Function] + # #endregion SupersetClientGetDatabase - # [DEF:SupersetClientGetDatabasesSummary:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch a summary of databases including uuid, name, and engine. - # @RELATION: CALLS -> [SupersetClientGetDatabases] + # #region SupersetClientGetDatabasesSummary [C:3] [TYPE Function] + # @BRIEF Fetch a summary of databases including uuid, name, and engine. + # @RELATION CALLS -> [SupersetClientGetDatabases] def get_databases_summary(self) -> List[Dict]: with belief_scope("SupersetClient.get_databases_summary"): query = {"columns": ["id", "uuid", "database_name", "backend"]} @@ -73,20 +67,18 @@ class SupersetDatabasesMixin: return databases - # [/DEF:SupersetClientGetDatabasesSummary:Function] + # #endregion SupersetClientGetDatabasesSummary - # [DEF:SupersetClientGetDatabaseByUuid:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Find a database by its UUID. - # @RELATION: CALLS -> [SupersetClientGetDatabases] + # #region SupersetClientGetDatabaseByUuid [C:3] [TYPE Function] + # @BRIEF Find a database by its UUID. + # @RELATION CALLS -> [SupersetClientGetDatabases] def get_database_by_uuid(self, db_uuid: str) -> Optional[Dict]: with belief_scope("SupersetClient.get_database_by_uuid", f"uuid={db_uuid}"): query = {"filters": [{"col": "uuid", "op": "eq", "value": db_uuid}]} _, databases = self.get_databases(query=query) return databases[0] if databases else None - # [/DEF:SupersetClientGetDatabaseByUuid:Function] + # #endregion SupersetClientGetDatabaseByUuid -# [/DEF:SupersetDatabasesMixin:Class] -# [/DEF:SupersetDatabasesMixin:Module] +# #endregion SupersetDatabasesMixin diff --git a/backend/src/core/superset_client/_datasets.py b/backend/src/core/superset_client/_datasets.py index 863d2c37..26d0c87e 100644 --- a/backend/src/core/superset_client/_datasets.py +++ b/backend/src/core/superset_client/_datasets.py @@ -1,9 +1,7 @@ -# [DEF:SupersetDatasetsMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, datasets, list, get, detail, update -# @PURPOSE: Dataset domain mixin for SupersetClient — list, get, detail, update. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDatasetsMixin [C:3] [TYPE Module] [SEMANTICS superset, datasets, list, get, detail, update] +# @BRIEF Dataset domain mixin for SupersetClient — list, get, detail, update. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] import json from typing import Any, Dict, List, Optional, Tuple, cast @@ -14,15 +12,13 @@ from ..logger import belief_scope log = MarkerLogger("SupersetDatasets") -# [DEF:SupersetDatasetsMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing basic dataset CRUD operations (list, get, detail, update). -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetDatasetsMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing basic dataset CRUD operations (list, get, detail, update). +# @RELATION DEPENDS_ON -> [SupersetClientBase] class SupersetDatasetsMixin: - # [DEF:SupersetClientGetDatasets:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию. - # @RELATION: CALLS -> [SupersetClientFetchAllPages] + # #region SupersetClientGetDatasets [C:3] [TYPE Function] + # @BRIEF Получает полный список датасетов, автоматически обрабатывая пагинацию. + # @RELATION CALLS -> [SupersetClientFetchAllPages] def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]: with belief_scope("get_datasets"): log.reason("Fetching datasets") @@ -39,12 +35,11 @@ class SupersetDatasetsMixin: log.reflect("Datasets fetched", payload={"total_count": total_count}) return total_count, paginated_data - # [/DEF:SupersetClientGetDatasets:Function] + # #endregion SupersetClientGetDatasets - # [DEF:SupersetClientGetDatasetsSummary:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches dataset metadata optimized for the Dataset Hub grid. - # @RELATION: CALLS -> [SupersetClientGetDatasets] + # #region SupersetClientGetDatasetsSummary [C:3] [TYPE Function] + # @BRIEF Fetches dataset metadata optimized for the Dataset Hub grid. + # @RELATION CALLS -> [SupersetClientGetDatasets] def get_datasets_summary(self) -> List[Dict]: with belief_scope("SupersetClient.get_datasets_summary"): query = {"columns": ["id", "table_name", "schema", "database"]} @@ -64,13 +59,12 @@ class SupersetDatasetsMixin: ) return result - # [/DEF:SupersetClientGetDatasetsSummary:Function] + # #endregion SupersetClientGetDatasetsSummary - # [DEF:SupersetClientGetDatasetDetail:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetches detailed dataset information including columns and linked dashboards. - # @RELATION: CALLS -> [SupersetClientGetDataset] - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDatasetDetail [C:3] [TYPE Function] + # @BRIEF Fetches detailed dataset information including columns and linked dashboards. + # @RELATION CALLS -> [SupersetClientGetDataset] + # @RELATION CALLS -> [APIClient] def get_dataset_detail(self, dataset_id: int) -> Dict: with belief_scope("SupersetClient.get_dataset_detail", f"id={dataset_id}"): @@ -180,12 +174,11 @@ class SupersetDatasetsMixin: ) return result - # [/DEF:SupersetClientGetDatasetDetail:Function] + # #endregion SupersetClientGetDatasetDetail - # [DEF:SupersetClientGetDataset:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Получает информацию о конкретном датасете по его ID. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientGetDataset [C:3] [TYPE Function] + # @BRIEF Получает информацию о конкретном датасете по его ID. + # @RELATION CALLS -> [APIClient] def get_dataset(self, dataset_id: int) -> Dict: with belief_scope("SupersetClient.get_dataset", f"id={dataset_id}"): log.reason("Fetching dataset", payload={"dataset_id": dataset_id}) @@ -196,13 +189,12 @@ class SupersetDatasetsMixin: log.reflect("Dataset fetched", payload={"dataset_id": dataset_id}) return response - # [/DEF:SupersetClientGetDataset:Function] + # #endregion SupersetClientGetDataset - # [DEF:SupersetClientUpdateDataset:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Обновляет данные датасета по его ID. - # @SIDE_EFFECT: Modifies resource in upstream Superset environment. - # @RELATION: CALLS -> [APIClient] + # #region SupersetClientUpdateDataset [C:3] [TYPE Function] + # @BRIEF Обновляет данные датасета по его ID. + # @SIDE_EFFECT Modifies resource in upstream Superset environment. + # @RELATION CALLS -> [APIClient] def update_dataset(self, dataset_id: int, data: Dict) -> Dict: with belief_scope("SupersetClient.update_dataset", f"id={dataset_id}"): log.reason("Updating dataset", payload={"dataset_id": dataset_id}) @@ -216,8 +208,7 @@ class SupersetDatasetsMixin: log.reflect("Dataset updated", payload={"dataset_id": dataset_id}) return response - # [/DEF:SupersetClientUpdateDataset:Function] + # #endregion SupersetClientUpdateDataset -# [/DEF:SupersetDatasetsMixin:Class] -# [/DEF:SupersetDatasetsMixin:Module] +# #endregion SupersetDatasetsMixin diff --git a/backend/src/core/superset_client/_datasets_preview.py b/backend/src/core/superset_client/_datasets_preview.py index ee193172..14623fb9 100644 --- a/backend/src/core/superset_client/_datasets_preview.py +++ b/backend/src/core/superset_client/_datasets_preview.py @@ -1,10 +1,8 @@ -# [DEF:SupersetDatasetsPreviewMixin:Module] -# @COMPLEXITY: 4 -# @LAYER: Infra -# @SEMANTICS: superset, datasets, preview, sql, compilation, filters -# @PURPOSE: Dataset preview compilation mixin for SupersetClient — build query context, compile SQL. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetDatasetsMixin] +# #region SupersetDatasetsPreviewMixin [C:4] [TYPE Module] [SEMANTICS superset, datasets, preview, sql, compilation, filters] +# @BRIEF Dataset preview compilation mixin for SupersetClient — build query context, compile SQL. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetDatasetsMixin] import json from copy import deepcopy @@ -16,13 +14,11 @@ from ..utils.network import SupersetAPIError log = MarkerLogger("SupersetDatasetsPreview") -# [DEF:SupersetDatasetsPreviewMixin:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Mixin providing dataset preview compilation and query context building. +# #region SupersetDatasetsPreviewMixin [C:4] [TYPE Class] +# @BRIEF Mixin providing dataset preview compilation and query context building. class SupersetDatasetsPreviewMixin: - # [DEF:SupersetClientCompileDatasetPreview:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output. + # #region SupersetClientCompileDatasetPreview [C:4] [TYPE Function] + # @BRIEF Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output. def compile_dataset_preview(self, dataset_id: int, template_params: Optional[Dict[str, Any]] = None, effective_filters: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]: with belief_scope('SupersetClientCompileDatasetPreview'): log.reason('Compiling dataset preview SQL', payload={"dataset_id": dataset_id}) @@ -70,11 +66,10 @@ class SupersetDatasetsPreviewMixin: strategy_attempts.append(failure_diagnostics) log.explore('Dataset preview compilation strategy failed', payload={'dataset_id': dataset_id, **failure_diagnostics, 'request_params': request_params, 'request_payload': request_body}, error=str(exc)) raise SupersetAPIError(f'Superset preview compilation failed for all known strategies (attempts={strategy_attempts!r})') - # [/DEF:SupersetClientCompileDatasetPreview:Function] + # #endregion SupersetClientCompileDatasetPreview - # [DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic. + # #region SupersetClientBuildDatasetPreviewLegacyFormData [C:4] [TYPE Function] + # @BRIEF Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic. def build_dataset_preview_legacy_form_data(self, dataset_id: int, dataset_record: Dict[str, Any], template_params: Dict[str, Any], effective_filters: List[Dict[str, Any]]) -> Dict[str, Any]: with belief_scope('SupersetClientBuildDatasetPreviewLegacyFormData'): log.reason('Belief protocol reasoning checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData') @@ -102,11 +97,10 @@ class SupersetDatasetsPreviewMixin: log.reflect('Built Superset legacy preview form_data payload from browser-observed request shape', payload={'dataset_id': dataset_id, 'legacy_endpoint_inference': 'POST /explore_json/form_data?form_data=... primary, POST /data?form_data=... fallback, based on observed browser traffic', 'contains_form_datasource': 'datasource' in legacy_form_data, 'legacy_form_data_keys': sorted(legacy_form_data.keys()), 'legacy_extra_filters': legacy_form_data.get('extra_filters', []), 'legacy_extra_form_data': legacy_form_data.get('extra_form_data', {})}) log.reflect('Belief protocol postcondition checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData') return legacy_form_data - # [/DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function] + # #endregion SupersetClientBuildDatasetPreviewLegacyFormData - # [DEF:SupersetClientBuildDatasetPreviewQueryContext:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Build a reduced-scope chart-data query context for deterministic dataset preview compilation. + # #region SupersetClientBuildDatasetPreviewQueryContext [C:4] [TYPE Function] + # @BRIEF Build a reduced-scope chart-data query context for deterministic dataset preview compilation. def build_dataset_preview_query_context( self, dataset_id: int, @@ -225,7 +219,6 @@ class SupersetDatasetsPreviewMixin: ) return payload - # [/DEF:SupersetClientBuildDatasetPreviewQueryContext:Function] + # #endregion SupersetClientBuildDatasetPreviewQueryContext -# [/DEF:SupersetDatasetsPreviewMixin:Class] -# [/DEF:SupersetDatasetsPreviewMixin:Module] +# #endregion SupersetDatasetsPreviewMixin diff --git a/backend/src/core/superset_client/_datasets_preview_filters.py b/backend/src/core/superset_client/_datasets_preview_filters.py index 6c24da0c..2b630d98 100644 --- a/backend/src/core/superset_client/_datasets_preview_filters.py +++ b/backend/src/core/superset_client/_datasets_preview_filters.py @@ -1,10 +1,8 @@ -# [DEF:SupersetDatasetsPreviewFiltersMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, datasets, preview, filters, normalization, sql, extraction -# @PURPOSE: Filter normalization and SQL extraction helpers for dataset preview compilation. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetDatasetsPreviewMixin] +# #region SupersetDatasetsPreviewFiltersMixin [C:3] [TYPE Module] [SEMANTICS superset, datasets, preview, filters, normalization, sql, extraction] +# @BRIEF Filter normalization and SQL extraction helpers for dataset preview compilation. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewMixin] from copy import deepcopy from typing import Any, Dict, List, Optional, Tuple @@ -15,15 +13,13 @@ from ..utils.network import SupersetAPIError log = MarkerLogger("SupersetDatasetsPreviewFilters") -# [DEF:SupersetDatasetsPreviewFiltersMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing filter normalization and compiled-SQL extraction for dataset preview operations. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] -# @RELATION: DEPENDS_ON -> [SupersetDatasetsPreviewMixin] +# #region SupersetDatasetsPreviewFiltersMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing filter normalization and compiled-SQL extraction for dataset preview operations. +# @RELATION DEPENDS_ON -> [SupersetClientBase] +# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewMixin] class SupersetDatasetsPreviewFiltersMixin: - # [DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Convert execution mappings into Superset chart-data filter objects. + # #region SupersetClientNormalizeEffectiveFiltersForQueryContext [C:3] [TYPE Function] + # @BRIEF Convert execution mappings into Superset chart-data filter objects. def _normalize_effective_filters_for_query_context( self, effective_filters: List[Dict[str, Any]], @@ -122,11 +118,10 @@ class SupersetDatasetsPreviewFiltersMixin: "diagnostics": diagnostics, } - # [/DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function] + # #endregion SupersetClientNormalizeEffectiveFiltersForQueryContext - # [DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Normalize compiled SQL from either chart-data or legacy form_data preview responses. + # #region SupersetClientExtractCompiledSqlFromPreviewResponse [C:3] [TYPE Function] + # @BRIEF Normalize compiled SQL from either chart-data or legacy form_data preview responses. def _extract_compiled_sql_from_preview_response( self, response: Any ) -> Dict[str, Any]: @@ -196,8 +191,7 @@ class SupersetDatasetsPreviewFiltersMixin: f"(diagnostics={response_diagnostics!r})" ) - # [/DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function] + # #endregion SupersetClientExtractCompiledSqlFromPreviewResponse -# [/DEF:SupersetDatasetsPreviewFiltersMixin:Class] -# [/DEF:SupersetDatasetsPreviewFiltersMixin:Module] +# #endregion SupersetDatasetsPreviewFiltersMixin diff --git a/backend/src/core/superset_client/_user_projection.py b/backend/src/core/superset_client/_user_projection.py index 19b4d02d..540a6e00 100644 --- a/backend/src/core/superset_client/_user_projection.py +++ b/backend/src/core/superset_client/_user_projection.py @@ -1,21 +1,17 @@ -# [DEF:SupersetUserProjection:Module] -# @COMPLEXITY: 2 -# @LAYER: Infra -# @SEMANTICS: superset, users, owners, projection, normalization -# @PURPOSE: User/owner payload normalization helpers for Superset client responses. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetUserProjection [C:2] [TYPE Module] [SEMANTICS superset, users, owners, projection, normalization] +# @BRIEF User/owner payload normalization helpers for Superset client responses. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetClientBase] from typing import Any, Dict, List, Optional, Union -# [DEF:SupersetUserProjectionMixin:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Mixin providing user/owner payload normalization for Superset API responses. -# @RELATION: DEPENDS_ON -> [SupersetClientBase] +# #region SupersetUserProjectionMixin [C:2] [TYPE Class] +# @BRIEF Mixin providing user/owner payload normalization for Superset API responses. +# @RELATION DEPENDS_ON -> [SupersetClientBase] class SupersetUserProjectionMixin: - # [DEF:SupersetClientExtractOwnerLabels:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Normalize dashboard owners payload to stable display labels. + # #region SupersetClientExtractOwnerLabels [C:1] [TYPE Function] + # @BRIEF Normalize dashboard owners payload to stable display labels. def _extract_owner_labels(self, owners_payload: Any) -> List[str]: if owners_payload is None: return [] @@ -37,11 +33,10 @@ class SupersetUserProjectionMixin: normalized.append(label) return normalized - # [/DEF:SupersetClientExtractOwnerLabels:Function] + # #endregion SupersetClientExtractOwnerLabels - # [DEF:SupersetClientExtractUserDisplay:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Normalize user payload to a stable display name. + # #region SupersetClientExtractUserDisplay [C:1] [TYPE Function] + # @BRIEF Normalize user payload to a stable display name. def _extract_user_display( self, preferred_value: Optional[str], user_payload: Optional[Dict] ) -> Optional[str]: @@ -68,11 +63,10 @@ class SupersetUserProjectionMixin: return email return None - # [/DEF:SupersetClientExtractUserDisplay:Function] + # #endregion SupersetClientExtractUserDisplay - # [DEF:SupersetClientSanitizeUserText:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Convert scalar value to non-empty user-facing text. + # #region SupersetClientSanitizeUserText [C:1] [TYPE Function] + # @BRIEF Convert scalar value to non-empty user-facing text. def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]: if value is None: return None @@ -81,8 +75,7 @@ class SupersetUserProjectionMixin: return None return normalized - # [/DEF:SupersetClientSanitizeUserText:Function] + # #endregion SupersetClientSanitizeUserText -# [/DEF:SupersetUserProjectionMixin:Class] -# [/DEF:SupersetUserProjection:Module] +# #endregion SupersetUserProjectionMixin diff --git a/backend/src/core/superset_profile_lookup.py b/backend/src/core/superset_profile_lookup.py index db613562..a66e5631 100644 --- a/backend/src/core/superset_profile_lookup.py +++ b/backend/src/core/superset_profile_lookup.py @@ -1,14 +1,12 @@ -# [DEF:SupersetProfileLookup:Module] +# #region SupersetProfileLookup [C:3] [TYPE Module] [SEMANTICS superset, users, lookup, profile, pagination, normalization] +# @BRIEF Provides environment-scoped Superset account lookup adapter with stable normalized output. +# @LAYER Core +# @INVARIANT Adapter never leaks raw upstream payload shape to API consumers. +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetAPIError] +# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter] # -# @COMPLEXITY: 3 -# @SEMANTICS: superset, users, lookup, profile, pagination, normalization -# @PURPOSE: Provides environment-scoped Superset account lookup adapter with stable normalized output. -# @LAYER: Core -# @RELATION: [DEPENDS_ON] ->[APIClient] -# @RELATION: [DEPENDS_ON] ->[SupersetAPIError] -# @RELATION: [DEPENDS_ON] ->[SupersetAccountLookupAdapter] # -# @INVARIANT: Adapter never leaks raw upstream payload shape to API consumers. # [SECTION: IMPORTS] import json @@ -19,26 +17,25 @@ from .utils.network import APIClient, AuthenticationError, SupersetAPIError # [/SECTION] -# [DEF:SupersetAccountLookupAdapter:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Lookup Superset users and normalize candidates for profile binding. -# @RELATION: [DEPENDS_ON] ->[APIClient] -# @RELATION: [DEPENDS_ON] ->[SupersetProfileLookup] +# #region SupersetAccountLookupAdapter [C:3] [TYPE Class] +# @BRIEF Lookup Superset users and normalize candidates for profile binding. +# @RELATION DEPENDS_ON -> [APIClient] +# @RELATION DEPENDS_ON -> [SupersetProfileLookup] class SupersetAccountLookupAdapter: - # [DEF:__init__:Function] - # @PURPOSE: Initializes lookup adapter with authenticated API client and environment context. - # @PRE: network_client supports request(method, endpoint, params=...). - # @POST: Adapter is ready to perform users lookup requests. + # #region __init__ [TYPE Function] + # @BRIEF Initializes lookup adapter with authenticated API client and environment context. + # @PRE network_client supports request(method, endpoint, params=...). + # @POST Adapter is ready to perform users lookup requests. def __init__(self, network_client: APIClient, environment_id: str): self.network_client = network_client self.environment_id = str(environment_id or "") - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:get_users_page:Function] - # @PURPOSE: Fetch one users page from Superset with passthrough search/sort parameters. - # @PRE: page_index >= 0 and page_size >= 1. - # @POST: Returns deterministic payload with normalized items and total count. - # @RETURN: Dict[str, Any] + # #region get_users_page [TYPE Function] + # @BRIEF Fetch one users page from Superset with passthrough search/sort parameters. + # @PRE page_index >= 0 and page_size >= 1. + # @POST Returns deterministic payload with normalized items and total count. + # @RETURN Dict[str, Any] def get_users_page( self, search: Optional[str] = None, @@ -136,13 +133,13 @@ class SupersetAccountLookupAdapter: ) raise selected_error raise SupersetAPIError("Superset users lookup failed without explicit error") - # [/DEF:get_users_page:Function] + # #endregion get_users_page - # [DEF:_normalize_lookup_payload:Function] - # @PURPOSE: Convert Superset users response variants into stable candidates payload. - # @PRE: response can be dict/list in any supported upstream shape. - # @POST: Output contains canonical keys: status, environment_id, page_index, page_size, total, items. - # @RETURN: Dict[str, Any] + # #region _normalize_lookup_payload [TYPE Function] + # @BRIEF Convert Superset users response variants into stable candidates payload. + # @PRE response can be dict/list in any supported upstream shape. + # @POST Output contains canonical keys: status, environment_id, page_index, page_size, total, items. + # @RETURN Dict[str, Any] def _normalize_lookup_payload( self, response: Any, @@ -197,13 +194,13 @@ class SupersetAccountLookupAdapter: "total": max(int(total), len(normalized_items)), "items": normalized_items, } - # [/DEF:_normalize_lookup_payload:Function] + # #endregion _normalize_lookup_payload - # [DEF:normalize_user_payload:Function] - # @PURPOSE: Project raw Superset user object to canonical candidate shape. - # @PRE: raw_user may have heterogenous key names between Superset versions. - # @POST: Returns normalized candidate keys (environment_id, username, display_name, email, is_active). - # @RETURN: Dict[str, Any] + # #region normalize_user_payload [TYPE Function] + # @BRIEF Project raw Superset user object to canonical candidate shape. + # @PRE raw_user may have heterogenous key names between Superset versions. + # @POST Returns normalized candidate keys (environment_id, username, display_name, email, is_active). + # @RETURN Dict[str, Any] def normalize_user_payload(self, raw_user: Any) -> Dict[str, Any]: if not isinstance(raw_user, dict): raw_user = {} @@ -235,7 +232,7 @@ class SupersetAccountLookupAdapter: "email": email, "is_active": is_active, } - # [/DEF:normalize_user_payload:Function] -# [/DEF:SupersetAccountLookupAdapter:Class] + # #endregion normalize_user_payload +# #endregion SupersetAccountLookupAdapter -# [/DEF:SupersetProfileLookup:Module] \ No newline at end of file +# #endregion SupersetProfileLookup diff --git a/backend/src/core/task_manager/__init__.py b/backend/src/core/task_manager/__init__.py index 33384e85..25af29fe 100644 --- a/backend/src/core/task_manager/__init__.py +++ b/backend/src/core/task_manager/__init__.py @@ -1,8 +1,8 @@ -# [DEF:TaskManagerPackage:Module] +# #region TaskManagerPackage [TYPE Module] from .models import Task, TaskStatus, LogEntry from .manager import TaskManager __all__ = ["TaskManager", "Task", "TaskStatus", "LogEntry"] -# [/DEF:TaskManagerPackage:Module] +# #endregion TaskManagerPackage diff --git a/backend/src/core/task_manager/__tests__/test_context.py b/backend/src/core/task_manager/__tests__/test_context.py index f2a5c816..41848231 100644 --- a/backend/src/core/task_manager/__tests__/test_context.py +++ b/backend/src/core/task_manager/__tests__/test_context.py @@ -1,19 +1,17 @@ -# [DEF:TestContext:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, task-context, background-tasks, sub-context -# @PURPOSE: Verify TaskContext preserves optional background task scheduler across sub-context creation. +# #region TestContext [C:3] [TYPE Module] [SEMANTICS tests, task-context, background-tasks, sub-context] +# @BRIEF Verify TaskContext preserves optional background task scheduler across sub-context creation. +# @RELATION BELONGS_TO -> [SrcRoot] from unittest.mock import MagicMock from src.core.task_manager.context import TaskContext -# [DEF:test_task_context_preserves_background_tasks_across_sub_context:Function] -# @RELATION: BINDS_TO -> TestContext -# @PURPOSE: Plugins must be able to access background_tasks from both root and sub-context loggers. -# @PRE: TaskContext is initialized with a BackgroundTasks-like object. -# @POST: background_tasks remains available on root and derived sub-contexts. +# #region test_task_context_preserves_background_tasks_across_sub_context [TYPE Function] +# @BRIEF Plugins must be able to access background_tasks from both root and sub-context loggers. +# @PRE TaskContext is initialized with a BackgroundTasks-like object. +# @POST background_tasks remains available on root and derived sub-contexts. +# @RELATION BINDS_TO -> [TestContext] def test_task_context_preserves_background_tasks_across_sub_context(): background_tasks = MagicMock() context = TaskContext( @@ -27,5 +25,5 @@ def test_task_context_preserves_background_tasks_across_sub_context(): assert context.background_tasks is background_tasks assert sub_context.background_tasks is background_tasks -# [/DEF:test_task_context_preserves_background_tasks_across_sub_context:Function] -# [/DEF:TestContext:Module] +# #endregion test_task_context_preserves_background_tasks_across_sub_context +# #endregion TestContext diff --git a/backend/src/core/task_manager/__tests__/test_task_logger.py b/backend/src/core/task_manager/__tests__/test_task_logger.py index 63528b45..aa22fb71 100644 --- a/backend/src/core/task_manager/__tests__/test_task_logger.py +++ b/backend/src/core/task_manager/__tests__/test_task_logger.py @@ -1,7 +1,7 @@ -# [DEF:__tests__/test_task_logger:Module] -# @RELATION: VERIFIES -> ../task_logger.py -# @PURPOSE: Contract testing for TaskLogger -# [/DEF:__tests__/test_task_logger:Module] +# #region __tests__/test_task_logger [TYPE Module] +# @BRIEF Contract testing for TaskLogger +# @RELATION VERIFIES -> [../task_logger.py] +# #endregion __tests__/test_task_logger import pytest from unittest.mock import MagicMock @@ -17,20 +17,20 @@ def task_logger(mock_add_log): return TaskLogger(task_id="test_123", add_log_fn=mock_add_log, source="test_plugin") # @TEST_CONTRACT: TaskLoggerModel -> Invariants -# [DEF:test_task_logger_initialization:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger initializes with correct task_id and state. +# #region test_task_logger_initialization [TYPE Function] +# @BRIEF Verify TaskLogger initializes with correct task_id and state. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_task_logger_initialization(task_logger): """Verify TaskLogger is bound to specific task_id and source.""" assert task_logger._task_id == "test_123" assert task_logger._default_source == "test_plugin" # @TEST_CONTRACT: invariants -> "All specific log methods (info, error) delegate to _log" -# [/DEF:test_task_logger_initialization:Function] +# #endregion test_task_logger_initialization -# [DEF:test_log_methods_delegation:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger delegates log method calls to the underlying persistence service. +# #region test_log_methods_delegation [TYPE Function] +# @BRIEF Verify TaskLogger delegates log method calls to the underlying persistence service. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_log_methods_delegation(task_logger, mock_add_log): """Verify info, error, warning, debug delegate to internal _log.""" task_logger.info("info message", metadata={"k": "v"}) @@ -70,11 +70,11 @@ def test_log_methods_delegation(task_logger, mock_add_log): ) # @TEST_CONTRACT: invariants -> "with_source creates a new logger with the same task_id" -# [/DEF:test_log_methods_delegation:Function] +# #endregion test_log_methods_delegation -# [DEF:test_with_source:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger.with_source returns a new logger with the correct source attribution. +# #region test_with_source [TYPE Function] +# @BRIEF Verify TaskLogger.with_source returns a new logger with the correct source attribution. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_with_source(task_logger): """Verify with_source returns a new instance with updated default source.""" new_logger = task_logger.with_source("new_source") @@ -84,33 +84,33 @@ def test_with_source(task_logger): assert new_logger is not task_logger # @TEST_EDGE: missing_task_id -> raises TypeError -# [/DEF:test_with_source:Function] +# #endregion test_with_source -# [DEF:test_missing_task_id:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger raises or handles missing task_id gracefully. +# #region test_missing_task_id [TYPE Function] +# @BRIEF Verify TaskLogger raises or handles missing task_id gracefully. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_missing_task_id(): with pytest.raises(TypeError): TaskLogger(add_log_fn=lambda x: x) # @TEST_EDGE: invalid_add_log_fn -> raises TypeError # (Python doesn't strictly enforce this at init, but let's verify it fails on call if not callable) -# [/DEF:test_missing_task_id:Function] +# #endregion test_missing_task_id -# [DEF:test_invalid_add_log_fn:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger raises ValueError for invalid add_log_fn parameter. +# #region test_invalid_add_log_fn [TYPE Function] +# @BRIEF Verify TaskLogger raises ValueError for invalid add_log_fn parameter. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_invalid_add_log_fn(): logger = TaskLogger(task_id="msg", add_log_fn=None) with pytest.raises(TypeError): logger.info("test") # @TEST_INVARIANT: consistent_delegation -# [/DEF:test_invalid_add_log_fn:Function] +# #endregion test_invalid_add_log_fn -# [DEF:test_progress_log:Function] -# @RELATION: BINDS_TO -> __tests__/test_task_logger -# @PURPOSE: Verify TaskLogger correctly logs progress updates with percentage and message. +# #region test_progress_log [TYPE Function] +# @BRIEF Verify TaskLogger correctly logs progress updates with percentage and message. +# @RELATION BINDS_TO -> [__tests__/test_task_logger] def test_progress_log(task_logger, mock_add_log): """Verify progress method correctly formats metadata.""" task_logger.progress("Step 1", 45.5) @@ -128,4 +128,4 @@ def test_progress_log(task_logger, mock_add_log): task_logger.progress("Step low", -10) assert mock_add_log.call_args[1]["metadata"]["progress"] == 0 -# [/DEF:test_progress_log:Function] +# #endregion test_progress_log diff --git a/backend/src/core/task_manager/cleanup.py b/backend/src/core/task_manager/cleanup.py index 8006797b..57308829 100644 --- a/backend/src/core/task_manager/cleanup.py +++ b/backend/src/core/task_manager/cleanup.py @@ -1,28 +1,25 @@ -# [DEF:TaskCleanupModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: task, cleanup, retention, logs -# @PURPOSE: Implements task cleanup and retention policies, including associated logs. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [TaskPersistenceService] -# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService] -# @RELATION: DEPENDS_ON -> [ConfigManager] +# #region TaskCleanupModule [C:3] [TYPE Module] [SEMANTICS task, cleanup, retention, logs] +# @BRIEF Implements task cleanup and retention policies, including associated logs. +# @LAYER Core +# @RELATION DEPENDS_ON -> [TaskPersistenceService] +# @RELATION DEPENDS_ON -> [TaskLogPersistenceService] +# @RELATION DEPENDS_ON -> [ConfigManager] from typing import List from .persistence import TaskPersistenceService, TaskLogPersistenceService from ..logger import logger, belief_scope from ..config_manager import ConfigManager -# [DEF:TaskCleanupService:Class] -# @PURPOSE: Provides methods to clean up old task records and their associated logs. -# @COMPLEXITY: 3 -# @RELATION: DEPENDS_ON -> [TaskPersistenceService] -# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService] -# @RELATION: DEPENDS_ON -> [ConfigManager] +# #region TaskCleanupService [C:3] [TYPE Class] +# @BRIEF Provides methods to clean up old task records and their associated logs. +# @RELATION DEPENDS_ON -> [TaskPersistenceService] +# @RELATION DEPENDS_ON -> [TaskLogPersistenceService] +# @RELATION DEPENDS_ON -> [ConfigManager] class TaskCleanupService: - # [DEF:__init__:Function] - # @PURPOSE: Initializes the cleanup service with dependencies. - # @PRE: persistence_service and config_manager are valid. - # @POST: Cleanup service is ready. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the cleanup service with dependencies. + # @PRE persistence_service and config_manager are valid. + # @POST Cleanup service is ready. def __init__( self, persistence_service: TaskPersistenceService, @@ -32,12 +29,12 @@ class TaskCleanupService: self.persistence_service = persistence_service self.log_persistence_service = log_persistence_service self.config_manager = config_manager - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:run_cleanup:Function] - # @PURPOSE: Deletes tasks older than the configured retention period and their logs. - # @PRE: Config manager has valid settings. - # @POST: Old tasks and their logs are deleted from persistence. + # #region run_cleanup [TYPE Function] + # @BRIEF Deletes tasks older than the configured retention period and their logs. + # @PRE Config manager has valid settings. + # @POST Old tasks and their logs are deleted from persistence. def run_cleanup(self): with belief_scope("TaskCleanupService.run_cleanup"): settings = self.config_manager.get_config().settings @@ -57,12 +54,12 @@ class TaskCleanupService: self.persistence_service.delete_tasks(to_delete) logger.info(f"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}") - # [/DEF:run_cleanup:Function] + # #endregion run_cleanup - # [DEF:delete_task_with_logs:Function] - # @PURPOSE: Delete a single task and all its associated logs. - # @PRE: task_id is a valid task ID. - # @POST: Task and all its logs are deleted. + # #region delete_task_with_logs [TYPE Function] + # @BRIEF Delete a single task and all its associated logs. + # @PRE task_id is a valid task ID. + # @POST Task and all its logs are deleted. # @PARAM: task_id (str) - The task ID to delete. def delete_task_with_logs(self, task_id: str) -> None: """Delete a single task and all its associated logs.""" @@ -74,7 +71,7 @@ class TaskCleanupService: self.persistence_service.delete_tasks([task_id]) logger.info(f"Deleted task {task_id} and its associated logs") - # [/DEF:delete_task_with_logs:Function] + # #endregion delete_task_with_logs -# [/DEF:TaskCleanupService:Class] -# [/DEF:TaskCleanupModule:Module] \ No newline at end of file +# #endregion TaskCleanupService +# #endregion TaskCleanupModule diff --git a/backend/src/core/task_manager/context.py b/backend/src/core/task_manager/context.py index 0c09be1c..5ef8f101 100644 --- a/backend/src/core/task_manager/context.py +++ b/backend/src/core/task_manager/context.py @@ -1,15 +1,13 @@ -# [DEF:TaskContextModule:Module] -# @SEMANTICS: task, context, plugin, execution, logger -# @PURPOSE: Provides execution context passed to plugins during task execution. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> [TaskLoggerModule] -# @RELATION: DEPENDS_ON -> [TaskManager] -# @COMPLEXITY: 5 -# @INVARIANT: Each TaskContext is bound to a single task execution. -# @PRE: Task execution pipeline provides valid task identifiers, logging callbacks, and parameter dictionaries. -# @POST: Plugins receive context instances with stable logger and parameter accessors. -# @SIDE_EFFECT: Creates task-scoped logger wrappers and carries optional background task handles across sub-contexts. -# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext] +# #region TaskContextModule [C:5] [TYPE Module] [SEMANTICS task, context, plugin, execution, logger] +# @BRIEF Provides execution context passed to plugins during task execution. +# @LAYER Core +# @PRE Task execution pipeline provides valid task identifiers, logging callbacks, and parameter dictionaries. +# @POST Plugins receive context instances with stable logger and parameter accessors. +# @SIDE_EFFECT Creates task-scoped logger wrappers and carries optional background task handles across sub-contexts. +# @DATA_CONTRACT Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext] +# @INVARIANT Each TaskContext is bound to a single task execution. +# @RELATION DEPENDS_ON -> [TaskLoggerModule] +# @RELATION DEPENDS_ON -> [TaskManager] # [SECTION: IMPORTS] # [SECTION: IMPORTS] @@ -19,16 +17,14 @@ from ..logger import belief_scope # [/SECTION] -# [DEF:TaskContext:Class] -# @SEMANTICS: context, task, execution, plugin -# @PURPOSE: A container passed to plugin.execute() providing the logger and other task-specific utilities. -# @COMPLEXITY: 5 -# @INVARIANT: logger is always a valid TaskLogger instance. -# @PRE: Constructor receives non-empty task_id, callable add_log_fn, and params mapping. -# @POST: Instance exposes immutable task identity with logger, params, and optional background task access. -# @RELATION: DEPENDS_ON -> [TaskLogger] -# @SIDE_EFFECT: Emits structured task logs through TaskLogger on plugin interactions. -# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext] +# #region TaskContext [C:5] [TYPE Class] [SEMANTICS context, task, execution, plugin] +# @BRIEF A container passed to plugin.execute() providing the logger and other task-specific utilities. +# @PRE Constructor receives non-empty task_id, callable add_log_fn, and params mapping. +# @POST Instance exposes immutable task identity with logger, params, and optional background task access. +# @SIDE_EFFECT Emits structured task logs through TaskLogger on plugin interactions. +# @DATA_CONTRACT Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext] +# @INVARIANT logger is always a valid TaskLogger instance. +# @RELATION DEPENDS_ON -> [TaskLogger] # @UX_STATE: Idle -> Active -> Complete # # @TEST_CONTRACT: TaskContextContract -> @@ -56,10 +52,10 @@ class TaskContext: # ... plugin logic """ - # [DEF:__init__:Function] - # @PURPOSE: Initialize the TaskContext with task-specific resources. - # @PRE: task_id is a valid task identifier, add_log_fn is callable. - # @POST: TaskContext is ready to be passed to plugin.execute(). + # #region __init__ [TYPE Function] + # @BRIEF Initialize the TaskContext with task-specific resources. + # @PRE task_id is a valid task identifier, add_log_fn is callable. + # @POST TaskContext is ready to be passed to plugin.execute(). # @PARAM: task_id (str) - The ID of the task. # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager. # @PARAM: params (Dict) - Task parameters. @@ -80,59 +76,59 @@ class TaskContext: task_id=task_id, add_log_fn=add_log_fn, source=default_source ) - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:task_id:Function] - # @PURPOSE: Get the task ID. - # @PRE: TaskContext must be initialized. - # @POST: Returns the task ID string. - # @RETURN: str - The task ID. + # #region task_id [TYPE Function] + # @BRIEF Get the task ID. + # @PRE TaskContext must be initialized. + # @POST Returns the task ID string. + # @RETURN str - The task ID. @property def task_id(self) -> str: with belief_scope("task_id"): return self._task_id - # [/DEF:task_id:Function] + # #endregion task_id - # [DEF:logger:Function] - # @PURPOSE: Get the TaskLogger instance for this context. - # @PRE: TaskContext must be initialized. - # @POST: Returns the TaskLogger instance. - # @RETURN: TaskLogger - The logger instance. + # #region logger [TYPE Function] + # @BRIEF Get the TaskLogger instance for this context. + # @PRE TaskContext must be initialized. + # @POST Returns the TaskLogger instance. + # @RETURN TaskLogger - The logger instance. @property def logger(self) -> TaskLogger: with belief_scope("logger"): return self._logger - # [/DEF:logger:Function] + # #endregion logger - # [DEF:params:Function] - # @PURPOSE: Get the task parameters. - # @PRE: TaskContext must be initialized. - # @POST: Returns the parameters dictionary. - # @RETURN: Dict[str, Any] - The task parameters. + # #region params [TYPE Function] + # @BRIEF Get the task parameters. + # @PRE TaskContext must be initialized. + # @POST Returns the parameters dictionary. + # @RETURN Dict[str, Any] - The task parameters. @property def params(self) -> Dict[str, Any]: with belief_scope("params"): return self._params - # [/DEF:params:Function] + # #endregion params - # [DEF:background_tasks:Function] - # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects. - # @PRE: TaskContext must be initialized. - # @POST: Returns BackgroundTasks-like object or None. + # #region background_tasks [TYPE Function] + # @BRIEF Expose optional background task scheduler for plugins that dispatch deferred side effects. + # @PRE TaskContext must be initialized. + # @POST Returns BackgroundTasks-like object or None. @property def background_tasks(self) -> Optional[Any]: with belief_scope("background_tasks"): return self._background_tasks - # [/DEF:background_tasks:Function] + # #endregion background_tasks - # [DEF:get_param:Function] - # @PURPOSE: Get a specific parameter value with optional default. - # @PRE: TaskContext must be initialized. - # @POST: Returns parameter value or default. + # #region get_param [TYPE Function] + # @BRIEF Get a specific parameter value with optional default. + # @PRE TaskContext must be initialized. + # @POST Returns parameter value or default. # @PARAM: key (str) - Parameter key. # @PARAM: default (Any) - Default value if key not found. # @RETURN: Any - Parameter value or default. @@ -140,12 +136,12 @@ class TaskContext: with belief_scope("get_param"): return self._params.get(key, default) - # [/DEF:get_param:Function] + # #endregion get_param - # [DEF:create_sub_context:Function] - # @PURPOSE: Create a sub-context with a different default source. - # @PRE: source is a non-empty string. - # @POST: Returns new TaskContext with different logger source. + # #region create_sub_context [TYPE Function] + # @BRIEF Create a sub-context with a different default source. + # @PRE source is a non-empty string. + # @POST Returns new TaskContext with different logger source. # @PARAM: source (str) - New default source for logging. # @RETURN: TaskContext - New context with different source. def create_sub_context(self, source: str) -> "TaskContext": @@ -159,9 +155,9 @@ class TaskContext: background_tasks=self._background_tasks, ) - # [/DEF:create_sub_context:Function] + # #endregion create_sub_context -# [/DEF:TaskContext:Class] +# #endregion TaskContext -# [/DEF:TaskContextModule:Module] +# #endregion TaskContextModule diff --git a/backend/src/core/task_manager/manager.py b/backend/src/core/task_manager/manager.py index ea89dee9..c46225f0 100644 --- a/backend/src/core/task_manager/manager.py +++ b/backend/src/core/task_manager/manager.py @@ -1,20 +1,18 @@ -# [DEF:TaskManagerModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: task, manager, lifecycle, execution, state -# @PURPOSE: Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously. -# @LAYER: Core -# @PRE: Plugin loader and database sessions are initialized. -# @POST: Orchestrates task execution and persistence. -# @SIDE_EFFECT: Spawns worker threads and flushes logs to DB. -# @DATA_CONTRACT: Input[plugin_id, params] -> Model[Task, LogEntry] -# @RELATION: [DEPENDS_ON] ->[PluginLoader] -# @RELATION: [DEPENDS_ON] ->[TaskPersistenceService] -# @RELATION: [DEPENDS_ON] ->[TaskLogPersistenceService] -# @RELATION: [DEPENDS_ON] ->[TaskContext] -# @RELATION: [DEPENDS_ON] ->[TaskGraph] -# @RELATION: [DEPENDS_ON] ->[JobLifecycle] -# @RELATION: [DEPENDS_ON] ->[EventBus] -# @INVARIANT: Task IDs are unique. +# #region TaskManagerModule [C:5] [TYPE Module] [SEMANTICS task, manager, lifecycle, execution, state] +# @BRIEF Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously. +# @LAYER Core +# @PRE Plugin loader and database sessions are initialized. +# @POST Orchestrates task execution and persistence. +# @SIDE_EFFECT Spawns worker threads and flushes logs to DB. +# @DATA_CONTRACT Input[plugin_id, params] -> Model[Task, LogEntry] +# @INVARIANT Task IDs are unique. +# @RELATION DEPENDS_ON -> [PluginLoader] +# @RELATION DEPENDS_ON -> [TaskPersistenceService] +# @RELATION DEPENDS_ON -> [TaskLogPersistenceService] +# @RELATION DEPENDS_ON -> [TaskContext] +# @RELATION DEPENDS_ON -> [TaskGraph] +# @RELATION DEPENDS_ON -> [JobLifecycle] +# @RELATION DEPENDS_ON -> [EventBus] # @TEST_CONTRACT: TaskManagerRuntime -> { # required_fields: {plugin_loader: PluginLoader}, # optional_fields: {}, @@ -45,25 +43,23 @@ log = MarkerLogger("TaskManager") # [/SECTION] -# [DEF:TaskManager:Class] -# @COMPLEXITY: 5 -# @SEMANTICS: task, manager, lifecycle, execution, state -# @PURPOSE: Manages the lifecycle of tasks, including their creation, execution, and state tracking. -# @LAYER: Core -# @RELATION: [DEPENDS_ON] ->[TaskPersistenceService] -# @RELATION: [DEPENDS_ON] ->[TaskLogPersistenceService] -# @RELATION: [DEPENDS_ON] ->[PluginLoader] -# @RELATION: [DEPENDS_ON] ->[TaskContext] -# @RELATION: [DEPENDS_ON] ->[TaskGraph] -# @RELATION: [DEPENDS_ON] ->[JobLifecycle] -# @RELATION: [DEPENDS_ON] ->[EventBus] -# @PRE: Plugin loader resolves plugin ids and persistence services are available for task state hydration. -# @POST: In-memory task graph, lifecycle scheduler, and log event bus stay consistent with persisted task state. -# @INVARIANT: Task IDs are unique within the registry. -# @INVARIANT: Each task has exactly one status at any time. -# @INVARIANT: Log entries are never deleted after being added to a task. -# @SIDE_EFFECT: Spawns worker threads, flushes logs to database, and mutates task states. -# @DATA_CONTRACT: Input[plugin_id, params] -> Output[Task] +# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state] +# @BRIEF Manages the lifecycle of tasks, including their creation, execution, and state tracking. +# @LAYER Core +# @PRE Plugin loader resolves plugin ids and persistence services are available for task state hydration. +# @POST In-memory task graph, lifecycle scheduler, and log event bus stay consistent with persisted task state. +# @SIDE_EFFECT Spawns worker threads, flushes logs to database, and mutates task states. +# @DATA_CONTRACT Input[plugin_id, params] -> Output[Task] +# @INVARIANT Task IDs are unique within the registry. +# @INVARIANT Each task has exactly one status at any time. +# @INVARIANT Log entries are never deleted after being added to a task. +# @RELATION DEPENDS_ON -> [TaskPersistenceService] +# @RELATION DEPENDS_ON -> [TaskLogPersistenceService] +# @RELATION DEPENDS_ON -> [PluginLoader] +# @RELATION DEPENDS_ON -> [TaskContext] +# @RELATION DEPENDS_ON -> [TaskGraph] +# @RELATION DEPENDS_ON -> [JobLifecycle] +# @RELATION DEPENDS_ON -> [EventBus] class TaskManager: """ Manages the lifecycle of tasks, including their creation, execution, and state tracking. @@ -72,53 +68,49 @@ class TaskManager: # Log flush interval in seconds LOG_FLUSH_INTERVAL = 2.0 - # [DEF:TaskGraph:Block] - # @COMPLEXITY: 5 - # @PURPOSE: Represents the in-memory task dependency graph spanning task registry nodes, paused futures, and persistence-backed hydration. - # @RELATION: [DEPENDS_ON] ->[Task] - # @RELATION: [DEPENDS_ON] ->[TaskPersistenceService] - # @PRE: Task ids are generated before insertion and persisted tasks can be reconstructed into Task models. - # @POST: Each live task id resolves to at most one active Task node and optional pause future. - # @SIDE_EFFECT: Mutates the in-memory task registry and loads persisted state during manager startup. - # @DATA_CONTRACT: Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]] - # @INVARIANT: Registry membership is keyed by unique task id and survives log streaming side channels. - # [/DEF:TaskGraph:Block] + # #region TaskGraph [C:5] [TYPE Block] + # @BRIEF Represents the in-memory task dependency graph spanning task registry nodes, paused futures, and persistence-backed hydration. + # @PRE Task ids are generated before insertion and persisted tasks can be reconstructed into Task models. + # @POST Each live task id resolves to at most one active Task node and optional pause future. + # @SIDE_EFFECT Mutates the in-memory task registry and loads persisted state during manager startup. + # @DATA_CONTRACT Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]] + # @INVARIANT Registry membership is keyed by unique task id and survives log streaming side channels. + # @RELATION DEPENDS_ON -> [Task] + # @RELATION DEPENDS_ON -> [TaskPersistenceService] + # #endregion TaskGraph - # [DEF:EventBus:Block] - # @COMPLEXITY: 5 - # @PURPOSE: Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time observers. - # @RELATION: [DEPENDS_ON] ->[LogEntry] - # @RELATION: [DEPENDS_ON] ->[TaskLogPersistenceService] - # @PRE: Task log records accept structured LogEntry payloads and subscribers consume asyncio queues. - # @POST: Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order. - # @SIDE_EFFECT: Writes task logs to persistence, mutates in-memory buffers, and pushes log entries into subscriber queues. - # @DATA_CONTRACT: Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events] - # @INVARIANT: Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events. - # [/DEF:EventBus:Block] + # #region EventBus [C:5] [TYPE Block] + # @BRIEF Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time observers. + # @PRE Task log records accept structured LogEntry payloads and subscribers consume asyncio queues. + # @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order. + # @SIDE_EFFECT Writes task logs to persistence, mutates in-memory buffers, and pushes log entries into subscriber queues. + # @DATA_CONTRACT Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events] + # @INVARIANT Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events. + # @RELATION DEPENDS_ON -> [LogEntry] + # @RELATION DEPENDS_ON -> [TaskLogPersistenceService] + # #endregion EventBus - # [DEF:JobLifecycle:Block] - # @COMPLEXITY: 5 - # @PURPOSE: Encodes task creation, execution, pause/resume, and completion transitions for plugin-backed jobs. - # @RELATION: [DEPENDS_ON] ->[TaskGraph] - # @RELATION: [DEPENDS_ON] ->[EventBus] - # @RELATION: [DEPENDS_ON] ->[TaskContext] - # @RELATION: [DEPENDS_ON] ->[PluginLoader] - # @PRE: Requested plugin ids resolve to executable plugins and task state transitions target known TaskStatus values. - # @POST: Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state. - # @SIDE_EFFECT: Schedules async execution, pauses on input/mapping requests, and mutates persisted task lifecycle markers. - # @DATA_CONTRACT: Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result] - # @INVARIANT: A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created. - # [/DEF:JobLifecycle:Block] + # #region JobLifecycle [C:5] [TYPE Block] + # @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for plugin-backed jobs. + # @PRE Requested plugin ids resolve to executable plugins and task state transitions target known TaskStatus values. + # @POST Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state. + # @SIDE_EFFECT Schedules async execution, pauses on input/mapping requests, and mutates persisted task lifecycle markers. + # @DATA_CONTRACT Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result] + # @INVARIANT A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created. + # @RELATION DEPENDS_ON -> [TaskGraph] + # @RELATION DEPENDS_ON -> [EventBus] + # @RELATION DEPENDS_ON -> [TaskContext] + # @RELATION DEPENDS_ON -> [PluginLoader] + # #endregion JobLifecycle - # [DEF:__init__:Function] - # @COMPLEXITY: 5 - # @PURPOSE: Initialize the TaskManager with dependencies. - # @PRE: plugin_loader is initialized. - # @POST: TaskManager is ready to accept tasks. - # @SIDE_EFFECT: Starts background flusher thread and loads persisted task state into memory. - # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks] - # @RELATION: [DEPENDS_ON] ->[TaskGraph] - # @RELATION: [DEPENDS_ON] ->[EventBus] + # #region __init__ [C:5] [TYPE Function] + # @BRIEF Initialize the TaskManager with dependencies. + # @PRE plugin_loader is initialized. + # @POST TaskManager is ready to accept tasks. + # @SIDE_EFFECT Starts background flusher thread and loads persisted task state into memory. + # @RELATION CALLS -> [TaskPersistenceService.load_tasks] + # @RELATION DEPENDS_ON -> [TaskGraph] + # @RELATION DEPENDS_ON -> [EventBus] # @PARAM: plugin_loader - The plugin loader instance. def __init__(self, plugin_loader): with belief_scope("TaskManager.__init__"): @@ -156,30 +148,28 @@ class TaskManager: payload={"task_count": len(self.tasks)}, ) - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:_flusher_loop:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Background thread that periodically flushes log buffer to database. - # @PRE: TaskManager is initialized. - # @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds. - # @RELATION: [CALLS] ->[TaskManager._flush_logs] - # @RELATION: [DEPENDS_ON] ->[EventBus] + # #region _flusher_loop [C:3] [TYPE Function] + # @BRIEF Background thread that periodically flushes log buffer to database. + # @PRE TaskManager is initialized. + # @POST Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds. + # @RELATION CALLS -> [TaskManager._flush_logs] + # @RELATION DEPENDS_ON -> [EventBus] def _flusher_loop(self): """Background thread that flushes log buffer to database.""" while not self._flusher_stop_event.is_set(): self._flush_logs() self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL) - # [/DEF:_flusher_loop:Function] + # #endregion _flusher_loop - # [DEF:_flush_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Flush all buffered logs to the database. - # @PRE: None. - # @POST: All buffered logs are written to task_logs table. - # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs] - # @RELATION: [DEPENDS_ON] ->[EventBus] + # #region _flush_logs [C:3] [TYPE Function] + # @BRIEF Flush all buffered logs to the database. + # @PRE None. + # @POST All buffered logs are written to task_logs table. + # @RELATION CALLS -> [TaskLogPersistenceService.add_logs] + # @RELATION DEPENDS_ON -> [EventBus] def _flush_logs(self): """Flush all buffered logs to the database.""" with self._log_buffer_lock: @@ -202,13 +192,12 @@ class TaskManager: self._log_buffer[task_id] = [] self._log_buffer[task_id].extend(logs) - # [/DEF:_flush_logs:Function] + # #endregion _flush_logs - # [DEF:_flush_task_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Flush logs for a specific task immediately. - # @PRE: task_id exists. - # @POST: Task's buffered logs are written to database. + # #region _flush_task_logs [C:3] [TYPE Function] + # @BRIEF Flush logs for a specific task immediately. + # @PRE task_id exists. + # @POST Task's buffered logs are written to database. # @PARAM: task_id (str) - The task ID. # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs] # @RELATION: [DEPENDS_ON] ->[EventBus] @@ -226,13 +215,12 @@ class TaskManager: log.explore("Failed to flush task logs immediately", error=str(e), payload={"task_id": task_id}) logger.error(f"Failed to flush logs for task {task_id}: {e}") - # [/DEF:_flush_task_logs:Function] + # #endregion _flush_task_logs - # [DEF:create_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Creates and queues a new task for execution. - # @PRE: Plugin with plugin_id exists. Params are valid. - # @POST: Task is created, added to registry, and scheduled for execution. + # #region create_task [C:3] [TYPE Function] + # @BRIEF Creates and queues a new task for execution. + # @PRE Plugin with plugin_id exists. Params are valid. + # @POST Task is created, added to registry, and scheduled for execution. # @PARAM: plugin_id (str) - The ID of the plugin to run. # @PARAM: params (Dict[str, Any]) - Parameters for the plugin. # @PARAM: user_id (Optional[str]) - ID of the user requesting the task. @@ -269,13 +257,12 @@ class TaskManager: ) return task - # [/DEF:create_task:Function] + # #endregion create_task - # [DEF:_run_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Internal method to execute a task with TaskContext support. - # @PRE: Task exists in registry. - # @POST: Task is executed, status updated to SUCCESS or FAILED. + # #region _run_task [C:3] [TYPE Function] + # @BRIEF Internal method to execute a task with TaskContext support. + # @PRE Task exists in registry. + # @POST Task is executed, status updated to SUCCESS or FAILED. # @PARAM: task_id (str) - The ID of the task to run. # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task] # @RELATION: [DEPENDS_ON] ->[JobLifecycle] @@ -364,13 +351,12 @@ class TaskManager: payload={"task_id": task_id, "status": str(task.status)}, ) - # [/DEF:_run_task:Function] + # #endregion _run_task - # [DEF:resolve_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Resumes a task that is awaiting mapping. - # @PRE: Task exists and is in AWAITING_MAPPING state. - # @POST: Task status updated to RUNNING, params updated, execution resumed. + # #region resolve_task [C:3] [TYPE Function] + # @BRIEF Resumes a task that is awaiting mapping. + # @PRE Task exists and is in AWAITING_MAPPING state. + # @POST Task status updated to RUNNING, params updated, execution resumed. # @PARAM: task_id (str) - The ID of the task. # @PARAM: resolution_params (Dict[str, Any]) - Params to resolve the wait. # @THROWS: ValueError if task not found or not awaiting mapping. @@ -392,13 +378,12 @@ class TaskManager: if task_id in self.task_futures: self.task_futures[task_id].set_result(True) - # [/DEF:resolve_task:Function] + # #endregion resolve_task - # [DEF:wait_for_resolution:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Pauses execution and waits for a resolution signal. - # @PRE: Task exists. - # @POST: Execution pauses until future is set. + # #region wait_for_resolution [C:3] [TYPE Function] + # @BRIEF Pauses execution and waits for a resolution signal. + # @PRE Task exists. + # @POST Execution pauses until future is set. # @PARAM: task_id (str) - The ID of the task. # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task] # @RELATION: [DEPENDS_ON] ->[JobLifecycle] @@ -418,13 +403,12 @@ class TaskManager: if task_id in self.task_futures: del self.task_futures[task_id] - # [/DEF:wait_for_resolution:Function] + # #endregion wait_for_resolution - # [DEF:wait_for_input:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Pauses execution and waits for user input. - # @PRE: Task exists. - # @POST: Execution pauses until future is set via resume_task_with_password. + # #region wait_for_input [C:3] [TYPE Function] + # @BRIEF Pauses execution and waits for user input. + # @PRE Task exists. + # @POST Execution pauses until future is set via resume_task_with_password. # @PARAM: task_id (str) - The ID of the task. # @RELATION: [DEPENDS_ON] ->[JobLifecycle] async def wait_for_input(self, task_id: str): @@ -442,13 +426,12 @@ class TaskManager: if task_id in self.task_futures: del self.task_futures[task_id] - # [/DEF:wait_for_input:Function] + # #endregion wait_for_input - # [DEF:get_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Retrieves a task by its ID. - # @PRE: task_id is a string. - # @POST: Returns Task object or None. + # #region get_task [C:3] [TYPE Function] + # @BRIEF Retrieves a task by its ID. + # @PRE task_id is a string. + # @POST Returns Task object or None. # @PARAM: task_id (str) - ID of the task. # @RETURN: Optional[Task] - The task or None. # @RELATION: [DEPENDS_ON] ->[TaskGraph] @@ -456,26 +439,24 @@ class TaskManager: with belief_scope("TaskManager.get_task", f"task_id={task_id}"): return self.tasks.get(task_id) - # [/DEF:get_task:Function] + # #endregion get_task - # [DEF:get_all_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Retrieves all registered tasks. - # @PRE: None. - # @POST: Returns list of all Task objects. - # @RETURN: List[Task] - All tasks. - # @RELATION: [DEPENDS_ON] ->[TaskGraph] + # #region get_all_tasks [C:3] [TYPE Function] + # @BRIEF Retrieves all registered tasks. + # @PRE None. + # @POST Returns list of all Task objects. + # @RETURN List[Task] - All tasks. + # @RELATION DEPENDS_ON -> [TaskGraph] def get_all_tasks(self) -> List[Task]: with belief_scope("TaskManager.get_all_tasks"): return list(self.tasks.values()) - # [/DEF:get_all_tasks:Function] + # #endregion get_all_tasks - # [DEF:get_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Retrieves tasks with pagination and optional status filter. - # @PRE: limit and offset are non-negative integers. - # @POST: Returns a list of tasks sorted by start_time descending. + # #region get_tasks [C:3] [TYPE Function] + # @BRIEF Retrieves tasks with pagination and optional status filter. + # @PRE limit and offset are non-negative integers. + # @POST Returns a list of tasks sorted by start_time descending. # @PARAM: limit (int) - Maximum number of tasks to return. # @PARAM: offset (int) - Number of tasks to skip. # @PARAM: status (Optional[TaskStatus]) - Filter by task status. @@ -515,13 +496,12 @@ class TaskManager: tasks.sort(key=sort_key, reverse=True) return tasks[offset : offset + limit] - # [/DEF:get_tasks:Function] + # #endregion get_tasks - # [DEF:get_task_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Retrieves logs for a specific task (from memory for running, persistence for completed). - # @PRE: task_id is a string. - # @POST: Returns list of LogEntry or TaskLog objects. + # #region get_task_logs [C:3] [TYPE Function] + # @BRIEF Retrieves logs for a specific task (from memory for running, persistence for completed). + # @PRE task_id is a string. + # @POST Returns list of LogEntry or TaskLog objects. # @PARAM: task_id (str) - ID of the task. # @PARAM: log_filter (Optional[LogFilter]) - Filter parameters. # @RETURN: List[LogEntry] - List of log entries. @@ -553,13 +533,12 @@ class TaskManager: # For running/pending tasks, return from memory return task.logs if task else [] - # [/DEF:get_task_logs:Function] + # #endregion get_task_logs - # [DEF:get_task_log_stats:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get statistics about logs for a task. - # @PRE: task_id is a valid task ID. - # @POST: Returns LogStats with counts by level and source. + # #region get_task_log_stats [C:3] [TYPE Function] + # @BRIEF Get statistics about logs for a task. + # @PRE task_id is a valid task ID. + # @POST Returns LogStats with counts by level and source. # @PARAM: task_id (str) - The task ID. # @RETURN: LogStats - Statistics about task logs. # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_log_stats] @@ -568,13 +547,12 @@ class TaskManager: with belief_scope("TaskManager.get_task_log_stats", f"task_id={task_id}"): return self.log_persistence_service.get_log_stats(task_id) - # [/DEF:get_task_log_stats:Function] + # #endregion get_task_log_stats - # [DEF:get_task_log_sources:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get unique sources for a task's logs. - # @PRE: task_id is a valid task ID. - # @POST: Returns list of unique source strings. + # #region get_task_log_sources [C:3] [TYPE Function] + # @BRIEF Get unique sources for a task's logs. + # @PRE task_id is a valid task ID. + # @POST Returns list of unique source strings. # @PARAM: task_id (str) - The task ID. # @RETURN: List[str] - Unique source names. # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_sources] @@ -583,13 +561,12 @@ class TaskManager: with belief_scope("TaskManager.get_task_log_sources", f"task_id={task_id}"): return self.log_persistence_service.get_sources(task_id) - # [/DEF:get_task_log_sources:Function] + # #endregion get_task_log_sources - # [DEF:_add_log:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Adds a log entry to a task buffer and notifies subscribers. - # @PRE: Task exists. - # @POST: Log added to buffer and pushed to queues (if level meets task_log_level filter). + # #region _add_log [C:3] [TYPE Function] + # @BRIEF Adds a log entry to a task buffer and notifies subscribers. + # @PRE Task exists. + # @POST Log added to buffer and pushed to queues (if level meets task_log_level filter). # @PARAM: task_id (str) - ID of the task. # @PARAM: level (str) - Log level. # @PARAM: message (str) - Log message. @@ -639,13 +616,12 @@ class TaskManager: for queue in self.subscribers[task_id]: self.loop.call_soon_threadsafe(queue.put_nowait, log_entry) - # [/DEF:_add_log:Function] + # #endregion _add_log - # [DEF:subscribe_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Subscribes to real-time logs for a task. - # @PRE: task_id is a string. - # @POST: Returns an asyncio.Queue for log entries. + # #region subscribe_logs [C:3] [TYPE Function] + # @BRIEF Subscribes to real-time logs for a task. + # @PRE task_id is a string. + # @POST Returns an asyncio.Queue for log entries. # @PARAM: task_id (str) - ID of the task. # @RETURN: asyncio.Queue - Queue for log entries. # @RELATION: [DEPENDS_ON] ->[EventBus] @@ -657,13 +633,12 @@ class TaskManager: self.subscribers[task_id].append(queue) return queue - # [/DEF:subscribe_logs:Function] + # #endregion subscribe_logs - # [DEF:unsubscribe_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Unsubscribes from real-time logs for a task. - # @PRE: task_id is a string, queue is asyncio.Queue. - # @POST: Queue removed from subscribers. + # #region unsubscribe_logs [C:3] [TYPE Function] + # @BRIEF Unsubscribes from real-time logs for a task. + # @PRE task_id is a string, queue is asyncio.Queue. + # @POST Queue removed from subscribers. # @PARAM: task_id (str) - ID of the task. # @PARAM: queue (asyncio.Queue) - Queue to remove. # @RELATION: [DEPENDS_ON] ->[EventBus] @@ -675,15 +650,14 @@ class TaskManager: if not self.subscribers[task_id]: del self.subscribers[task_id] - # [/DEF:unsubscribe_logs:Function] + # #endregion unsubscribe_logs - # [DEF:load_persisted_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Load persisted tasks using persistence service. - # @PRE: None. - # @POST: Persisted tasks loaded into self.tasks. - # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks] - # @RELATION: [DEPENDS_ON] ->[TaskGraph] + # #region load_persisted_tasks [C:3] [TYPE Function] + # @BRIEF Load persisted tasks using persistence service. + # @PRE None. + # @POST Persisted tasks loaded into self.tasks. + # @RELATION CALLS -> [TaskPersistenceService.load_tasks] + # @RELATION DEPENDS_ON -> [TaskGraph] def load_persisted_tasks(self) -> None: with belief_scope("TaskManager.load_persisted_tasks"): loaded_tasks = self.persistence_service.load_tasks(limit=100) @@ -691,13 +665,12 @@ class TaskManager: if task.id not in self.tasks: self.tasks[task.id] = task - # [/DEF:load_persisted_tasks:Function] + # #endregion load_persisted_tasks - # [DEF:await_input:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Transition a task to AWAITING_INPUT state with input request. - # @PRE: Task exists and is in RUNNING state. - # @POST: Task status changed to AWAITING_INPUT, input_request set, persisted. + # #region await_input [C:3] [TYPE Function] + # @BRIEF Transition a task to AWAITING_INPUT state with input request. + # @PRE Task exists and is in RUNNING state. + # @POST Task status changed to AWAITING_INPUT, input_request set, persisted. # @PARAM: task_id (str) - ID of the task. # @PARAM: input_request (Dict) - Details about required input. # @THROWS: ValueError if task not found or not RUNNING. @@ -724,13 +697,12 @@ class TaskManager: metadata={"input_request": input_request}, ) - # [/DEF:await_input:Function] + # #endregion await_input - # [DEF:resume_task_with_password:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Resume a task that is awaiting input with provided passwords. - # @PRE: Task exists and is in AWAITING_INPUT state. - # @POST: Task status changed to RUNNING, passwords injected, task resumed. + # #region resume_task_with_password [C:3] [TYPE Function] + # @BRIEF Resume a task that is awaiting input with provided passwords. + # @PRE Task exists and is in AWAITING_INPUT state. + # @POST Task status changed to RUNNING, passwords injected, task resumed. # @PARAM: task_id (str) - ID of the task. # @PARAM: passwords (Dict[str, str]) - Mapping of database name to password. # @THROWS: ValueError if task not found, not awaiting input, or passwords invalid. @@ -768,13 +740,12 @@ class TaskManager: if task_id in self.task_futures: self.task_futures[task_id].set_result(True) - # [/DEF:resume_task_with_password:Function] + # #endregion resume_task_with_password - # [DEF:clear_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Clears tasks based on status filter (also deletes associated logs). - # @PRE: status is Optional[TaskStatus]. - # @POST: Tasks matching filter (or all non-active) cleared from registry and database. + # #region clear_tasks [C:3] [TYPE Function] + # @BRIEF Clears tasks based on status filter (also deletes associated logs). + # @PRE status is Optional[TaskStatus]. + # @POST Tasks matching filter (or all non-active) cleared from registry and database. # @PARAM: status (Optional[TaskStatus]) - Filter by task status. # @RETURN: int - Number of tasks cleared. # @RELATION: [CALLS] ->[TaskPersistenceService.delete_tasks] @@ -823,8 +794,7 @@ class TaskManager: log.reflect("Tasks cleared from registry and database", payload={"count": len(tasks_to_remove)}) return len(tasks_to_remove) - # [/DEF:clear_tasks:Function] + # #endregion clear_tasks -# [/DEF:TaskManager:Class] -# [/DEF:TaskManagerModule:Module] +# #endregion TaskManager diff --git a/backend/src/core/task_manager/models.py b/backend/src/core/task_manager/models.py index aeff33d9..6a8c4f28 100644 --- a/backend/src/core/task_manager/models.py +++ b/backend/src/core/task_manager/models.py @@ -1,11 +1,9 @@ -# [DEF:TaskManagerModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: task, models, pydantic, enum, state -# @PURPOSE: Defines the data models and enumerations used by the Task Manager. -# @LAYER: Core -# @RELATION: [USED_BY] -> [TaskManager] -# @RELATION: [USED_BY] -> [TaskManagerPackage] -# @INVARIANT: Task IDs are immutable once created. +# #region TaskManagerModels [C:3] [TYPE Module] [SEMANTICS task, models, pydantic, enum, state] +# @BRIEF Defines the data models and enumerations used by the Task Manager. +# @LAYER Core +# @INVARIANT Task IDs are immutable once created. +# @RELATION USED_BY -> [TaskManager] +# @RELATION USED_BY -> [TaskManagerPackage] # @CONSTRAINT: Must use Pydantic for data validation. # [SECTION: IMPORTS] @@ -18,7 +16,7 @@ from pydantic import BaseModel, Field # [/SECTION] -# [DEF:TaskStatus:Enum] +# #region TaskStatus [TYPE Enum] class TaskStatus(str, Enum): PENDING = "PENDING" RUNNING = "RUNNING" @@ -28,10 +26,10 @@ class TaskStatus(str, Enum): AWAITING_INPUT = "AWAITING_INPUT" -# [/DEF:TaskStatus:Enum] +# #endregion TaskStatus -# [DEF:LogLevel:Enum] +# #region LogLevel [TYPE Enum] class LogLevel(str, Enum): DEBUG = "DEBUG" INFO = "INFO" @@ -39,12 +37,11 @@ class LogLevel(str, Enum): ERROR = "ERROR" -# [/DEF:LogLevel:Enum] +# #endregion LogLevel -# [DEF:LogEntry:Class] -# @PURPOSE: A Pydantic model representing a single, structured log entry associated with a task. -# @COMPLEXITY: 2 +# #region LogEntry [C:2] [TYPE Class] +# @BRIEF A Pydantic model representing a single, structured log entry associated with a task. # # { # required_fields: {message: str}, @@ -65,14 +62,12 @@ class LogEntry(BaseModel): ) -# [/DEF:LogEntry:Class] +# #endregion LogEntry -# [DEF:TaskLog:Class] -# @SEMANTICS: task, log, persistent, pydantic -# @PURPOSE: A Pydantic model representing a persisted log entry from the database. -# @COMPLEXITY: 3 -# @RELATION: [DEPENDS_ON] -> [TaskLogRecord] +# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic] +# @BRIEF A Pydantic model representing a persisted log entry from the database. +# @RELATION DEPENDS_ON -> [TaskLogRecord] class TaskLog(BaseModel): id: int task_id: str @@ -86,10 +81,10 @@ class TaskLog(BaseModel): from_attributes = True -# [/DEF:TaskLog:Class] +# #endregion TaskLog -# [DEF:LogFilter:Class] +# #region LogFilter [TYPE Class] class LogFilter(BaseModel): level: Optional[str] = None # Filter by log level source: Optional[str] = None # Filter by source component @@ -98,30 +93,26 @@ class LogFilter(BaseModel): limit: int = Field(default=100, ge=1, le=1000) -# [/DEF:LogFilter:Class] +# #endregion LogFilter -# [DEF:LogStats:Class] -# @SEMANTICS: log, stats, aggregation, pydantic -# @PURPOSE: Statistics about log entries for a task. -# @COMPLEXITY: 1 -# @RELATION: [DEPENDS_ON] -> [TaskLog] +# #region LogStats [C:1] [TYPE Class] [SEMANTICS log, stats, aggregation, pydantic] +# @BRIEF Statistics about log entries for a task. +# @RELATION DEPENDS_ON -> [TaskLog] class LogStats(BaseModel): total_count: int by_level: Dict[str, int] # {"INFO": 10, "ERROR": 2} by_source: Dict[str, int] # {"plugin": 5, "superset_api": 7} -# [/DEF:LogStats:Class] +# #endregion LogStats -# [DEF:Task:Class] -# @COMPLEXITY: 3 -# @SEMANTICS: task, job, execution, state, pydantic -# @PURPOSE: A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs. -# @RELATION: [DEPENDS_ON] -> [TaskStatus] -# @RELATION: [DEPENDS_ON] -> [LogEntry] -# @RELATION: [DEPENDS_ON] -> [TaskManager] +# #region Task [C:3] [TYPE Class] [SEMANTICS task, job, execution, state, pydantic] +# @BRIEF A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs. +# @RELATION DEPENDS_ON -> [TaskStatus] +# @RELATION DEPENDS_ON -> [LogEntry] +# @RELATION DEPENDS_ON -> [TaskManager] class Task(BaseModel): id: str = Field(default_factory=lambda: str(uuid.uuid4())) plugin_id: str @@ -136,19 +127,19 @@ class Task(BaseModel): # Result payload can be dict/list/scalar depending on plugin and legacy records. result: Optional[Any] = None - # [DEF:__init__:Function] - # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status. - # @PRE: If status is AWAITING_INPUT, input_request must be provided. - # @POST: Task instance is created or ValueError is raised. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the Task model and validates input_request for AWAITING_INPUT status. + # @PRE If status is AWAITING_INPUT, input_request must be provided. + # @POST Task instance is created or ValueError is raised. # @PARAM: **data - Keyword arguments for model initialization. def __init__(self, **data): super().__init__(**data) if self.status == TaskStatus.AWAITING_INPUT and not self.input_request: raise ValueError("input_request is required when status is AWAITING_INPUT") - # [/DEF:__init__:Function] + # #endregion __init__ -# [/DEF:Task:Class] +# #endregion Task -# [/DEF:TaskManagerModels:Module] +# #endregion TaskManagerModels diff --git a/backend/src/core/task_manager/persistence.py b/backend/src/core/task_manager/persistence.py index 7bdaee11..3414f597 100644 --- a/backend/src/core/task_manager/persistence.py +++ b/backend/src/core/task_manager/persistence.py @@ -1,16 +1,14 @@ -# [DEF:TaskPersistenceModule:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage -# @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database. -# @LAYER: Core -# @PRE: Tasks database must be initialized with TaskRecord and TaskLogRecord schemas. -# @POST: Provides reliable storage and retrieval for task metadata and logs. -# @SIDE_EFFECT: Performs database I/O on tasks.db. -# @DATA_CONTRACT: Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord] -# @RELATION: [DEPENDS_ON] ->[TaskManager] -# @RELATION: [DEPENDS_ON] ->[TaskGraph] -# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal] -# @INVARIANT: Database schema must match the TaskRecord model structure. +# #region TaskPersistenceModule [C:5] [TYPE Module] [SEMANTICS persistence, sqlite, sqlalchemy, task, storage] +# @BRIEF Handles the persistence of tasks using SQLAlchemy and the tasks.db database. +# @LAYER Core +# @PRE Tasks database must be initialized with TaskRecord and TaskLogRecord schemas. +# @POST Provides reliable storage and retrieval for task metadata and logs. +# @SIDE_EFFECT Performs database I/O on tasks.db. +# @DATA_CONTRACT Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord] +# @INVARIANT Database schema must match the TaskRecord model structure. +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [TaskGraph] +# @RELATION DEPENDS_ON -> [TasksSessionLocal] # [SECTION: IMPORTS] from datetime import datetime @@ -31,20 +29,18 @@ log_pl = MarkerLogger("TaskLogPersistence") # [/SECTION] -# [DEF:TaskPersistenceService:Class] -# @COMPLEXITY: 5 -# @SEMANTICS: persistence, service, database, sqlalchemy -# @PURPOSE: Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models. -# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available. -# @POST: Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows. -# @SIDE_EFFECT: Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures. -# @DATA_CONTRACT: Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]] -# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal] -# @RELATION: [DEPENDS_ON] ->[TaskRecord] -# @RELATION: [DEPENDS_ON] ->[Environment] -# @RELATION: [DEPENDS_ON] ->[TaskManager] -# @RELATION: [DEPENDS_ON] ->[TaskGraph] -# @INVARIANT: Persistence must handle potentially missing task fields natively. +# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy] +# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models. +# @PRE TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available. +# @POST Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows. +# @SIDE_EFFECT Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures. +# @DATA_CONTRACT Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]] +# @INVARIANT Persistence must handle potentially missing task fields natively. +# @RELATION DEPENDS_ON -> [TasksSessionLocal] +# @RELATION DEPENDS_ON -> [TaskRecord] +# @RELATION DEPENDS_ON -> [Environment] +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [TaskGraph] # # @TEST_CONTRACT: TaskPersistenceContract -> # { @@ -60,11 +56,10 @@ log_pl = MarkerLogger("TaskLogPersistence") # @TEST_EDGE: load_corrupt_json_params -> handled gracefully # @TEST_INVARIANT: accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params] class TaskPersistenceService: - # [DEF:_json_load_if_needed:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Safely load JSON strings from DB if necessary - # @PRE: value is an arbitrary database value - # @POST: Returns parsed JSON object, list, string, or primitive + # #region _json_load_if_needed [C:1] [TYPE Function] + # @BRIEF Safely load JSON strings from DB if necessary + # @PRE value is an arbitrary database value + # @POST Returns parsed JSON object, list, string, or primitive @staticmethod def _json_load_if_needed(value): with belief_scope("TaskPersistenceService._json_load_if_needed"): @@ -82,13 +77,12 @@ class TaskPersistenceService: return value return value - # [/DEF:_json_load_if_needed:Function] + # #endregion _json_load_if_needed - # [DEF:_parse_datetime:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Safely parse a datetime string from the database - # @PRE: value is an ISO string or datetime object - # @POST: Returns datetime object or None + # #region _parse_datetime [C:1] [TYPE Function] + # @BRIEF Safely parse a datetime string from the database + # @PRE value is an ISO string or datetime object + # @POST Returns datetime object or None @staticmethod def _parse_datetime(value): with belief_scope("TaskPersistenceService._parse_datetime"): @@ -101,15 +95,14 @@ class TaskPersistenceService: return None return None - # [/DEF:_parse_datetime:Function] + # #endregion _parse_datetime - # [DEF:_resolve_environment_id:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints. - # @PRE: Session is active - # @POST: Returns existing environments.id or None when unresolved. - # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]] - # @RELATION: [DEPENDS_ON] ->[Environment] + # #region _resolve_environment_id [C:3] [TYPE Function] + # @BRIEF Resolve environment id into existing environments.id value to satisfy FK constraints. + # @PRE Session is active + # @POST Returns existing environments.id or None when unresolved. + # @DATA_CONTRACT Input[env_id: Optional[str]] -> Output[Optional[str]] + # @RELATION DEPENDS_ON -> [Environment] @staticmethod def _resolve_environment_id( session: Session, env_id: Optional[str] @@ -151,25 +144,23 @@ class TaskPersistenceService: return None - # [/DEF:_resolve_environment_id:Function] + # #endregion _resolve_environment_id - # [DEF:__init__:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Initializes the persistence service. - # @PRE: None. - # @POST: Service is ready. + # #region __init__ [C:3] [TYPE Function] + # @BRIEF Initializes the persistence service. + # @PRE None. + # @POST Service is ready. def __init__(self): with belief_scope("TaskPersistenceService.__init__"): # We use TasksSessionLocal from database.py pass - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:persist_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persists or updates a single task in the database. - # @PRE: isinstance(task, Task) - # @POST: Task record created or updated in database. + # #region persist_task [C:3] [TYPE Function] + # @BRIEF Persists or updates a single task in the database. + # @PRE isinstance(task, Task) + # @POST Task record created or updated in database. # @PARAM: task (Task) - The task object to persist. # @SIDE_EFFECT: Writes to task_records table in tasks.db # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord] @@ -238,13 +229,12 @@ class TaskPersistenceService: finally: session.close() - # [/DEF:persist_task:Function] + # #endregion persist_task - # [DEF:persist_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persists multiple tasks. - # @PRE: isinstance(tasks, list) - # @POST: All tasks in list are persisted. + # #region persist_tasks [C:3] [TYPE Function] + # @BRIEF Persists multiple tasks. + # @PRE isinstance(tasks, list) + # @POST All tasks in list are persisted. # @PARAM: tasks (List[Task]) - The list of tasks to persist. # @RELATION: [CALLS] ->[persist_task] def persist_tasks(self, tasks: List[Task]) -> None: @@ -254,13 +244,12 @@ class TaskPersistenceService: self.persist_task(task) log.reflect("All tasks persisted", payload={"count": len(tasks)}) - # [/DEF:persist_tasks:Function] + # #endregion persist_tasks - # [DEF:load_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Loads tasks from the database. - # @PRE: limit is an integer. - # @POST: Returns list of Task objects. + # #region load_tasks [C:3] [TYPE Function] + # @BRIEF Loads tasks from the database. + # @PRE limit is an integer. + # @POST Returns list of Task objects. # @PARAM: limit (int) - Max tasks to load. # @PARAM: status (Optional[TaskStatus]) - Filter by status. # @RETURN: List[Task] - The loaded tasks. @@ -323,13 +312,12 @@ class TaskPersistenceService: finally: session.close() - # [/DEF:load_tasks:Function] + # #endregion load_tasks - # [DEF:delete_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Deletes specific tasks from the database. - # @PRE: task_ids is a list of strings. - # @POST: Specified task records deleted from database. + # #region delete_tasks [C:3] [TYPE Function] + # @BRIEF Deletes specific tasks from the database. + # @PRE task_ids is a list of strings. + # @POST Specified task records deleted from database. # @PARAM: task_ids (List[str]) - List of task IDs to delete. # @SIDE_EFFECT: Deletes rows from task_records table. # @RELATION: [DEPENDS_ON] ->[TaskRecord] @@ -352,25 +340,15 @@ class TaskPersistenceService: finally: session.close() - # [/DEF:delete_tasks:Function] + # #endregion delete_tasks -# [/DEF:TaskPersistenceService:Class] - - -# [DEF:TaskLogPersistenceService:Class] -# @COMPLEXITY: 5 -# @SEMANTICS: persistence, service, database, log, sqlalchemy -# @PURPOSE: Provides methods to store, query, summarize, and delete task log rows in the task_logs table. -# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries. -# @POST: add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers. -# @SIDE_EFFECT: Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures. -# @DATA_CONTRACT: Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]] -# @RELATION: [DEPENDS_ON] ->[TaskLogRecord] -# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal] -# @RELATION: [DEPENDS_ON] ->[TaskManager] -# @RELATION: [DEPENDS_ON] ->[EventBus] -# @INVARIANT: Log entries are batch-inserted for performance. +# #endregion TaskPersistenceService +# @INVARIANT Log entries are batch-inserted for performance. +# @RELATION DEPENDS_ON -> [TaskLogRecord] +# @RELATION DEPENDS_ON -> [TasksSessionLocal] +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [EventBus] # # @TEST_CONTRACT: TaskLogPersistenceContract -> # { @@ -390,21 +368,19 @@ class TaskLogPersistenceService: Supports batch inserts, filtering, and statistics. """ - # [DEF:__init__:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Initializes the TaskLogPersistenceService - # @PRE: config is provided or defaults are used - # @POST: Service is ready for log persistence + # #region __init__ [C:3] [TYPE Function] + # @BRIEF Initializes the TaskLogPersistenceService + # @PRE config is provided or defaults are used + # @POST Service is ready for log persistence def __init__(self, config=None): pass - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:add_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Batch insert log entries for a task. - # @PRE: logs is a list of LogEntry objects. - # @POST: All logs inserted into task_logs table. + # #region add_logs [C:3] [TYPE Function] + # @BRIEF Batch insert log entries for a task. + # @PRE logs is a list of LogEntry objects. + # @POST All logs inserted into task_logs table. # @PARAM: task_id (str) - The task ID. # @PARAM: logs (List[LogEntry]) - Log entries to insert. # @SIDE_EFFECT: Writes to task_logs table. @@ -438,13 +414,12 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:add_logs:Function] + # #endregion add_logs - # [DEF:get_logs:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Query logs for a task with filtering and pagination. - # @PRE: task_id is a valid task ID. - # @POST: Returns list of TaskLog objects matching filters. + # #region get_logs [C:3] [TYPE Function] + # @BRIEF Query logs for a task with filtering and pagination. + # @PRE task_id is a valid task ID. + # @POST Returns list of TaskLog objects matching filters. # @PARAM: task_id (str) - The task ID. # @PARAM: log_filter (LogFilter) - Filter parameters. # @RETURN: List[TaskLog] - Filtered log entries. @@ -504,13 +479,12 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:get_logs:Function] + # #endregion get_logs - # [DEF:get_log_stats:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get statistics about logs for a task. - # @PRE: task_id is a valid task ID. - # @POST: Returns LogStats with counts by level and source. + # #region get_log_stats [C:3] [TYPE Function] + # @BRIEF Get statistics about logs for a task. + # @PRE task_id is a valid task ID. + # @POST Returns LogStats with counts by level and source. # @PARAM: task_id (str) - The task ID. # @RETURN: LogStats - Statistics about task logs. # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats] @@ -559,13 +533,12 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:get_log_stats:Function] + # #endregion get_log_stats - # [DEF:get_sources:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get unique sources for a task's logs. - # @PRE: task_id is a valid task ID. - # @POST: Returns list of unique source strings. + # #region get_sources [C:3] [TYPE Function] + # @BRIEF Get unique sources for a task's logs. + # @PRE task_id is a valid task ID. + # @POST Returns list of unique source strings. # @PARAM: task_id (str) - The task ID. # @RETURN: List[str] - Unique source names. # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]] @@ -590,13 +563,12 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:get_sources:Function] + # #endregion get_sources - # [DEF:delete_logs_for_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Delete all logs for a specific task. - # @PRE: task_id is a valid task ID. - # @POST: All logs for the task are deleted. + # #region delete_logs_for_task [C:3] [TYPE Function] + # @BRIEF Delete all logs for a specific task. + # @PRE task_id is a valid task ID. + # @POST All logs for the task are deleted. # @PARAM: task_id (str) - The task ID. # @SIDE_EFFECT: Deletes from task_logs table. # @RELATION: [DEPENDS_ON] ->[TaskLogRecord] @@ -619,13 +591,12 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:delete_logs_for_task:Function] + # #endregion delete_logs_for_task - # [DEF:delete_logs_for_tasks:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Delete all logs for multiple tasks. - # @PRE: task_ids is a list of task IDs. - # @POST: All logs for the tasks are deleted. + # #region delete_logs_for_tasks [C:3] [TYPE Function] + # @BRIEF Delete all logs for multiple tasks. + # @PRE task_ids is a list of task IDs. + # @POST All logs for the tasks are deleted. # @PARAM: task_ids (List[str]) - List of task IDs. # @SIDE_EFFECT: Deletes rows from task_logs table. # @RELATION: [DEPENDS_ON] ->[TaskLogRecord] @@ -648,8 +619,7 @@ class TaskLogPersistenceService: finally: session.close() - # [/DEF:delete_logs_for_tasks:Function] + # #endregion delete_logs_for_tasks -# [/DEF:TaskLogPersistenceService:Class] -# [/DEF:TaskPersistenceModule:Module] +# #endregion TaskLogPersistenceService diff --git a/backend/src/core/task_manager/task_logger.py b/backend/src/core/task_manager/task_logger.py index 62c59959..d62109d0 100644 --- a/backend/src/core/task_manager/task_logger.py +++ b/backend/src/core/task_manager/task_logger.py @@ -1,25 +1,21 @@ -# [DEF:TaskLoggerModule:Module] -# @SEMANTICS: task, logger, context, plugin, attribution -# @PURPOSE: Provides a dedicated logger for tasks with automatic source attribution. -# @LAYER: Core -# @RELATION: [DEPENDS_ON] -> [TaskManager] -# @RELATION: [DEPENDS_ON] -> [EventBus] -# @COMPLEXITY: 2 -# @INVARIANT: Each TaskLogger instance is bound to a specific task_id and default source. +# #region TaskLoggerModule [C:2] [TYPE Module] [SEMANTICS task, logger, context, plugin, attribution] +# @BRIEF Provides a dedicated logger for tasks with automatic source attribution. +# @LAYER Core +# @INVARIANT Each TaskLogger instance is bound to a specific task_id and default source. +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [EventBus] # [SECTION: IMPORTS] from typing import Dict, Any, Optional, Callable # [/SECTION] -# [DEF:TaskLogger:Class] -# @SEMANTICS: logger, task, source, attribution -# @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context. -# @COMPLEXITY: 2 -# @RELATION: [DEPENDS_ON] -> [TaskManager] -# @RELATION: [DEPENDS_ON] -> [EventBus] -# @RELATION: [USED_BY] -> [TaskContext] -# @INVARIANT: All log calls include the task_id and source. +# #region TaskLogger [C:2] [TYPE Class] [SEMANTICS logger, task, source, attribution] +# @BRIEF A wrapper around TaskManager._add_log that carries task_id and source context. +# @INVARIANT All log calls include the task_id and source. +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [EventBus] +# @RELATION USED_BY -> [TaskContext] # @UX_STATE: Idle -> Logging -> (system records log) # # @TEST_CONTRACT: TaskLoggerContract -> @@ -49,10 +45,10 @@ class TaskLogger: api_logger.info("Fetching dashboards") """ - # [DEF:__init__:Function] - # @PURPOSE: Initialize the TaskLogger with task context. - # @PRE: add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata). - # @POST: TaskLogger is ready to log messages. + # #region __init__ [TYPE Function] + # @BRIEF Initialize the TaskLogger with task context. + # @PRE add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata). + # @POST TaskLogger is ready to log messages. # @PARAM: task_id (str) - The ID of the task. # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager. # @PARAM: source (str) - Default source for logs (default: "plugin"). @@ -61,12 +57,12 @@ class TaskLogger: self._add_log = add_log_fn self._default_source = source - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:with_source:Function] - # @PURPOSE: Create a sub-logger with a different default source. - # @PRE: source is a non-empty string. - # @POST: Returns new TaskLogger with the same task_id but different source. + # #region with_source [TYPE Function] + # @BRIEF Create a sub-logger with a different default source. + # @PRE source is a non-empty string. + # @POST Returns new TaskLogger with the same task_id but different source. # @PARAM: source (str) - New default source. # @RETURN: TaskLogger - New logger instance. def with_source(self, source: str) -> "TaskLogger": @@ -75,12 +71,12 @@ class TaskLogger: task_id=self._task_id, add_log_fn=self._add_log, source=source ) - # [/DEF:with_source:Function] + # #endregion with_source - # [DEF:_log:Function] - # @PURPOSE: Internal method to log a message at a given level. - # @PRE: level is a valid log level string. - # @POST: Log entry added via add_log_fn. + # #region _log [TYPE Function] + # @BRIEF Internal method to log a message at a given level. + # @PRE level is a valid log level string. + # @POST Log entry added via add_log_fn. # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR). # @PARAM: message (str) - Log message. # @PARAM: source (Optional[str]) - Override source for this log entry. @@ -102,12 +98,12 @@ class TaskLogger: metadata=metadata, ) - # [/DEF:_log:Function] + # #endregion _log - # [DEF:debug:Function] - # @PURPOSE: Log a DEBUG level message. - # @PRE: message is a string. - # @POST: Log entry added via internally with DEBUG level. + # #region debug [TYPE Function] + # @BRIEF Log a DEBUG level message. + # @PRE message is a string. + # @POST Log entry added via internally with DEBUG level. # @PARAM: message (str) - Log message. # @PARAM: source (Optional[str]) - Override source. # @PARAM: metadata (Optional[Dict]) - Additional data. @@ -119,12 +115,12 @@ class TaskLogger: ) -> None: self._log("DEBUG", message, source, metadata) - # [/DEF:debug:Function] + # #endregion debug - # [DEF:info:Function] - # @PURPOSE: Log an INFO level message. - # @PRE: message is a string. - # @POST: Log entry added internally with INFO level. + # #region info [TYPE Function] + # @BRIEF Log an INFO level message. + # @PRE message is a string. + # @POST Log entry added internally with INFO level. # @PARAM: message (str) - Log message. # @PARAM: source (Optional[str]) - Override source. # @PARAM: metadata (Optional[Dict]) - Additional data. @@ -136,12 +132,12 @@ class TaskLogger: ) -> None: self._log("INFO", message, source, metadata) - # [/DEF:info:Function] + # #endregion info - # [DEF:warning:Function] - # @PURPOSE: Log a WARNING level message. - # @PRE: message is a string. - # @POST: Log entry added internally with WARNING level. + # #region warning [TYPE Function] + # @BRIEF Log a WARNING level message. + # @PRE message is a string. + # @POST Log entry added internally with WARNING level. # @PARAM: message (str) - Log message. # @PARAM: source (Optional[str]) - Override source. # @PARAM: metadata (Optional[Dict]) - Additional data. @@ -153,12 +149,12 @@ class TaskLogger: ) -> None: self._log("WARNING", message, source, metadata) - # [/DEF:warning:Function] + # #endregion warning - # [DEF:error:Function] - # @PURPOSE: Log an ERROR level message. - # @PRE: message is a string. - # @POST: Log entry added internally with ERROR level. + # #region error [TYPE Function] + # @BRIEF Log an ERROR level message. + # @PRE message is a string. + # @POST Log entry added internally with ERROR level. # @PARAM: message (str) - Log message. # @PARAM: source (Optional[str]) - Override source. # @PARAM: metadata (Optional[Dict]) - Additional data. @@ -170,12 +166,12 @@ class TaskLogger: ) -> None: self._log("ERROR", message, source, metadata) - # [/DEF:error:Function] + # #endregion error - # [DEF:progress:Function] - # @PURPOSE: Log a progress update with percentage. - # @PRE: percent is between 0 and 100. - # @POST: Log entry with progress metadata added. + # #region progress [TYPE Function] + # @BRIEF Log a progress update with percentage. + # @PRE percent is between 0 and 100. + # @POST Log entry with progress metadata added. # @PARAM: message (str) - Progress message. # @PARAM: percent (float) - Progress percentage (0-100). # @PARAM: source (Optional[str]) - Override source. @@ -186,9 +182,9 @@ class TaskLogger: metadata = {"progress": min(100, max(0, percent))} self._log("INFO", message, source, metadata) - # [/DEF:progress:Function] + # #endregion progress -# [/DEF:TaskLogger:Class] +# #endregion TaskLogger -# [/DEF:TaskLoggerModule:Module] +# #endregion TaskLoggerModule diff --git a/backend/src/core/utils/__init__.py b/backend/src/core/utils/__init__.py index 6f4eda66..4055b8f9 100644 --- a/backend/src/core/utils/__init__.py +++ b/backend/src/core/utils/__init__.py @@ -1,3 +1,3 @@ -# [DEF:CoreUtils:Package] -# @PURPOSE: Shared utility package root. -# [/DEF:CoreUtils:Package] +# #region CoreUtils [TYPE Package] +# @BRIEF Shared utility package root. +# #endregion CoreUtils diff --git a/backend/src/core/utils/async_network.py b/backend/src/core/utils/async_network.py index 78de1591..193656cb 100644 --- a/backend/src/core/utils/async_network.py +++ b/backend/src/core/utils/async_network.py @@ -1,15 +1,13 @@ -# [DEF:AsyncNetworkModule:Module] +# #region AsyncNetworkModule [C:5] [TYPE Module] [SEMANTICS network, httpx, async, superset, authentication, cache] +# @BRIEF Provides async Superset API client with shared auth-token cache to avoid per-request re-login. +# @LAYER Infra +# @PRE Config payloads contain a Superset base URL and authentication fields needed for login. +# @POST Async network clients reuse cached auth tokens and expose stable async request/error translation flow. +# @SIDE_EFFECT Performs upstream HTTP I/O and mutates process-local auth cache entries. +# @DATA_CONTRACT Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions] +# @INVARIANT Async client reuses cached auth tokens per environment credentials and invalidates on 401. +# @RELATION DEPENDS_ON -> [SupersetAuthCache] # -# @COMPLEXITY: 5 -# @SEMANTICS: network, httpx, async, superset, authentication, cache -# @PURPOSE: Provides async Superset API client with shared auth-token cache to avoid per-request re-login. -# @LAYER: Infra -# @PRE: Config payloads contain a Superset base URL and authentication fields needed for login. -# @POST: Async network clients reuse cached auth tokens and expose stable async request/error translation flow. -# @SIDE_EFFECT: Performs upstream HTTP I/O and mutates process-local auth cache entries. -# @DATA_CONTRACT: Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions] -# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache] -# @INVARIANT: Async client reuses cached auth tokens per environment credentials and invalidates on 401. # [SECTION: IMPORTS] from typing import Optional, Dict, Any, Union @@ -31,24 +29,22 @@ from .network import ( log = MarkerLogger("AsyncNetwork") -# [DEF:AsyncAPIClient:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Async Superset API client backed by httpx.AsyncClient with shared auth cache. -# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache] -# @RELATION: [CALLS] ->[SupersetAuthCache.get] -# @RELATION: [CALLS] ->[SupersetAuthCache.set] +# #region AsyncAPIClient [C:3] [TYPE Class] +# @BRIEF Async Superset API client backed by httpx.AsyncClient with shared auth cache. +# @RELATION DEPENDS_ON -> [SupersetAuthCache] +# @RELATION CALLS -> [SupersetAuthCache.get] +# @RELATION CALLS -> [SupersetAuthCache.set] class AsyncAPIClient: DEFAULT_TIMEOUT = 30 _auth_locks: Dict[tuple[str, str, bool], asyncio.Lock] = {} - # [DEF:AsyncAPIClient.__init__:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Initialize async API client for one environment. - # @PRE: config contains base_url and auth payload. - # @POST: Client is ready for async request/authentication flow. - # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str] - # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url] - # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache] + # #region AsyncAPIClient.__init__ [C:3] [TYPE Function] + # @BRIEF Initialize async API client for one environment. + # @PRE config contains base_url and auth payload. + # @POST Client is ready for async request/authentication flow. + # @DATA_CONTRACT Input[config: Dict[str, Any]] -> self._auth_cache_key[str] + # @RELATION CALLS -> [AsyncAPIClient._normalize_base_url] + # @RELATION DEPENDS_ON -> [SupersetAuthCache] def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT): self.base_url: str = self._normalize_base_url(config.get("base_url", "")) self.api_base_url: str = f"{self.base_url}/api/v1" @@ -67,23 +63,21 @@ class AsyncAPIClient: verify_ssl, ) - # [/DEF:AsyncAPIClient.__init__:Function] + # #endregion AsyncAPIClient.__init__ - # [DEF:AsyncAPIClient._normalize_base_url:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Normalize base URL for Superset API root construction. - # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix. + # #region AsyncAPIClient._normalize_base_url [C:1] [TYPE Function] + # @BRIEF Normalize base URL for Superset API root construction. + # @POST Returns canonical base URL without trailing slash and duplicate /api/v1 suffix. def _normalize_base_url(self, raw_url: str) -> str: normalized = str(raw_url or "").strip().rstrip("/") if normalized.lower().endswith("/api/v1"): normalized = normalized[:-len("/api/v1")] return normalized.rstrip("/") - # [/DEF:AsyncAPIClient._normalize_base_url:Function] + # #endregion AsyncAPIClient._normalize_base_url - # [DEF:AsyncAPIClient._build_api_url:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Build full API URL from relative Superset endpoint. - # @POST: Returns absolute URL for upstream request. + # #region AsyncAPIClient._build_api_url [C:1] [TYPE Function] + # @BRIEF Build full API URL from relative Superset endpoint. + # @POST Returns absolute URL for upstream request. def _build_api_url(self, endpoint: str) -> str: normalized_endpoint = str(endpoint or "").strip() if normalized_endpoint.startswith("http://") or normalized_endpoint.startswith("https://"): @@ -93,12 +87,11 @@ class AsyncAPIClient: if normalized_endpoint.startswith("/api/v1/") or normalized_endpoint == "/api/v1": return f"{self.base_url}{normalized_endpoint}" return f"{self.api_base_url}{normalized_endpoint}" - # [/DEF:AsyncAPIClient._build_api_url:Function] + # #endregion AsyncAPIClient._build_api_url - # [DEF:AsyncAPIClient._get_auth_lock:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts. - # @POST: Returns stable asyncio.Lock instance. + # #region AsyncAPIClient._get_auth_lock [C:1] [TYPE Function] + # @BRIEF Return per-cache-key async lock to serialize fresh login attempts. + # @POST Returns stable asyncio.Lock instance. @classmethod def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock: existing_lock = cls._auth_locks.get(cache_key) @@ -107,17 +100,16 @@ class AsyncAPIClient: created_lock = asyncio.Lock() cls._auth_locks[cache_key] = created_lock return created_lock - # [/DEF:AsyncAPIClient._get_auth_lock:Function] + # #endregion AsyncAPIClient._get_auth_lock - # [DEF:AsyncAPIClient.authenticate:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Authenticate against Superset and cache access/csrf tokens. - # @POST: Client tokens are populated and reusable across requests. - # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints. - # @DATA_CONTRACT: None -> Output[Dict[str, str]] - # @RELATION: [CALLS] ->[SupersetAuthCache.get] - # @RELATION: [CALLS] ->[SupersetAuthCache.set] - # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock] + # #region AsyncAPIClient.authenticate [C:3] [TYPE Function] + # @BRIEF Authenticate against Superset and cache access/csrf tokens. + # @POST Client tokens are populated and reusable across requests. + # @SIDE_EFFECT Performs network requests to Superset authentication endpoints. + # @DATA_CONTRACT None -> Output[Dict[str, str]] + # @RELATION CALLS -> [SupersetAuthCache.get] + # @RELATION CALLS -> [SupersetAuthCache.set] + # @RELATION CALLS -> [AsyncAPIClient._get_auth_lock] async def authenticate(self) -> Dict[str, str]: cached_tokens = SupersetAuthCache.get(self._auth_cache_key) @@ -187,13 +179,12 @@ class AsyncAPIClient: except (httpx.HTTPError, KeyError) as exc: SupersetAuthCache.invalidate(self._auth_cache_key) raise NetworkError(f"Network or parsing error during authentication: {exc}") from exc - # [/DEF:AsyncAPIClient.authenticate:Function] + # #endregion AsyncAPIClient.authenticate - # [DEF:AsyncAPIClient.get_headers:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Return authenticated Superset headers for async requests. - # @POST: Headers include Authorization and CSRF tokens. - # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate] + # #region AsyncAPIClient.get_headers [C:3] [TYPE Function] + # @BRIEF Return authenticated Superset headers for async requests. + # @POST Headers include Authorization and CSRF tokens. + # @RELATION CALLS -> [AsyncAPIClient.authenticate] async def get_headers(self) -> Dict[str, str]: if not self._authenticated: await self.authenticate() @@ -203,16 +194,15 @@ class AsyncAPIClient: "Referer": self.base_url, "Content-Type": "application/json", } - # [/DEF:AsyncAPIClient.get_headers:Function] + # #endregion AsyncAPIClient.get_headers - # [DEF:AsyncAPIClient.request:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Perform one authenticated async Superset API request. - # @POST: Returns JSON payload or raw httpx.Response when raw_response=true. - # @SIDE_EFFECT: Performs network I/O. - # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers] - # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error] - # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error] + # #region AsyncAPIClient.request [C:3] [TYPE Function] + # @BRIEF Perform one authenticated async Superset API request. + # @POST Returns JSON payload or raw httpx.Response when raw_response=true. + # @SIDE_EFFECT Performs network I/O. + # @RELATION CALLS -> [AsyncAPIClient.get_headers] + # @RELATION CALLS -> [AsyncAPIClient._handle_http_error] + # @RELATION CALLS -> [AsyncAPIClient._handle_network_error] async def request( self, method: str, @@ -240,19 +230,18 @@ class AsyncAPIClient: self._handle_http_error(exc, endpoint) except httpx.HTTPError as exc: self._handle_network_error(exc, full_url) - # [/DEF:AsyncAPIClient.request:Function] + # #endregion AsyncAPIClient.request - # [DEF:AsyncAPIClient._handle_http_error:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions. - # @POST: Raises domain-specific exception for caller flow control. - # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception - # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint] - # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError] - # @RELATION: [DEPENDS_ON] ->[SupersetAPIError] - # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError] - # @RELATION: [DEPENDS_ON] ->[AuthenticationError] - # @RELATION: [DEPENDS_ON] ->[NetworkError] + # #region AsyncAPIClient._handle_http_error [C:3] [TYPE Function] + # @BRIEF Translate upstream HTTP errors into stable domain exceptions. + # @POST Raises domain-specific exception for caller flow control. + # @DATA_CONTRACT Input[httpx.HTTPStatusError] -> Exception + # @RELATION CALLS -> [AsyncAPIClient._is_dashboard_endpoint] + # @RELATION DEPENDS_ON -> [DashboardNotFoundError] + # @RELATION DEPENDS_ON -> [SupersetAPIError] + # @RELATION DEPENDS_ON -> [PermissionDeniedError] + # @RELATION DEPENDS_ON -> [AuthenticationError] + # @RELATION DEPENDS_ON -> [NetworkError] def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None: with belief_scope("AsyncAPIClient._handle_http_error"): status_code = exc.response.status_code @@ -272,12 +261,11 @@ class AsyncAPIClient: if status_code == 401: raise AuthenticationError() from exc raise SupersetAPIError(f"API Error {status_code}: {exc.response.text}") from exc - # [/DEF:AsyncAPIClient._handle_http_error:Function] + # #endregion AsyncAPIClient._handle_http_error - # [DEF:AsyncAPIClient._is_dashboard_endpoint:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation. - # @POST: Returns true only for dashboard-specific endpoints. + # #region AsyncAPIClient._is_dashboard_endpoint [C:2] [TYPE Function] + # @BRIEF Determine whether an API endpoint represents a dashboard resource for 404 translation. + # @POST Returns true only for dashboard-specific endpoints. def _is_dashboard_endpoint(self, endpoint: str) -> bool: normalized_endpoint = str(endpoint or "").strip().lower() if not normalized_endpoint: @@ -290,14 +278,13 @@ class AsyncAPIClient: if normalized_endpoint.startswith("/api/v1/"): normalized_endpoint = normalized_endpoint[len("/api/v1"):] return normalized_endpoint.startswith("/dashboard/") or normalized_endpoint == "/dashboard" - # [/DEF:AsyncAPIClient._is_dashboard_endpoint:Function] + # #endregion AsyncAPIClient._is_dashboard_endpoint - # [DEF:AsyncAPIClient._handle_network_error:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Translate generic httpx errors into NetworkError. - # @POST: Raises NetworkError with URL context. - # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError - # @RELATION: [DEPENDS_ON] ->[NetworkError] + # #region AsyncAPIClient._handle_network_error [C:3] [TYPE Function] + # @BRIEF Translate generic httpx errors into NetworkError. + # @POST Raises NetworkError with URL context. + # @DATA_CONTRACT Input[httpx.HTTPError] -> NetworkError + # @RELATION DEPENDS_ON -> [NetworkError] def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None: with belief_scope("AsyncAPIClient._handle_network_error"): if isinstance(exc, httpx.TimeoutException): @@ -307,17 +294,14 @@ class AsyncAPIClient: else: message = f"Unknown network error: {exc}" raise NetworkError(message, url=url) from exc - # [/DEF:AsyncAPIClient._handle_network_error:Function] + # #endregion AsyncAPIClient._handle_network_error - # [DEF:AsyncAPIClient.aclose:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Close underlying httpx client. - # @POST: Client resources are released. - # @SIDE_EFFECT: Closes network connections. - # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__] + # #region AsyncAPIClient.aclose [C:3] [TYPE Function] + # @BRIEF Close underlying httpx client. + # @POST Client resources are released. + # @SIDE_EFFECT Closes network connections. + # @RELATION DEPENDS_ON -> [AsyncAPIClient.__init__] async def aclose(self) -> None: await self._client.aclose() - # [/DEF:AsyncAPIClient.aclose:Function] -# [/DEF:AsyncAPIClient:Class] - -# [/DEF:AsyncNetworkModule:Module] + # #endregion AsyncAPIClient.aclose +# #endregion AsyncAPIClient diff --git a/backend/src/core/utils/dataset_mapper.py b/backend/src/core/utils/dataset_mapper.py index 04801c26..56d456ee 100644 --- a/backend/src/core/utils/dataset_mapper.py +++ b/backend/src/core/utils/dataset_mapper.py @@ -1,11 +1,10 @@ -# [DEF:DatasetMapperModule:Module] +# #region DatasetMapperModule [TYPE Module] [SEMANTICS dataset, mapping, postgresql, xlsx, superset] +# @BRIEF Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [backend.core.superset_client] +# @RELATION DEPENDS_ON -> [pandas] +# @RELATION DEPENDS_ON -> [psycopg2] # -# @SEMANTICS: dataset, mapping, postgresql, xlsx, superset -# @PURPOSE: Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> backend.core.superset_client -# @RELATION: DEPENDS_ON -> pandas -# @RELATION: DEPENDS_ON -> psycopg2 # @PUBLIC_API: DatasetMapper # [SECTION: IMPORTS] @@ -18,21 +17,21 @@ from ..cot_logger import MarkerLogger log = MarkerLogger("DatasetMapper") -# [DEF:DatasetMapper:Class] -# @PURPOSE: Класс для меппинга и обновления verbose_map в датасетах Superset. +# #region DatasetMapper [TYPE Class] +# @BRIEF Класс для меппинга и обновления verbose_map в датасетах Superset. class DatasetMapper: - # [DEF:__init__:Function] - # @PURPOSE: Initializes the mapper. - # @POST: Объект DatasetMapper инициализирован. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the mapper. + # @POST Объект DatasetMapper инициализирован. def __init__(self): pass - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:get_postgres_comments:Function] - # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL. - # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname). - # @PRE: table_name и table_schema должны быть строками. - # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД. + # #region get_postgres_comments [TYPE Function] + # @BRIEF Извлекает комментарии к колонкам из системного каталога PostgreSQL. + # @PRE db_config должен содержать валидные параметры подключения (host, port, user, password, dbname). + # @PRE table_name и table_schema должны быть строками. + # @POST Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД. # @THROW: Exception - При ошибках подключения или выполнения запроса к БД. # @PARAM: db_config (Dict) - Конфигурация для подключения к БД. # @PARAM: table_name (str) - Имя таблицы. @@ -91,12 +90,12 @@ class DatasetMapper: log.explore("Failed to fetch PostgreSQL comments", payload={"table_schema": table_schema, "table_name": table_name}, error=str(e)) raise return comments - # [/DEF:get_postgres_comments:Function] + # #endregion get_postgres_comments - # [DEF:load_excel_mappings:Function] - # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла. - # @PRE: file_path должен указывать на существующий XLSX файл. - # @POST: Возвращается словарь с меппингами из файла. + # #region load_excel_mappings [TYPE Function] + # @BRIEF Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла. + # @PRE file_path должен указывать на существующий XLSX файл. + # @POST Возвращается словарь с меппингами из файла. # @THROW: Exception - При ошибках чтения файла или парсинга. # @PARAM: file_path (str) - Путь к XLSX файлу. # @RETURN: Dict[str, str] - Словарь с меппингами. @@ -111,17 +110,17 @@ class DatasetMapper: except Exception as e: log.explore("Failed to load Excel mappings", payload={"file_path": file_path}, error=str(e)) raise - # [/DEF:load_excel_mappings:Function] + # #endregion load_excel_mappings - # [DEF:run_mapping:Function] - # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset. - # @PRE: superset_client должен быть авторизован. - # @PRE: dataset_id должен быть существующим ID в Superset. - # @POST: Если найдены изменения, датасет в Superset обновлен через API. - # @RELATION: CALLS -> self.get_postgres_comments - # @RELATION: CALLS -> self.load_excel_mappings - # @RELATION: CALLS -> superset_client.get_dataset - # @RELATION: CALLS -> superset_client.update_dataset + # #region run_mapping [TYPE Function] + # @BRIEF Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset. + # @PRE superset_client должен быть авторизован. + # @PRE dataset_id должен быть существующим ID в Superset. + # @POST Если найдены изменения, датасет в Superset обновлен через API. + # @RELATION CALLS -> [self.get_postgres_comments] + # @RELATION CALLS -> [self.load_excel_mappings] + # @RELATION CALLS -> [superset_client.get_dataset] + # @RELATION CALLS -> [superset_client.update_dataset] # @PARAM: superset_client (Any) - Клиент Superset. # @PARAM: dataset_id (int) - ID датасета для обновления. # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both'). @@ -236,7 +235,7 @@ class DatasetMapper: except (FileNotFoundError, Exception) as e: log.explore("Dataset mapping failed", payload={"dataset_id": dataset_id, "source": source}, error=str(e)) return - # [/DEF:run_mapping:Function] -# [/DEF:DatasetMapper:Class] + # #endregion run_mapping +# #endregion DatasetMapper -# [/DEF:DatasetMapperModule:Module] \ No newline at end of file +# #endregion DatasetMapperModule diff --git a/backend/src/core/utils/fileio.py b/backend/src/core/utils/fileio.py index 8dd5f98e..0f12d87c 100644 --- a/backend/src/core/utils/fileio.py +++ b/backend/src/core/utils/fileio.py @@ -1,4 +1,4 @@ -# [DEF:FileIO:Module] +# #region FileIO [TYPE Module] # # @TIER: STANDARD # @SEMANTICS: file, io, zip, yaml, temp, archive, utility @@ -25,16 +25,16 @@ from ..cot_logger import MarkerLogger log = MarkerLogger("FileIO") -# [DEF:InvalidZipFormatError:Class] -# @PURPOSE: Exception raised when a file is not a valid ZIP archive. +# #region InvalidZipFormatError [TYPE Class] +# @BRIEF Exception raised when a file is not a valid ZIP archive. class InvalidZipFormatError(Exception): pass -# [/DEF:InvalidZipFormatError:Class] +# #endregion InvalidZipFormatError -# [DEF:create_temp_file:Function] -# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением. -# @PRE: suffix должен быть строкой, определяющей тип ресурса. -# @POST: Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста. +# #region create_temp_file [TYPE Function] +# @BRIEF Контекстный менеджер для создания временного файла или директории с гарантированным удалением. +# @PRE suffix должен быть строкой, определяющей тип ресурса. +# @POST Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста. # @PARAM: content (Optional[bytes]) - Бинарное содержимое для записи во временный файл. # @PARAM: suffix (str) - Суффикс ресурса. Если `.dir`, создается директория. # @PARAM: mode (str) - Режим записи в файл (e.g., 'wb'). @@ -70,12 +70,12 @@ def create_temp_file(content: Optional[bytes] = None, suffix: str = ".zip", mode log.reflect("Cleaned up temporary file", payload={"path": str(resource_path)}) except OSError as e: log.explore("Error during temp resource cleanup", payload={"path": str(resource_path)}, error=str(e)) -# [/DEF:create_temp_file:Function] +# #endregion create_temp_file -# [DEF:remove_empty_directories:Function] -# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути. -# @PRE: root_dir должен быть путем к существующей директории. -# @POST: Все пустые поддиректории удалены, возвращено их количество. +# #region remove_empty_directories [TYPE Function] +# @BRIEF Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути. +# @PRE root_dir должен быть путем к существующей директории. +# @POST Все пустые поддиректории удалены, возвращено их количество. # @PARAM: root_dir (str) - Путь к корневой директории для очистки. # @RETURN: int - Количество удаленных директорий. def remove_empty_directories(root_dir: str) -> int: @@ -94,12 +94,12 @@ def remove_empty_directories(root_dir: str) -> int: log.explore("Failed to remove empty directory", payload={"directory": current_dir}, error=str(e)) log.reflect("Empty directory cleanup complete", payload={"removed_count": removed_count}) return removed_count -# [/DEF:remove_empty_directories:Function] +# #endregion remove_empty_directories -# [DEF:read_dashboard_from_disk:Function] -# @PURPOSE: Читает бинарное содержимое файла с диска. -# @PRE: file_path должен указывать на существующий файл. -# @POST: Возвращает байты содержимого и имя файла. +# #region read_dashboard_from_disk [TYPE Function] +# @BRIEF Читает бинарное содержимое файла с диска. +# @PRE file_path должен указывать на существующий файл. +# @POST Возвращает байты содержимого и имя файла. # @PARAM: file_path (str) - Путь к файлу. # @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла). # @THROW: FileNotFoundError - Если файл не найден. @@ -112,12 +112,12 @@ def read_dashboard_from_disk(file_path: str) -> Tuple[bytes, str]: if not content: log.explore("Dashboard file is empty", payload={"file_path": file_path}, error="Empty dashboard file") return content, path.name -# [/DEF:read_dashboard_from_disk:Function] +# #endregion read_dashboard_from_disk -# [DEF:calculate_crc32:Function] -# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла. -# @PRE: file_path должен быть объектом Path к существующему файлу. -# @POST: Возвращает 8-значную hex-строку CRC32. +# #region calculate_crc32 [TYPE Function] +# @BRIEF Вычисляет контрольную сумму CRC32 для файла. +# @PRE file_path должен быть объектом Path к существующему файлу. +# @POST Возвращает 8-значную hex-строку CRC32. # @PARAM: file_path (Path) - Путь к файлу. # @RETURN: str - 8-значное шестнадцатеричное представление CRC32. # @THROW: IOError - При ошибках чтения файла. @@ -126,25 +126,25 @@ def calculate_crc32(file_path: Path) -> str: with open(file_path, 'rb') as f: crc32_value = zlib.crc32(f.read()) return f"{crc32_value:08x}" -# [/DEF:calculate_crc32:Function] +# #endregion calculate_crc32 # [SECTION: DATA_CLASSES] -# [DEF:RetentionPolicy:DataClass] -# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные). +# #region RetentionPolicy [TYPE DataClass] +# @BRIEF Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные). @dataclass class RetentionPolicy: daily: int = 7 weekly: int = 4 monthly: int = 12 -# [/DEF:RetentionPolicy:DataClass] +# #endregion RetentionPolicy # [/SECTION] -# [DEF:archive_exports:Function] -# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию. -# @PRE: output_dir должен быть путем к существующей директории. -# @POST: Старые или дублирующиеся архивы удалены согласно политике. -# @RELATION: CALLS -> apply_retention_policy -# @RELATION: CALLS -> calculate_crc32 +# #region archive_exports [TYPE Function] +# @BRIEF Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию. +# @PRE output_dir должен быть путем к существующей директории. +# @POST Старые или дублирующиеся архивы удалены согласно политике. +# @RELATION CALLS -> [apply_retention_policy] +# @RELATION CALLS -> [calculate_crc32] # @PARAM: output_dir (str) - Директория с архивами. # @PARAM: policy (RetentionPolicy) - Политика хранения. # @PARAM: deduplicate (bool) - Флаг для включения удаления дубликатов по CRC32. @@ -220,12 +220,12 @@ def archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool log.reason("Removed archive by retention policy", payload={"file": file_path.name}) except OSError as e: log.explore("Failed to remove archive by retention policy", payload={"file": str(file_path)}, error=str(e)) -# [/DEF:archive_exports:Function] +# #endregion archive_exports -# [DEF:apply_retention_policy:Function] -# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить. -# @PRE: files_with_dates is a list of (Path, date) tuples. -# @POST: Returns a set of files to keep. +# #region apply_retention_policy [TYPE Function] +# @BRIEF (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить. +# @PRE files_with_dates is a list of (Path, date) tuples. +# @POST Returns a set of files to keep. # @PARAM: files_with_dates (List[Tuple[Path, date]]) - Список файлов с датами. # @PARAM: policy (RetentionPolicy) - Политика хранения. # @RETURN: set - Множество путей к файлам, которые должны быть сохранены. @@ -255,12 +255,12 @@ def apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: Re files_to_keep.update(monthly_files[:policy.monthly]) log.reflect("Retention policy applied", payload={"files_to_keep": len(files_to_keep)}) return files_to_keep -# [/DEF:apply_retention_policy:Function] +# #endregion apply_retention_policy -# [DEF:save_and_unpack_dashboard:Function] -# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его. -# @PRE: zip_content должен быть байтами валидного ZIP-архива. -# @POST: ZIP-файл сохранен, и если unpack=True, он распакован в output_dir. +# #region save_and_unpack_dashboard [TYPE Function] +# @BRIEF Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его. +# @PRE zip_content должен быть байтами валидного ZIP-архива. +# @POST ZIP-файл сохранен, и если unpack=True, он распакован в output_dir. # @PARAM: zip_content (bytes) - Содержимое ZIP-архива. # @PARAM: output_dir (Union[str, Path]) - Директория для сохранения. # @PARAM: unpack (bool) - Флаг, нужно ли распаковывать архив. @@ -286,13 +286,13 @@ def save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path], except zipfile.BadZipFile as e: log.explore("Invalid ZIP archive format", payload={"output_dir": str(output_dir)}, error=str(e)) raise InvalidZipFormatError(f"Invalid ZIP file: {e}") from e -# [/DEF:save_and_unpack_dashboard:Function] +# #endregion save_and_unpack_dashboard -# [DEF:update_yamls:Function] -# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex. -# @PRE: path должен быть существующей директорией. -# @POST: Все YAML файлы в директории обновлены согласно переданным параметрам. -# @RELATION: CALLS -> _update_yaml_file +# #region update_yamls [TYPE Function] +# @BRIEF Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex. +# @PRE path должен быть существующей директорией. +# @POST Все YAML файлы в директории обновлены согласно переданным параметрам. +# @RELATION CALLS -> [_update_yaml_file] # @THROW: FileNotFoundError - Если `path` не существует. # @PARAM: db_configs (Optional[List[Dict]]) - Список конфигураций для замены. # @PARAM: path (str) - Путь к директории с YAML файлами. @@ -308,12 +308,12 @@ def update_yamls(db_configs: Optional[List[Dict[str, Any]]] = None, path: str = for file_path in dir_path.rglob("*.yaml"): _update_yaml_file(file_path, configs, regexp_pattern, replace_string) -# [/DEF:update_yamls:Function] +# #endregion update_yamls -# [DEF:_update_yaml_file:Function] -# @PURPOSE: (Helper) Обновляет один YAML файл. -# @PRE: file_path должен быть объектом Path к существующему YAML файлу. -# @POST: Файл обновлен согласно переданным конфигурациям или регулярному выражению. +# #region _update_yaml_file [TYPE Function] +# @BRIEF (Helper) Обновляет один YAML файл. +# @PRE file_path должен быть объектом Path к существующему YAML файлу. +# @POST Файл обновлен согласно переданным конфигурациям или регулярному выражению. # @PARAM: file_path (Path) - Путь к файлу. # @PARAM: db_configs (List[Dict]) - Конфигурации. # @PARAM: regexp_pattern (Optional[str]) - Паттерн. @@ -357,16 +357,16 @@ def _update_yaml_file(file_path: Path, db_configs: List[Dict[str, Any]], regexp_ # Группы: 1=ключ+разделитель, 2=открывающая кавычка (опц), 3=значение, 4=закрывающая кавычка (опц) pattern = rf'({key_pattern}\s*:\s*)(["\']?)({val_pattern})(["\']?)' - # [DEF:replacer:Function] - # @PURPOSE: Функция замены, сохраняющая кавычки если они были. - # @PRE: match должен быть объектом совпадения регулярного выражения. - # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки. + # #region replacer [TYPE Function] + # @BRIEF Функция замены, сохраняющая кавычки если они были. + # @PRE match должен быть объектом совпадения регулярного выражения. + # @POST Возвращает строку с новым значением, сохраняя префикс и кавычки. def replacer(match): prefix = match.group(1) quote_open = match.group(2) quote_close = match.group(4) return f"{prefix}{quote_open}{new_val}{quote_close}" - # [/DEF:replacer:Function] + # #endregion replacer modified_content = re.sub(pattern, replacer, modified_content) log.reason("Replaced YAML config value", payload={"key": key, "file_path": str(file_path)}) @@ -375,12 +375,12 @@ def _update_yaml_file(file_path: Path, db_configs: List[Dict[str, Any]], regexp_ f.write(modified_content) except Exception as e: log.explore("Error performing raw replacement in YAML", payload={"file_path": str(file_path)}, error=str(e)) -# [/DEF:_update_yaml_file:Function] +# #endregion _update_yaml_file -# [DEF:create_dashboard_export:Function] -# @PURPOSE: Создает ZIP-архив из указанных исходных путей. -# @PRE: source_paths должен содержать существующие пути. -# @POST: ZIP-архив создан по пути zip_path. +# #region create_dashboard_export [TYPE Function] +# @BRIEF Создает ZIP-архив из указанных исходных путей. +# @PRE source_paths должен содержать существующие пути. +# @POST ZIP-архив создан по пути zip_path. # @PARAM: zip_path (Union[str, Path]) - Путь для сохранения ZIP архива. # @PARAM: source_paths (List[Union[str, Path]]) - Список исходных путей для архивации. # @PARAM: exclude_extensions (Optional[List[str]]) - Список расширений для исключения. @@ -403,23 +403,23 @@ def create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union except (IOError, zipfile.BadZipFile, AssertionError) as e: log.explore("Dashboard archive creation failed", payload={"zip_path": str(zip_path)}, error=str(e)) return False -# [/DEF:create_dashboard_export:Function] +# #endregion create_dashboard_export -# [DEF:sanitize_filename:Function] -# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов. -# @PRE: filename должен быть строкой. -# @POST: Возвращает строку без спецсимволов. +# #region sanitize_filename [TYPE Function] +# @BRIEF Очищает строку от символов, недопустимых в именах файлов. +# @PRE filename должен быть строкой. +# @POST Возвращает строку без спецсимволов. # @PARAM: filename (str) - Исходное имя файла. # @RETURN: str - Очищенная строка. def sanitize_filename(filename: str) -> str: with belief_scope(f"Sanitize filename: {filename}"): return re.sub(r'[\\/*?:"<>|]', "_", filename).strip() -# [/DEF:sanitize_filename:Function] +# #endregion sanitize_filename -# [DEF:get_filename_from_headers:Function] -# @PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'. -# @PRE: headers должен быть словарем заголовков. -# @POST: Возвращает имя файла или None, если заголовок отсутствует. +# #region get_filename_from_headers [TYPE Function] +# @BRIEF Извлекает имя файла из HTTP заголовка 'Content-Disposition'. +# @PRE headers должен быть словарем заголовков. +# @POST Возвращает имя файла или None, если заголовок отсутствует. # @PARAM: headers (dict) - Словарь HTTP заголовков. # @RETURN: Optional[str] - Имя файла or `None`. def get_filename_from_headers(headers: dict) -> Optional[str]: @@ -428,12 +428,12 @@ def get_filename_from_headers(headers: dict) -> Optional[str]: if match := re.search(r'filename="?([^"]+)"?', content_disposition): return match.group(1).strip() return None -# [/DEF:get_filename_from_headers:Function] +# #endregion get_filename_from_headers -# [DEF:consolidate_archive_folders:Function] -# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени. -# @PRE: root_directory должен быть объектом Path к существующей директории. -# @POST: Директории с одинаковым префиксом объединены в одну. +# #region consolidate_archive_folders [TYPE Function] +# @BRIEF Консолидирует директории архивов на основе общего слага в имени. +# @PRE root_directory должен быть объектом Path к существующей директории. +# @POST Директории с одинаковым префиксом объединены в одну. # @THROW: TypeError, ValueError - Если `root_directory` невалиден. # @PARAM: root_directory (Path) - Корневая директория для консолидации. def consolidate_archive_folders(root_directory: Path) -> None: @@ -484,6 +484,6 @@ def consolidate_archive_folders(root_directory: Path) -> None: log.reason("Removed source directory after consolidation", payload={"directory": str(source_dir)}) except Exception as e: log.explore("Failed to remove source directory after consolidation", payload={"directory": str(source_dir)}, error=str(e)) -# [/DEF:consolidate_archive_folders:Function] +# #endregion consolidate_archive_folders -# [/DEF:FileIO:Module] \ No newline at end of file +# #endregion FileIO diff --git a/backend/src/core/utils/matching.py b/backend/src/core/utils/matching.py index c4224a08..0cb17273 100644 --- a/backend/src/core/utils/matching.py +++ b/backend/src/core/utils/matching.py @@ -1,21 +1,20 @@ -# [DEF:FuzzyMatching:Module] +# #region FuzzyMatching [TYPE Module] [SEMANTICS fuzzy, matching, rapidfuzz, database, mapping] +# @BRIEF Provides utility functions for fuzzy matching database names. +# @LAYER Core +# @INVARIANT Confidence scores are returned as floats between 0.0 and 1.0. +# @RELATION DEPENDS_ON -> [rapidfuzz] # -# @SEMANTICS: fuzzy, matching, rapidfuzz, database, mapping -# @PURPOSE: Provides utility functions for fuzzy matching database names. -# @LAYER: Core -# @RELATION: DEPENDS_ON -> rapidfuzz # -# @INVARIANT: Confidence scores are returned as floats between 0.0 and 1.0. # [SECTION: IMPORTS] from rapidfuzz import fuzz, process from typing import List, Dict # [/SECTION] -# [DEF:suggest_mappings:Function] -# @PURPOSE: Suggests mappings between source and target databases using fuzzy matching. -# @PRE: source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'. -# @POST: Returns a list of suggested mappings with confidence scores. +# #region suggest_mappings [TYPE Function] +# @BRIEF Suggests mappings between source and target databases using fuzzy matching. +# @PRE source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'. +# @POST Returns a list of suggested mappings with confidence scores. # @PARAM: source_databases (List[Dict]) - Databases from the source environment. # @PARAM: target_databases (List[Dict]) - Databases from the target environment. # @PARAM: threshold (int) - Minimum confidence score (0-100). @@ -50,6 +49,6 @@ def suggest_mappings(source_databases: List[Dict], target_databases: List[Dict], }) return suggestions -# [/DEF:suggest_mappings:Function] +# #endregion suggest_mappings -# [/DEF:FuzzyMatching:Module] +# #endregion FuzzyMatching diff --git a/backend/src/core/utils/network.py b/backend/src/core/utils/network.py index 97d5154e..60f620f7 100644 --- a/backend/src/core/utils/network.py +++ b/backend/src/core/utils/network.py @@ -1,10 +1,8 @@ -# [DEF:NetworkModule:Module] +# #region NetworkModule [C:3] [TYPE Module] [SEMANTICS network, http, client, api, requests, session, authentication] +# @BRIEF Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [LoggerModule] # -# @COMPLEXITY: 3 -# @SEMANTICS: network, http, client, api, requests, session, authentication -# @PURPOSE: Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок. -# @LAYER: Infra -# @RELATION: [DEPENDS_ON] ->[LoggerModule] # @PUBLIC_API: APIClient # [SECTION: IMPORTS] @@ -24,82 +22,76 @@ from ..cot_logger import MarkerLogger log = MarkerLogger("Network") -# [DEF:SupersetAPIError:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Base exception for all Superset API related errors. +# #region SupersetAPIError [C:1] [TYPE Class] +# @BRIEF Base exception for all Superset API related errors. class SupersetAPIError(Exception): - # [DEF:__init__:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Initializes the exception with a message and context. - # @PRE: message is a string, context is a dict. - # @POST: Exception is initialized with context. + # #region __init__ [C:1] [TYPE Function] + # @BRIEF Initializes the exception with a message and context. + # @PRE message is a string, context is a dict. + # @POST Exception is initialized with context. def __init__(self, message: str = "Superset API error", **context: Any): with belief_scope("SupersetAPIError.__init__"): self.context = context super().__init__(f"[API_FAILURE] {message} | Context: {self.context}") - # [/DEF:__init__:Function] -# [/DEF:SupersetAPIError:Class] - -# [DEF:AuthenticationError:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Exception raised when authentication fails. + # #endregion __init__ +# #endregion SupersetAPIError +# #region AuthenticationError [C:1] [TYPE Class] +# @BRIEF Exception raised when authentication fails. class AuthenticationError(SupersetAPIError): - # [DEF:__init__:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Initializes the authentication error. - # @PRE: message is a string, context is a dict. - # @POST: AuthenticationError is initialized. + # #region __init__ [C:1] [TYPE Function] + # @BRIEF Initializes the authentication error. + # @PRE message is a string, context is a dict. + # @POST AuthenticationError is initialized. def __init__(self, message: str = "Authentication failed", **context: Any): with belief_scope("AuthenticationError.__init__"): super().__init__(message, type="authentication", **context) - # [/DEF:__init__:Function] -# [/DEF:AuthenticationError:Class] - -# [DEF:PermissionDeniedError:Class] -# @PURPOSE: Exception raised when access is denied. + # #endregion __init__ +# #endregion AuthenticationError +# #region PermissionDeniedError [TYPE Class] +# @BRIEF Exception raised when access is denied. class PermissionDeniedError(AuthenticationError): - # [DEF:__init__:Function] - # @PURPOSE: Initializes the permission denied error. - # @PRE: message is a string, context is a dict. - # @POST: PermissionDeniedError is initialized. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the permission denied error. + # @PRE message is a string, context is a dict. + # @POST PermissionDeniedError is initialized. def __init__(self, message: str = "Permission denied", **context: Any): with belief_scope("PermissionDeniedError.__init__"): super().__init__(message, **context) - # [/DEF:__init__:Function] -# [/DEF:PermissionDeniedError:Class] + # #endregion __init__ +# #endregion PermissionDeniedError -# [DEF:DashboardNotFoundError:Class] -# @PURPOSE: Exception raised when a dashboard cannot be found. +# #region DashboardNotFoundError [TYPE Class] +# @BRIEF Exception raised when a dashboard cannot be found. class DashboardNotFoundError(SupersetAPIError): - # [DEF:__init__:Function] - # @PURPOSE: Initializes the not found error with resource ID. - # @PRE: resource_id is provided. - # @POST: DashboardNotFoundError is initialized. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the not found error with resource ID. + # @PRE resource_id is provided. + # @POST DashboardNotFoundError is initialized. def __init__(self, resource_id: Union[int, str], message: str = "Dashboard not found", **context: Any): with belief_scope("DashboardNotFoundError.__init__"): super().__init__(f"Dashboard '{resource_id}' {message}", subtype="not_found", resource_id=resource_id, **context) - # [/DEF:__init__:Function] -# [/DEF:DashboardNotFoundError:Class] + # #endregion __init__ +# #endregion DashboardNotFoundError -# [DEF:NetworkError:Class] -# @PURPOSE: Exception raised when a network level error occurs. +# #region NetworkError [TYPE Class] +# @BRIEF Exception raised when a network level error occurs. class NetworkError(Exception): - # [DEF:NetworkError.__init__:Function] - # @PURPOSE: Initializes the network error. - # @PRE: message is a string. - # @POST: NetworkError is initialized. + # #region NetworkError.__init__ [TYPE Function] + # @BRIEF Initializes the network error. + # @PRE message is a string. + # @POST NetworkError is initialized. def __init__(self, message: str = "Network connection failed", **context: Any): with belief_scope("NetworkError.__init__"): self.context = context super().__init__(f"[NETWORK_FAILURE] {message} | Context: {self.context}") - # [/DEF:NetworkError.__init__:Function] -# [/DEF:NetworkError:Class] + # #endregion NetworkError.__init__ +# #endregion NetworkError -# [DEF:SupersetAuthCache:Class] -# @PURPOSE: Process-local cache for Superset access/csrf tokens keyed by environment credentials. -# @PRE: base_url and username are stable strings. -# @POST: Cached entries expire automatically by TTL and can be reused across requests. +# #region SupersetAuthCache [TYPE Class] +# @BRIEF Process-local cache for Superset access/csrf tokens keyed by environment credentials. +# @PRE base_url and username are stable strings. +# @POST Cached entries expire automatically by TTL and can be reused across requests. class SupersetAuthCache: TTL_SECONDS = 300 @@ -114,7 +106,7 @@ class SupersetAuthCache: return (str(base_url or "").strip(), username, bool(verify_ssl)) @classmethod - # [DEF:SupersetAuthCache.get:Function] + # #region SupersetAuthCache.get [TYPE Function] def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]: now = time.time() with cls._lock: @@ -133,10 +125,10 @@ class SupersetAuthCache: "access_token": str(tokens.get("access_token") or ""), "csrf_token": str(tokens.get("csrf_token") or ""), } - # [/DEF:SupersetAuthCache.get:Function] + # #endregion SupersetAuthCache.get @classmethod - # [DEF:SupersetAuthCache.set:Function] + # #region SupersetAuthCache.set [TYPE Function] def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None: normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1) with cls._lock: @@ -147,24 +139,23 @@ class SupersetAuthCache: }, "expires_at": time.time() + normalized_ttl, } - # [/DEF:SupersetAuthCache.set:Function] + # #endregion SupersetAuthCache.set @classmethod def invalidate(cls, key: Tuple[str, str, bool]) -> None: with cls._lock: cls._entries.pop(key, None) -# [/DEF:SupersetAuthCache:Class] +# #endregion SupersetAuthCache -# [DEF:APIClient:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Synchronous Superset API client with process-local auth token caching. -# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache] -# @RELATION: [DEPENDS_ON] ->[LoggerModule] +# #region APIClient [C:3] [TYPE Class] +# @BRIEF Synchronous Superset API client with process-local auth token caching. +# @RELATION DEPENDS_ON -> [SupersetAuthCache] +# @RELATION DEPENDS_ON -> [LoggerModule] class APIClient: DEFAULT_TIMEOUT = 30 - # [DEF:APIClient.__init__:Function] - # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером. + # #region APIClient.__init__ [TYPE Function] + # @BRIEF Инициализирует API клиент с конфигурацией, сессией и логгером. # @PARAM: config (Dict[str, Any]) - Конфигурация. # @PARAM: verify_ssl (bool) - Проверять ли SSL. # @PARAM: timeout (int) - Таймаут запросов. @@ -186,13 +177,13 @@ class APIClient: ) self._authenticated = False log.reflect("APIClient initialized") - # [/DEF:APIClient.__init__:Function] + # #endregion APIClient.__init__ - # [DEF:_init_session:Function] - # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой. - # @PRE: self.request_settings must be initialized. - # @POST: Returns a configured requests.Session instance. - # @RETURN: requests.Session - Настроенная сессия. + # #region _init_session [TYPE Function] + # @BRIEF Создает и настраивает `requests.Session` с retry-логикой. + # @PRE self.request_settings must be initialized. + # @POST Returns a configured requests.Session instance. + # @RETURN requests.Session - Настроенная сессия. def _init_session(self) -> requests.Session: with belief_scope("_init_session"): session = requests.Session() @@ -232,25 +223,25 @@ class APIClient: session.verify = True return session - # [/DEF:_init_session:Function] + # #endregion _init_session - # [DEF:_normalize_base_url:Function] - # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix. - # @PRE: raw_url can be empty. - # @POST: Returns canonical base URL suitable for building API endpoints. - # @RETURN: str + # #region _normalize_base_url [TYPE Function] + # @BRIEF Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix. + # @PRE raw_url can be empty. + # @POST Returns canonical base URL suitable for building API endpoints. + # @RETURN str def _normalize_base_url(self, raw_url: str) -> str: normalized = str(raw_url or "").strip().rstrip("/") if normalized.lower().endswith("/api/v1"): normalized = normalized[:-len("/api/v1")] return normalized.rstrip("/") - # [/DEF:_normalize_base_url:Function] + # #endregion _normalize_base_url - # [DEF:_build_api_url:Function] - # @PURPOSE: Build absolute Superset API URL for endpoint using canonical /api/v1 base. - # @PRE: endpoint is relative path or absolute URL. - # @POST: Returns full URL without accidental duplicate slashes. - # @RETURN: str + # #region _build_api_url [TYPE Function] + # @BRIEF Build absolute Superset API URL for endpoint using canonical /api/v1 base. + # @PRE endpoint is relative path or absolute URL. + # @POST Returns full URL without accidental duplicate slashes. + # @RETURN str def _build_api_url(self, endpoint: str) -> str: normalized_endpoint = str(endpoint or "").strip() if normalized_endpoint.startswith("http://") or normalized_endpoint.startswith("https://"): @@ -260,13 +251,13 @@ class APIClient: if normalized_endpoint.startswith("/api/v1/") or normalized_endpoint == "/api/v1": return f"{self.base_url}{normalized_endpoint}" return f"{self.api_base_url}{normalized_endpoint}" - # [/DEF:_build_api_url:Function] + # #endregion _build_api_url - # [DEF:APIClient.authenticate:Function] - # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены. - # @PRE: self.auth and self.base_url must be valid. - # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`. - # @RETURN: Dict[str, str] - Словарь с токенами. + # #region APIClient.authenticate [TYPE Function] + # @BRIEF Выполняет аутентификацию в Superset API и получает access и CSRF токены. + # @PRE self.auth and self.base_url must be valid. + # @POST `self._tokens` заполнен, `self._authenticated` установлен в `True`. + # @RETURN Dict[str, str] - Словарь с токенами. # @THROW: AuthenticationError, NetworkError - при ошибках. # @RELATION: [CALLS] ->[SupersetAuthCache.get] # @RELATION: [CALLS] ->[SupersetAuthCache.set] @@ -344,13 +335,13 @@ class APIClient: self._authenticated = True log.reflect("Authenticated successfully (from cached tokens)") return self._tokens - # [/DEF:APIClient.authenticate:Function] + # #endregion APIClient.authenticate @property - # [DEF:headers:Function] - # @PURPOSE: Возвращает HTTP-заголовки для аутентифицированных запросов. - # @PRE: APIClient is initialized and authenticated or can be authenticated. - # @POST: Returns headers including auth tokens. + # #region headers [TYPE Function] + # @BRIEF Возвращает HTTP-заголовки для аутентифицированных запросов. + # @PRE APIClient is initialized and authenticated or can be authenticated. + # @POST Returns headers including auth tokens. def headers(self) -> Dict[str, str]: if not self._authenticated: self.authenticate() @@ -360,10 +351,10 @@ class APIClient: "Referer": self.base_url, "Content-Type": "application/json" } - # [/DEF:headers:Function] + # #endregion headers - # [DEF:request:Function] - # @PURPOSE: Выполняет универсальный HTTP-запрос к API. + # #region request [TYPE Function] + # @BRIEF Выполняет универсальный HTTP-запрос к API. # @PARAM: method (str) - HTTP метод. # @PARAM: endpoint (str) - API эндпоинт. # @PARAM: headers (Optional[Dict]) - Дополнительные заголовки. @@ -390,10 +381,10 @@ class APIClient: self._handle_http_error(e, endpoint) except requests.exceptions.RequestException as e: self._handle_network_error(e, full_url) - # [/DEF:request:Function] + # #endregion request - # [DEF:_handle_http_error:Function] - # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения. + # #region _handle_http_error [TYPE Function] + # @BRIEF (Helper) Преобразует HTTP ошибки в кастомные исключения. # @PARAM: e (requests.exceptions.HTTPError) - Ошибка. # @PARAM: endpoint (str) - Эндпоинт. # @PRE: e must be a valid HTTPError with a response. @@ -417,12 +408,12 @@ class APIClient: if status_code == 401: raise AuthenticationError() from e raise SupersetAPIError(f"API Error {status_code}: {e.response.text}") from e - # [/DEF:_handle_http_error:Function] + # #endregion _handle_http_error - # [DEF:_is_dashboard_endpoint:Function] - # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation. - # @PRE: endpoint may be relative or absolute. - # @POST: Returns true only for dashboard-specific endpoints. + # #region _is_dashboard_endpoint [TYPE Function] + # @BRIEF Determine whether an API endpoint represents a dashboard resource for 404 translation. + # @PRE endpoint may be relative or absolute. + # @POST Returns true only for dashboard-specific endpoints. def _is_dashboard_endpoint(self, endpoint: str) -> bool: normalized_endpoint = str(endpoint or "").strip().lower() if not normalized_endpoint: @@ -435,10 +426,10 @@ class APIClient: if normalized_endpoint.startswith("/api/v1/"): normalized_endpoint = normalized_endpoint[len("/api/v1"):] return normalized_endpoint.startswith("/dashboard/") or normalized_endpoint == "/dashboard" - # [/DEF:_is_dashboard_endpoint:Function] + # #endregion _is_dashboard_endpoint - # [DEF:_handle_network_error:Function] - # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`. + # #region _handle_network_error [TYPE Function] + # @BRIEF (Helper) Преобразует сетевые ошибки в `NetworkError`. # @PARAM: e (requests.exceptions.RequestException) - Ошибка. # @PARAM: url (str) - URL. # @PRE: e must be a RequestException. @@ -452,10 +443,10 @@ class APIClient: else: msg = f"Unknown network error: {e}" raise NetworkError(msg, url=url) from e - # [/DEF:_handle_network_error:Function] + # #endregion _handle_network_error - # [DEF:upload_file:Function] - # @PURPOSE: Загружает файл на сервер через multipart/form-data. + # #region upload_file [TYPE Function] + # @BRIEF Загружает файл на сервер через multipart/form-data. # @PARAM: endpoint (str) - Эндпоинт. # @PARAM: file_info (Dict[str, Any]) - Информация о файле. # @PARAM: extra_data (Optional[Dict]) - Дополнительные данные. @@ -483,10 +474,10 @@ class APIClient: raise TypeError(f"Unsupported file_obj type: {type(file_obj)}") return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout) - # [/DEF:upload_file:Function] + # #endregion upload_file - # [DEF:_perform_upload:Function] - # @PURPOSE: (Helper) Выполняет POST запрос с файлом. + # #region _perform_upload [TYPE Function] + # @BRIEF (Helper) Выполняет POST запрос с файлом. # @PARAM: url (str) - URL. # @PARAM: files (Dict) - Файлы. # @PARAM: data (Optional[Dict]) - Данные. @@ -511,10 +502,10 @@ class APIClient: raise SupersetAPIError(f"API error during upload: {e.response.text}") from e except requests.exceptions.RequestException as e: raise NetworkError(f"Network error during upload: {e}", url=url) from e - # [/DEF:_perform_upload:Function] + # #endregion _perform_upload - # [DEF:fetch_paginated_count:Function] - # @PURPOSE: Получает общее количество элементов для пагинации. + # #region fetch_paginated_count [TYPE Function] + # @BRIEF Получает общее количество элементов для пагинации. # @PARAM: endpoint (str) - Эндпоинт. # @PARAM: query_params (Dict) - Параметры запроса. # @PARAM: count_field (str) - Поле с количеством. @@ -525,10 +516,10 @@ class APIClient: with belief_scope("fetch_paginated_count"): response_json = cast(Dict[str, Any], self.request("GET", endpoint, params={"q": json.dumps(query_params)})) return response_json.get(count_field, 0) - # [/DEF:fetch_paginated_count:Function] + # #endregion fetch_paginated_count - # [DEF:fetch_paginated_data:Function] - # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта. + # #region fetch_paginated_data [TYPE Function] + # @BRIEF Автоматически собирает данные со всех страниц пагинированного эндпоинта. # @PARAM: endpoint (str) - Эндпоинт. # @PARAM: pagination_options (Dict[str, Any]) - Опции пагинации. # @PRE: pagination_options must contain 'base_query', 'results_field'. 'total_count' is optional. @@ -566,8 +557,8 @@ class APIClient: results.extend(response_json.get(results_field, [])) return results - # [/DEF:fetch_paginated_data:Function] + # #endregion fetch_paginated_data -# [/DEF:APIClient:Class] +# #endregion APIClient -# [/DEF:NetworkModule:Module] +# #endregion NetworkModule diff --git a/backend/src/core/utils/superset_compilation_adapter.py b/backend/src/core/utils/superset_compilation_adapter.py index 5596a64c..725bead8 100644 --- a/backend/src/core/utils/superset_compilation_adapter.py +++ b/backend/src/core/utils/superset_compilation_adapter.py @@ -1,18 +1,16 @@ -# [DEF:SupersetCompilationAdapter:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: dataset_review, superset, compilation_preview, sql_lab_launch, execution_truth -# @PURPOSE: Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context. -# @LAYER: Infra -# @RELATION: [CALLS] ->[SupersetClient] -# @RELATION: [DEPENDS_ON] ->[CompiledPreview] -# @PRE: effective template params and dataset execution reference are available. -# @POST: preview and launch calls return Superset-originated artifacts or explicit errors. -# @SIDE_EFFECT: performs upstream Superset preview and SQL Lab calls. -# @INVARIANT: The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only. +# #region SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS dataset_review, superset, compilation_preview, sql_lab_launch, execution_truth] +# @BRIEF Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context. +# @LAYER Infra +# @PRE effective template params and dataset execution reference are available. +# @POST preview and launch calls return Superset-originated artifacts or explicit errors. +# @SIDE_EFFECT performs upstream Superset preview and SQL Lab calls. +# @INVARIANT The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only. +# @RELATION CALLS -> [SupersetClient] +# @RELATION DEPENDS_ON -> [CompiledPreview] from __future__ import annotations -# [DEF:SupersetCompilationAdapter.imports:Block] +# #region SupersetCompilationAdapter.imports [TYPE Block] from dataclasses import dataclass from datetime import datetime from typing import Any, Dict, List, Optional @@ -21,12 +19,11 @@ from src.core.config_models import Environment from src.core.logger import belief_scope, logger from src.core.superset_client import SupersetClient from src.models.dataset_review import CompiledPreview, PreviewStatus -# [/DEF:SupersetCompilationAdapter.imports:Block] +# #endregion SupersetCompilationAdapter.imports -# [DEF:PreviewCompilationPayload:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed preview payload for Superset-side compilation. +# #region PreviewCompilationPayload [C:2] [TYPE Class] +# @BRIEF Typed preview payload for Superset-side compilation. @dataclass(frozen=True) class PreviewCompilationPayload: session_id: str @@ -36,12 +33,11 @@ class PreviewCompilationPayload: effective_filters: List[Dict[str, Any]] -# [/DEF:PreviewCompilationPayload:Class] +# #endregion PreviewCompilationPayload -# [DEF:SqlLabLaunchPayload:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed SQL Lab payload for audited launch handoff. +# #region SqlLabLaunchPayload [C:2] [TYPE Class] +# @BRIEF Typed SQL Lab payload for audited launch handoff. @dataclass(frozen=True) class SqlLabLaunchPayload: session_id: str @@ -51,47 +47,43 @@ class SqlLabLaunchPayload: template_params: Dict[str, Any] -# [/DEF:SqlLabLaunchPayload:Class] +# #endregion SqlLabLaunchPayload -# [DEF:SupersetCompilationAdapter:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication. -# @RELATION: [CALLS] ->[SupersetClient] -# @PRE: environment is configured and Superset is reachable for the target session. -# @POST: adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references. -# @SIDE_EFFECT: issues network requests to Superset API surfaces. +# #region SupersetCompilationAdapter [C:4] [TYPE Class] +# @BRIEF Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication. +# @PRE environment is configured and Superset is reachable for the target session. +# @POST adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references. +# @SIDE_EFFECT issues network requests to Superset API surfaces. +# @RELATION CALLS -> [SupersetClient] class SupersetCompilationAdapter: - # [DEF:SupersetCompilationAdapter.__init__:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Bind adapter to one Superset environment and client instance. + # #region SupersetCompilationAdapter.__init__ [C:2] [TYPE Function] + # @BRIEF Bind adapter to one Superset environment and client instance. def __init__( self, environment: Environment, client: Optional[SupersetClient] = None ) -> None: self.environment = environment self.client = client or SupersetClient(environment) - # [/DEF:SupersetCompilationAdapter.__init__:Function] + # #endregion SupersetCompilationAdapter.__init__ - # [DEF:SupersetCompilationAdapter._supports_client_method:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods. + # #region SupersetCompilationAdapter._supports_client_method [C:2] [TYPE Function] + # @BRIEF Detect explicitly implemented client capabilities without treating loose mocks as real methods. def _supports_client_method(self, method_name: str) -> bool: client_dict = getattr(self.client, "__dict__", {}) if method_name in client_dict: return callable(client_dict[method_name]) return callable(getattr(type(self.client), method_name, None)) - # [/DEF:SupersetCompilationAdapter._supports_client_method:Function] + # #endregion SupersetCompilationAdapter._supports_client_method - # [DEF:SupersetCompilationAdapter.compile_preview:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs. - # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_superset_preview] - # @PRE: dataset_id and effective inputs are available for the current session. - # @POST: returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics. - # @SIDE_EFFECT: performs upstream preview requests. - # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[CompiledPreview] + # #region SupersetCompilationAdapter.compile_preview [C:4] [TYPE Function] + # @BRIEF Request Superset-side compiled SQL preview for the current effective inputs. + # @PRE dataset_id and effective inputs are available for the current session. + # @POST returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics. + # @SIDE_EFFECT performs upstream preview requests. + # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[CompiledPreview] + # @RELATION CALLS -> [SupersetCompilationAdapter._request_superset_preview] def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview: with belief_scope("SupersetCompilationAdapter.compile_preview"): if payload.dataset_id <= 0: @@ -177,27 +169,25 @@ class SupersetCompilationAdapter: ) return preview - # [/DEF:SupersetCompilationAdapter.compile_preview:Function] + # #endregion SupersetCompilationAdapter.compile_preview - # [DEF:SupersetCompilationAdapter.mark_preview_stale:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Invalidate previous preview after mapping or value changes. - # @PRE: preview is a persisted preview artifact or current in-memory snapshot. - # @POST: preview status becomes stale without fabricating a replacement artifact. + # #region SupersetCompilationAdapter.mark_preview_stale [C:2] [TYPE Function] + # @BRIEF Invalidate previous preview after mapping or value changes. + # @PRE preview is a persisted preview artifact or current in-memory snapshot. + # @POST preview status becomes stale without fabricating a replacement artifact. def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview: preview.preview_status = PreviewStatus.STALE return preview - # [/DEF:SupersetCompilationAdapter.mark_preview_stale:Function] + # #endregion SupersetCompilationAdapter.mark_preview_stale - # [DEF:SupersetCompilationAdapter.create_sql_lab_session:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Create the canonical audited execution session after all launch gates pass. - # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_sql_lab_session] - # @PRE: compiled_sql is Superset-originated and launch gates are already satisfied. - # @POST: returns one canonical SQL Lab session reference from Superset. - # @SIDE_EFFECT: performs upstream SQL Lab execution/session creation. - # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[str] + # #region SupersetCompilationAdapter.create_sql_lab_session [C:4] [TYPE Function] + # @BRIEF Create the canonical audited execution session after all launch gates pass. + # @PRE compiled_sql is Superset-originated and launch gates are already satisfied. + # @POST returns one canonical SQL Lab session reference from Superset. + # @SIDE_EFFECT performs upstream SQL Lab execution/session creation. + # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[str] + # @RELATION CALLS -> [SupersetCompilationAdapter._request_sql_lab_session] def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str: with belief_scope("SupersetCompilationAdapter.create_sql_lab_session"): compiled_sql = str(payload.compiled_sql or "").strip() @@ -249,16 +239,15 @@ class SupersetCompilationAdapter: ) return sql_lab_session_ref - # [/DEF:SupersetCompilationAdapter.create_sql_lab_session:Function] + # #endregion SupersetCompilationAdapter.create_sql_lab_session - # [DEF:SupersetCompilationAdapter._request_superset_preview:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only. - # @RELATION: [CALLS] ->[SupersetClient.compile_dataset_preview] - # @PRE: payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt. - # @POST: returns one normalized upstream compilation response including the chosen strategy metadata. - # @SIDE_EFFECT: issues one or more Superset preview requests through the client fallback chain. - # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[Dict[str,Any]] + # #region SupersetCompilationAdapter._request_superset_preview [C:4] [TYPE Function] + # @BRIEF Request preview compilation through explicit client support backed by real Superset endpoints only. + # @PRE payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt. + # @POST returns one normalized upstream compilation response including the chosen strategy metadata. + # @SIDE_EFFECT issues one or more Superset preview requests through the client fallback chain. + # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[Dict[str,Any]] + # @RELATION CALLS -> [SupersetClient.compile_dataset_preview] def _request_superset_preview( self, payload: PreviewCompilationPayload ) -> Dict[str, Any]: @@ -395,16 +384,15 @@ class SupersetCompilationAdapter: ) raise RuntimeError(str(exc)) from exc - # [/DEF:SupersetCompilationAdapter._request_superset_preview:Function] + # #endregion SupersetCompilationAdapter._request_superset_preview - # [DEF:SupersetCompilationAdapter._request_sql_lab_session:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response. - # @RELATION: [CALLS] ->[SupersetClient.get_dataset] - # @PRE: payload carries non-empty Superset-originated SQL and a preview identifier for the current launch. - # @POST: returns the first successful SQL Lab execution response from Superset. - # @SIDE_EFFECT: issues Superset dataset lookup and SQL Lab execution requests. - # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]] + # #region SupersetCompilationAdapter._request_sql_lab_session [C:4] [TYPE Function] + # @BRIEF Probe supported SQL Lab execution surfaces and return the first successful response. + # @PRE payload carries non-empty Superset-originated SQL and a preview identifier for the current launch. + # @POST returns the first successful SQL Lab execution response from Superset. + # @SIDE_EFFECT issues Superset dataset lookup and SQL Lab execution requests. + # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]] + # @RELATION CALLS -> [SupersetClient.get_dataset] def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> Dict[str, Any]: dataset_raw = self.client.get_dataset(payload.dataset_id) dataset_record = ( @@ -456,12 +444,11 @@ class SupersetCompilationAdapter: "; ".join(errors) or "No Superset SQL Lab surface accepted the request" ) - # [/DEF:SupersetCompilationAdapter._request_sql_lab_session:Function] + # #endregion SupersetCompilationAdapter._request_sql_lab_session - # [DEF:SupersetCompilationAdapter._normalize_preview_response:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure. - # @RELATION: [DEPENDS_ON] ->[CompiledPreview] + # #region SupersetCompilationAdapter._normalize_preview_response [C:3] [TYPE Function] + # @BRIEF Normalize candidate Superset preview responses into one compiled-sql structure. + # @RELATION DEPENDS_ON -> [CompiledPreview] def _normalize_preview_response(self, response: Any) -> Optional[Dict[str, Any]]: if not isinstance(response, dict): return None @@ -490,19 +477,16 @@ class SupersetCompilationAdapter: } return None - # [/DEF:SupersetCompilationAdapter._normalize_preview_response:Function] + # #endregion SupersetCompilationAdapter._normalize_preview_response - # [DEF:SupersetCompilationAdapter._dump_json:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Serialize Superset request payload deterministically for network transport. + # #region SupersetCompilationAdapter._dump_json [C:1] [TYPE Function] + # @BRIEF Serialize Superset request payload deterministically for network transport. def _dump_json(self, payload: Dict[str, Any]) -> str: import json return json.dumps(payload, sort_keys=True, default=str) - # [/DEF:SupersetCompilationAdapter._dump_json:Function] + # #endregion SupersetCompilationAdapter._dump_json -# [/DEF:SupersetCompilationAdapter:Class] - -# [/DEF:SupersetCompilationAdapter:Module] +# #endregion SupersetCompilationAdapter diff --git a/backend/src/core/utils/superset_context_extractor/__init__.py b/backend/src/core/utils/superset_context_extractor/__init__.py index 8a49a811..a356b648 100644 --- a/backend/src/core/utils/superset_context_extractor/__init__.py +++ b/backend/src/core/utils/superset_context_extractor/__init__.py @@ -1,22 +1,20 @@ -# [DEF:SupersetContextExtractorPackage:Module] -# @COMPLEXITY: 4 -# @LAYER: Infra -# @SEMANTICS: dataset_review, superset, link_parsing, context_recovery, partial_recovery -# @PURPOSE: Recover dataset and dashboard context from Superset links while preserving explicit partial-recovery markers. -# @RELATION: DEPENDS_ON -> [ImportedFilter] -# @RELATION: DEPENDS_ON -> [TemplateVariable] -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource. -# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary. -# @SIDE_EFFECT: Performs upstream Superset API reads. -# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context. +# #region SupersetContextExtractorPackage [C:4] [TYPE Module] [SEMANTICS dataset_review, superset, link_parsing, context_recovery, partial_recovery] +# @BRIEF Recover dataset and dashboard context from Superset links while preserving explicit partial-recovery markers. +# @LAYER Infra +# @PRE Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource. +# @POST Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary. +# @SIDE_EFFECT Performs upstream Superset API reads. +# @INVARIANT Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context. +# @RELATION DEPENDS_ON -> [ImportedFilter] +# @RELATION DEPENDS_ON -> [TemplateVariable] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RATIONALE Decomposed from monolithic superset_context_extractor.py (1397 lines) into # -# @RATIONALE: Decomposed from monolithic superset_context_extractor.py (1397 lines) into # domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class # preserves the original public API surface — all consumers continue to import # from `src.core.utils.superset_context_extractor` without changes. # @REJECTED: Keeping a single 1397-line file — violates fractal limit INV_7. -# [/DEF:SupersetContextExtractorPackage:Module] +# #endregion SupersetContextExtractorPackage from ._base import SupersetContextExtractorBase, SupersetParsedContext from ._parsing import SupersetContextParsingMixin @@ -26,18 +24,17 @@ from ._filters import SupersetContextFiltersExtractMixin from ._pii import mask_pii_value, sanitize_imported_filter_for_assistant, _mask_sensitive_text -# [DEF:SupersetContextExtractor:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake. -# @RELATION: DEPENDS_ON -> [Environment] -# @RELATION: INHERITS -> [SupersetContextExtractorBase] -# @RELATION: INHERITS -> [SupersetContextParsingMixin] -# @RELATION: INHERITS -> [SupersetContextRecoveryMixin] -# @RELATION: INHERITS -> [SupersetContextTemplatesMixin] -# @RELATION: INHERITS -> [SupersetContextFiltersExtractMixin] -# @PRE: constructor receives a configured environment with a usable Superset base URL. -# @POST: extractor instance is ready to parse links against one Superset environment. -# @SIDE_EFFECT: downstream parse operations may call Superset APIs through SupersetClient. +# #region SupersetContextExtractor [C:4] [TYPE Class] +# @BRIEF Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake. +# @PRE constructor receives a configured environment with a usable Superset base URL. +# @POST extractor instance is ready to parse links against one Superset environment. +# @SIDE_EFFECT downstream parse operations may call Superset APIs through SupersetClient. +# @RELATION DEPENDS_ON -> [Environment] +# @RELATION INHERITS -> [SupersetContextExtractorBase] +# @RELATION INHERITS -> [SupersetContextParsingMixin] +# @RELATION INHERITS -> [SupersetContextRecoveryMixin] +# @RELATION INHERITS -> [SupersetContextTemplatesMixin] +# @RELATION INHERITS -> [SupersetContextFiltersExtractMixin] class SupersetContextExtractor( SupersetContextFiltersExtractMixin, SupersetContextRecoveryMixin, @@ -54,7 +51,7 @@ class SupersetContextExtractor( pass -# [/DEF:SupersetContextExtractor:Class] +# #endregion SupersetContextExtractor __all__ = [ diff --git a/backend/src/core/utils/superset_context_extractor/_base.py b/backend/src/core/utils/superset_context_extractor/_base.py index 6664e009..ce824bf8 100644 --- a/backend/src/core/utils/superset_context_extractor/_base.py +++ b/backend/src/core/utils/superset_context_extractor/_base.py @@ -1,18 +1,16 @@ -# [DEF:SupersetContextExtractorBase:Module] -# @COMPLEXITY: 4 -# @LAYER: Infra -# @SEMANTICS: dataset_review, superset, link_parsing, context_recovery, partial_recovery -# @PURPOSE: Base class and helpers for recovering dataset/dashboard context from Superset links with explicit partial-recovery markers. -# @RELATION: DEPENDS_ON -> [ImportedFilter] -# @RELATION: DEPENDS_ON -> [TemplateVariable] -# @RELATION: CALLS -> [SupersetClient] -# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource. -# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary. -# @SIDE_EFFECT: Performs upstream Superset API reads. -# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context. -# [/DEF:SupersetContextExtractorBase:Module] +# #region SupersetContextExtractorBase [C:4] [TYPE Module] [SEMANTICS dataset_review, superset, link_parsing, context_recovery, partial_recovery] +# @BRIEF Base class and helpers for recovering dataset/dashboard context from Superset links with explicit partial-recovery markers. +# @LAYER Infra +# @PRE Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource. +# @POST Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary. +# @SIDE_EFFECT Performs upstream Superset API reads. +# @INVARIANT Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context. +# @RELATION DEPENDS_ON -> [ImportedFilter] +# @RELATION DEPENDS_ON -> [TemplateVariable] +# @RELATION CALLS -> [SupersetClient] +# #endregion SupersetContextExtractorBase -# [DEF:_base_imports:Block] +# #region _base_imports [TYPE Block] from __future__ import annotations import json @@ -25,14 +23,13 @@ from urllib.parse import parse_qs, unquote, urlparse from ...config_models import Environment from ...logger import belief_scope, logger from ...superset_client import SupersetClient -# [/DEF:_base_imports:Block] +# #endregion _base_imports logger = cast(Any, logger) -# [DEF:SupersetParsedContext:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Normalized output of Superset link parsing for session intake and recovery. +# #region SupersetParsedContext [C:2] [TYPE Class] +# @BRIEF Normalized output of Superset link parsing for session intake and recovery. @dataclass class SupersetParsedContext: source_url: str @@ -48,31 +45,28 @@ class SupersetParsedContext: dataset_payload: Optional[Dict[str, Any]] = None -# [/DEF:SupersetParsedContext:Class] +# #endregion SupersetParsedContext -# [DEF:SupersetContextExtractorBase:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Bind extractor to one Superset environment and client instance; provide shared URL-parsing helpers. -# @RELATION: DEPENDS_ON -> [Environment] -# @PRE: constructor receives a configured environment with a usable Superset base URL. -# @POST: extractor instance is ready to parse links against one Superset environment. -# @SIDE_EFFECT: downstream parse operations may call Superset APIs through SupersetClient. +# #region SupersetContextExtractorBase [C:4] [TYPE Class] +# @BRIEF Bind extractor to one Superset environment and client instance; provide shared URL-parsing helpers. +# @PRE constructor receives a configured environment with a usable Superset base URL. +# @POST extractor instance is ready to parse links against one Superset environment. +# @SIDE_EFFECT downstream parse operations may call Superset APIs through SupersetClient. +# @RELATION DEPENDS_ON -> [Environment] class SupersetContextExtractorBase: - # [DEF:SupersetContextExtractorBase.__init__:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Bind extractor to one Superset environment and client instance. + # #region SupersetContextExtractorBase.__init__ [C:2] [TYPE Function] + # @BRIEF Bind extractor to one Superset environment and client instance. def __init__( self, environment: Environment, client: Optional[SupersetClient] = None ) -> None: self.environment = environment self.client = client or SupersetClient(environment) - # [/DEF:SupersetContextExtractorBase.__init__:Function] + # #endregion SupersetContextExtractorBase.__init__ - # [DEF:SupersetContextExtractorBase.build_recovery_summary:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Summarize recovered, partial, and unresolved context for session state and UX. + # #region SupersetContextExtractorBase.build_recovery_summary [C:2] [TYPE Function] + # @BRIEF Summarize recovered, partial, and unresolved context for session state and UX. def build_recovery_summary( self, parsed_context: SupersetParsedContext ) -> Dict[str, Any]: @@ -86,11 +80,10 @@ class SupersetContextExtractorBase: "imported_filter_count": len(parsed_context.imported_filters), } - # [/DEF:SupersetContextExtractorBase.build_recovery_summary:Function] + # #endregion SupersetContextExtractorBase.build_recovery_summary - # [DEF:SupersetContextExtractorBase._extract_numeric_identifier:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a numeric identifier from a REST-like Superset URL path. + # #region SupersetContextExtractorBase._extract_numeric_identifier [C:2] [TYPE Function] + # @BRIEF Extract a numeric identifier from a REST-like Superset URL path. def _extract_numeric_identifier( self, path_parts: List[str], resource_name: str ) -> Optional[int]: @@ -109,11 +102,10 @@ class SupersetContextExtractorBase: return None return int(candidate) - # [/DEF:SupersetContextExtractorBase._extract_numeric_identifier:Function] + # #endregion SupersetContextExtractorBase._extract_numeric_identifier - # [DEF:SupersetContextExtractorBase._extract_dashboard_reference:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a dashboard id-or-slug reference from a Superset URL path. + # #region SupersetContextExtractorBase._extract_dashboard_reference [C:2] [TYPE Function] + # @BRIEF Extract a dashboard id-or-slug reference from a Superset URL path. def _extract_dashboard_reference(self, path_parts: List[str]) -> Optional[str]: if "dashboard" not in path_parts: return None @@ -130,11 +122,10 @@ class SupersetContextExtractorBase: return None return candidate - # [/DEF:SupersetContextExtractorBase._extract_dashboard_reference:Function] + # #endregion SupersetContextExtractorBase._extract_dashboard_reference - # [DEF:SupersetContextExtractorBase._extract_dashboard_permalink_key:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a dashboard permalink key from a Superset URL path. + # #region SupersetContextExtractorBase._extract_dashboard_permalink_key [C:2] [TYPE Function] + # @BRIEF Extract a dashboard permalink key from a Superset URL path. def _extract_dashboard_permalink_key(self, path_parts: List[str]) -> Optional[str]: if "dashboard" not in path_parts: return None @@ -152,34 +143,31 @@ class SupersetContextExtractorBase: return None return permalink_key - # [/DEF:SupersetContextExtractorBase._extract_dashboard_permalink_key:Function] + # #endregion SupersetContextExtractorBase._extract_dashboard_permalink_key - # [DEF:SupersetContextExtractorBase._extract_dashboard_id_from_state:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a dashboard identifier from returned permalink state when present. + # #region SupersetContextExtractorBase._extract_dashboard_id_from_state [C:2] [TYPE Function] + # @BRIEF Extract a dashboard identifier from returned permalink state when present. def _extract_dashboard_id_from_state(self, state: Dict[str, Any]) -> Optional[int]: return self._search_nested_numeric_key( payload=state, candidate_keys={"dashboardId", "dashboard_id", "dashboard_id_value"}, ) - # [/DEF:SupersetContextExtractorBase._extract_dashboard_id_from_state:Function] + # #endregion SupersetContextExtractorBase._extract_dashboard_id_from_state - # [DEF:SupersetContextExtractorBase._extract_chart_id_from_state:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a chart identifier from returned permalink state when dashboard id is absent. + # #region SupersetContextExtractorBase._extract_chart_id_from_state [C:2] [TYPE Function] + # @BRIEF Extract a chart identifier from returned permalink state when dashboard id is absent. def _extract_chart_id_from_state(self, state: Dict[str, Any]) -> Optional[int]: return self._search_nested_numeric_key( payload=state, candidate_keys={"slice_id", "sliceId", "chartId", "chart_id"}, ) - # [/DEF:SupersetContextExtractorBase._extract_chart_id_from_state:Function] + # #endregion SupersetContextExtractorBase._extract_chart_id_from_state - # [DEF:SupersetContextExtractorBase._search_nested_numeric_key:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Recursively search nested dict/list payloads for the first numeric value under a candidate key set. - # @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase] + # #region SupersetContextExtractorBase._search_nested_numeric_key [C:3] [TYPE Function] + # @BRIEF Recursively search nested dict/list payloads for the first numeric value under a candidate key set. + # @RELATION DEPENDS_ON -> [SupersetContextExtractorBase] def _search_nested_numeric_key( self, payload: Any, candidate_keys: Set[str] ) -> Optional[int]: @@ -201,11 +189,10 @@ class SupersetContextExtractorBase: return found return None - # [/DEF:SupersetContextExtractorBase._search_nested_numeric_key:Function] + # #endregion SupersetContextExtractorBase._search_nested_numeric_key - # [DEF:SupersetContextExtractorBase._decode_query_state:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Decode query-string structures used by Superset URL state transport. + # #region SupersetContextExtractorBase._decode_query_state [C:2] [TYPE Function] + # @BRIEF Decode query-string structures used by Superset URL state transport. def _decode_query_state(self, query_params: Dict[str, List[str]]) -> Dict[str, Any]: query_state: Dict[str, Any] = {} for key, values in query_params.items(): @@ -225,7 +212,7 @@ class SupersetContextExtractorBase: query_state[key] = decoded_value return query_state - # [/DEF:SupersetContextExtractorBase._decode_query_state:Function] + # #endregion SupersetContextExtractorBase._decode_query_state -# [/DEF:SupersetContextExtractorBase:Class] +# #endregion SupersetContextExtractorBase diff --git a/backend/src/core/utils/superset_context_extractor/_filters.py b/backend/src/core/utils/superset_context_extractor/_filters.py index 06070815..3b2c680e 100644 --- a/backend/src/core/utils/superset_context_extractor/_filters.py +++ b/backend/src/core/utils/superset_context_extractor/_filters.py @@ -1,12 +1,10 @@ -# [DEF:SupersetContextFiltersExtractMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, filters, extraction, query_state, dataMask, native_filters -# @PURPOSE: Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data). -# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase] -# [/DEF:SupersetContextFiltersExtractMixin:Module] +# #region SupersetContextFiltersExtractMixin [C:3] [TYPE Module] [SEMANTICS superset, filters, extraction, query_state, dataMask, native_filters] +# @BRIEF Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data). +# @LAYER Infra +# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase] +# #endregion SupersetContextFiltersExtractMixin -# [DEF:_filters_imports:Block] +# #region _filters_imports [TYPE Block] from __future__ import annotations from copy import deepcopy @@ -15,16 +13,14 @@ from typing import Any, Dict, List, cast from ...logger import logger as app_logger app_logger = cast(Any, app_logger) -# [/DEF:_filters_imports:Block] +# #endregion _filters_imports -# [DEF:SupersetContextFiltersExtractMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing query-state filter extraction for the composed SupersetContextExtractor. +# #region SupersetContextFiltersExtractMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing query-state filter extraction for the composed SupersetContextExtractor. class SupersetContextFiltersExtractMixin: - # [DEF:SupersetContextFiltersExtractMixin._extract_imported_filters:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize imported filters from decoded query state without fabricating missing values. + # #region SupersetContextFiltersExtractMixin._extract_imported_filters [C:2] [TYPE Function] + # @BRIEF Normalize imported filters from decoded query state without fabricating missing values. def _extract_imported_filters( self, query_state: Dict[str, Any] ) -> List[Dict[str, Any]]: @@ -255,7 +251,7 @@ class SupersetContextFiltersExtractMixin: return imported_filters - # [/DEF:SupersetContextFiltersExtractMixin._extract_imported_filters:Function] + # #endregion SupersetContextFiltersExtractMixin._extract_imported_filters -# [/DEF:SupersetContextFiltersExtractMixin:Class] +# #endregion SupersetContextFiltersExtractMixin diff --git a/backend/src/core/utils/superset_context_extractor/_parsing.py b/backend/src/core/utils/superset_context_extractor/_parsing.py index 28070035..cbff2f13 100644 --- a/backend/src/core/utils/superset_context_extractor/_parsing.py +++ b/backend/src/core/utils/superset_context_extractor/_parsing.py @@ -1,13 +1,11 @@ -# [DEF:SupersetContextParsingMixin:Module] -# @COMPLEXITY: 4 -# @LAYER: Infra -# @SEMANTICS: superset, link_parsing, url, permalink, recovery -# @PURPOSE: Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake. -# @RELATION: CALLS -> [SupersetClient] -# @RELATION: CALLS -> [SupersetContextExtractorBase] -# [/DEF:SupersetContextParsingMixin:Module] +# #region SupersetContextParsingMixin [C:4] [TYPE Module] [SEMANTICS superset, link_parsing, url, permalink, recovery] +# @BRIEF Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake. +# @LAYER Infra +# @RELATION CALLS -> [SupersetClient] +# @RELATION CALLS -> [SupersetContextExtractorBase] +# #endregion SupersetContextParsingMixin -# [DEF:_parsing_imports:Block] +# #region _parsing_imports [TYPE Block] from __future__ import annotations from typing import Any, Dict, List, Optional, cast @@ -17,21 +15,19 @@ from ...logger import belief_scope, logger from ._base import SupersetParsedContext logger = cast(Any, logger) -# [/DEF:_parsing_imports:Block] +# #endregion _parsing_imports -# [DEF:SupersetContextParsingMixin:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Mixin providing Superset URL parsing logic for the composed SupersetContextExtractor. +# #region SupersetContextParsingMixin [C:4] [TYPE Class] +# @BRIEF Mixin providing Superset URL parsing logic for the composed SupersetContextExtractor. class SupersetContextParsingMixin: - # [DEF:SupersetContextParsingMixin.parse_superset_link:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Extract candidate identifiers and query state from supported Superset URLs. - # @RELATION: CALLS -> [SupersetClient.get_dashboard_detail] - # @PRE: link is a non-empty Superset URL compatible with the configured environment. - # @POST: returns resolved dataset/dashboard context, preserving explicit partial-recovery state if some identifiers cannot be confirmed. - # @SIDE_EFFECT: may issue Superset API reads to resolve dataset references from dashboard or chart URLs. - # @DATA_CONTRACT: Input[link:str] -> Output[SupersetParsedContext] + # #region SupersetContextParsingMixin.parse_superset_link [C:4] [TYPE Function] + # @BRIEF Extract candidate identifiers and query state from supported Superset URLs. + # @PRE link is a non-empty Superset URL compatible with the configured environment. + # @POST returns resolved dataset/dashboard context, preserving explicit partial-recovery state if some identifiers cannot be confirmed. + # @SIDE_EFFECT may issue Superset API reads to resolve dataset references from dashboard or chart URLs. + # @DATA_CONTRACT Input[link:str] -> Output[SupersetParsedContext] + # @RELATION CALLS -> [SupersetClient.get_dashboard_detail] def parse_superset_link(self, link: str) -> SupersetParsedContext: with belief_scope("SupersetContextExtractor.parse_superset_link"): normalized_link = str(link or "").strip() @@ -332,12 +328,11 @@ class SupersetContextParsingMixin: ) return result - # [/DEF:SupersetContextParsingMixin.parse_superset_link:Function] + # #endregion SupersetContextParsingMixin.parse_superset_link - # [DEF:SupersetContextParsingMixin._recover_dataset_binding_from_dashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Recover a dataset binding from resolved dashboard context while preserving explicit unresolved markers. - # @RELATION: CALLS -> [SupersetClient.get_dashboard_detail] + # #region SupersetContextParsingMixin._recover_dataset_binding_from_dashboard [C:3] [TYPE Function] + # @BRIEF Recover a dataset binding from resolved dashboard context while preserving explicit unresolved markers. + # @RELATION CALLS -> [SupersetClient.get_dashboard_detail] def _recover_dataset_binding_from_dashboard( self, dashboard_id: int, @@ -374,7 +369,7 @@ class SupersetContextParsingMixin: unresolved_references.append("dashboard_dataset_binding_missing") return None, unresolved_references - # [/DEF:SupersetContextParsingMixin._recover_dataset_binding_from_dashboard:Function] + # #endregion SupersetContextParsingMixin._recover_dataset_binding_from_dashboard -# [/DEF:SupersetContextParsingMixin:Class] +# #endregion SupersetContextParsingMixin diff --git a/backend/src/core/utils/superset_context_extractor/_pii.py b/backend/src/core/utils/superset_context_extractor/_pii.py index dea57138..c285f2ab 100644 --- a/backend/src/core/utils/superset_context_extractor/_pii.py +++ b/backend/src/core/utils/superset_context_extractor/_pii.py @@ -1,15 +1,13 @@ -# [DEF:SupersetContextExtractorPII:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, pii, masking, sanitization, privacy -# @PURPOSE: PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers. -# [/DEF:SupersetContextExtractorPII:Module] +# #region SupersetContextExtractorPII [C:3] [TYPE Module] [SEMANTICS superset, pii, masking, sanitization, privacy] +# @BRIEF PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers. +# @LAYER Infra +# #endregion SupersetContextExtractorPII -# [DEF:_pii_imports:Block] +# #region _pii_imports [TYPE Block] import re from copy import deepcopy from typing import Any, Dict, List, Tuple -# [/DEF:_pii_imports:Block] +# #endregion _pii_imports _EMAIL_PATTERN = re.compile( r"(?P[A-Za-z0-9._%+-]+)@(?P[A-Za-z0-9.-]+\.[A-Za-z]{2,})" @@ -23,9 +21,8 @@ _MIXED_IDENTIFIER_PATTERN = re.compile( ) -# [DEF:mask_pii_value:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context. +# #region mask_pii_value [C:2] [TYPE Function] +# @BRIEF Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context. def mask_pii_value(value: Any) -> Tuple[Any, bool]: if isinstance(value, str): return _mask_sensitive_text(value) @@ -48,12 +45,11 @@ def mask_pii_value(value: Any) -> Tuple[Any, bool]: return value, False -# [/DEF:mask_pii_value:Function] +# #endregion mask_pii_value -# [DEF:sanitize_imported_filter_for_assistant:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected. +# #region sanitize_imported_filter_for_assistant [C:2] [TYPE Function] +# @BRIEF Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected. def sanitize_imported_filter_for_assistant( filter_payload: Dict[str, Any], ) -> Dict[str, Any]: @@ -66,12 +62,11 @@ def sanitize_imported_filter_for_assistant( return sanitized -# [/DEF:sanitize_imported_filter_for_assistant:Function] +# #endregion sanitize_imported_filter_for_assistant -# [DEF:_mask_sensitive_text:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape. +# #region _mask_sensitive_text [C:2] [TYPE Function] +# @BRIEF Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape. def _mask_sensitive_text(value: str) -> Tuple[str, bool]: masked = value masked_any = False @@ -109,4 +104,4 @@ def _mask_sensitive_text(value: str) -> Tuple[str, bool]: return masked, masked_any -# [/DEF:_mask_sensitive_text:Function] +# #endregion _mask_sensitive_text diff --git a/backend/src/core/utils/superset_context_extractor/_recovery.py b/backend/src/core/utils/superset_context_extractor/_recovery.py index 1ca11888..18fbac87 100644 --- a/backend/src/core/utils/superset_context_extractor/_recovery.py +++ b/backend/src/core/utils/superset_context_extractor/_recovery.py @@ -1,13 +1,11 @@ -# [DEF:SupersetContextRecoveryMixin:Module] -# @COMPLEXITY: 4 -# @LAYER: Infra -# @SEMANTICS: superset, filters, recovery, imported_filters -# @PURPOSE: Recover imported filters from Superset parsed context and dashboard metadata. -# @RELATION: CALLS -> [SupersetClient] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase] -# [/DEF:SupersetContextRecoveryMixin:Module] +# #region SupersetContextRecoveryMixin [C:4] [TYPE Module] [SEMANTICS superset, filters, recovery, imported_filters] +# @BRIEF Recover imported filters from Superset parsed context and dashboard metadata. +# @LAYER Infra +# @RELATION CALLS -> [SupersetClient] +# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase] +# #endregion SupersetContextRecoveryMixin -# [DEF:_recovery_imports:Block] +# #region _recovery_imports [TYPE Block] from __future__ import annotations import json @@ -18,21 +16,19 @@ from ...logger import belief_scope, logger from ._base import SupersetParsedContext logger = cast(Any, logger) -# [/DEF:_recovery_imports:Block] +# #endregion _recovery_imports -# [DEF:SupersetContextRecoveryMixin:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Mixin providing filter recovery for the composed SupersetContextExtractor. +# #region SupersetContextRecoveryMixin [C:4] [TYPE Class] +# @BRIEF Mixin providing filter recovery for the composed SupersetContextExtractor. class SupersetContextRecoveryMixin: - # [DEF:SupersetContextRecoveryMixin.recover_imported_filters:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Build imported filter entries from URL state and Superset-side saved context. - # @RELATION: CALLS -> [SupersetClient.get_dashboard] - # @PRE: parsed_context comes from a successful Superset link parse for one environment. - # @POST: returns explicit recovered and partial filter entries with preserved provenance and confirmation requirements. - # @SIDE_EFFECT: may issue Superset reads for dashboard metadata enrichment. - # @DATA_CONTRACT: Input[SupersetParsedContext] -> Output[List[Dict[str,Any]]] + # #region SupersetContextRecoveryMixin.recover_imported_filters [C:4] [TYPE Function] + # @BRIEF Build imported filter entries from URL state and Superset-side saved context. + # @PRE parsed_context comes from a successful Superset link parse for one environment. + # @POST returns explicit recovered and partial filter entries with preserved provenance and confirmation requirements. + # @SIDE_EFFECT may issue Superset reads for dashboard metadata enrichment. + # @DATA_CONTRACT Input[SupersetParsedContext] -> Output[List[Dict[str,Any]]] + # @RELATION CALLS -> [SupersetClient.get_dashboard] def recover_imported_filters( self, parsed_context: SupersetParsedContext ) -> List[Dict[str, Any]]: @@ -263,11 +259,10 @@ class SupersetContextRecoveryMixin: ) return recovered_filters - # [/DEF:SupersetContextRecoveryMixin.recover_imported_filters:Function] + # #endregion SupersetContextRecoveryMixin.recover_imported_filters - # [DEF:SupersetContextRecoveryMixin._normalize_imported_filter_payload:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize one imported-filter payload with explicit provenance and confirmation state. + # #region SupersetContextRecoveryMixin._normalize_imported_filter_payload [C:2] [TYPE Function] + # @BRIEF Normalize one imported-filter payload with explicit provenance and confirmation state. def _normalize_imported_filter_payload( self, payload: Dict[str, Any], @@ -308,7 +303,7 @@ class SupersetContextRecoveryMixin: "notes": str(payload.get("notes") or default_note), } - # [/DEF:SupersetContextRecoveryMixin._normalize_imported_filter_payload:Function] + # #endregion SupersetContextRecoveryMixin._normalize_imported_filter_payload -# [/DEF:SupersetContextRecoveryMixin:Class] +# #endregion SupersetContextRecoveryMixin diff --git a/backend/src/core/utils/superset_context_extractor/_templates.py b/backend/src/core/utils/superset_context_extractor/_templates.py index 7c076b0c..37b1ddbc 100644 --- a/backend/src/core/utils/superset_context_extractor/_templates.py +++ b/backend/src/core/utils/superset_context_extractor/_templates.py @@ -1,13 +1,11 @@ -# [DEF:SupersetContextTemplatesMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: superset, template_variables, jinja, discovery, deterministic -# @PURPOSE: Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution. -# @RELATION: DEPENDS_ON -> [TemplateVariable] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase] -# [/DEF:SupersetContextTemplatesMixin:Module] +# #region SupersetContextTemplatesMixin [C:3] [TYPE Module] [SEMANTICS superset, template_variables, jinja, discovery, deterministic] +# @BRIEF Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [TemplateVariable] +# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase] +# #endregion SupersetContextTemplatesMixin -# [DEF:_templates_imports:Block] +# #region _templates_imports [TYPE Block] from __future__ import annotations import re @@ -16,20 +14,18 @@ from typing import Any, Dict, List, Optional, Set, cast from ...logger import belief_scope, logger logger = cast(Any, logger) -# [/DEF:_templates_imports:Block] +# #endregion _templates_imports -# [DEF:SupersetContextTemplatesMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing template variable discovery for the composed SupersetContextExtractor. +# #region SupersetContextTemplatesMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing template variable discovery for the composed SupersetContextExtractor. class SupersetContextTemplatesMixin: - # [DEF:SupersetContextTemplatesMixin.discover_template_variables:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Detect runtime variables and Jinja references from dataset query-bearing fields. - # @PRE: dataset_payload is a Superset dataset-detail style payload with query-bearing fields when available. - # @POST: returns deduplicated explicit variable records without executing Jinja or fabricating runtime values. - # @SIDE_EFFECT: none. - # @DATA_CONTRACT: Input[dataset_payload:Dict[str,Any]] -> Output[List[Dict[str,Any]]] + # #region SupersetContextTemplatesMixin.discover_template_variables [C:4] [TYPE Function] + # @BRIEF Detect runtime variables and Jinja references from dataset query-bearing fields. + # @PRE dataset_payload is a Superset dataset-detail style payload with query-bearing fields when available. + # @POST returns deduplicated explicit variable records without executing Jinja or fabricating runtime values. + # @SIDE_EFFECT none. + # @DATA_CONTRACT Input[dataset_payload:Dict[str,Any]] -> Output[List[Dict[str,Any]]] def discover_template_variables( self, dataset_payload: Dict[str, Any] ) -> List[Dict[str, Any]]: @@ -121,12 +117,11 @@ class SupersetContextTemplatesMixin: ) return discovered - # [/DEF:SupersetContextTemplatesMixin.discover_template_variables:Function] + # #endregion SupersetContextTemplatesMixin.discover_template_variables - # [DEF:SupersetContextTemplatesMixin._collect_query_bearing_expressions:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Collect SQL and expression-bearing dataset fields for deterministic template-variable discovery. - # @RELATION: DEPENDS_ON -> [SupersetContextTemplatesMixin.discover_template_variables] + # #region SupersetContextTemplatesMixin._collect_query_bearing_expressions [C:3] [TYPE Function] + # @BRIEF Collect SQL and expression-bearing dataset fields for deterministic template-variable discovery. + # @RELATION DEPENDS_ON -> [SupersetContextTemplatesMixin.discover_template_variables] def _collect_query_bearing_expressions( self, dataset_payload: Dict[str, Any] ) -> List[str]: @@ -165,11 +160,10 @@ class SupersetContextTemplatesMixin: return expressions - # [/DEF:SupersetContextTemplatesMixin._collect_query_bearing_expressions:Function] + # #endregion SupersetContextTemplatesMixin._collect_query_bearing_expressions - # [DEF:SupersetContextTemplatesMixin._append_template_variable:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Append one deduplicated template-variable descriptor. + # #region SupersetContextTemplatesMixin._append_template_variable [C:2] [TYPE Function] + # @BRIEF Append one deduplicated template-variable descriptor. def _append_template_variable( self, discovered: List[Dict[str, Any]], @@ -198,11 +192,10 @@ class SupersetContextTemplatesMixin: } ) - # [/DEF:SupersetContextTemplatesMixin._append_template_variable:Function] + # #endregion SupersetContextTemplatesMixin._append_template_variable - # [DEF:SupersetContextTemplatesMixin._extract_primary_jinja_identifier:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Extract a deterministic primary identifier from a Jinja expression without executing it. + # #region SupersetContextTemplatesMixin._extract_primary_jinja_identifier [C:2] [TYPE Function] + # @BRIEF Extract a deterministic primary identifier from a Jinja expression without executing it. def _extract_primary_jinja_identifier(self, expression: str) -> Optional[str]: matched = re.match(r"([A-Za-z_][A-Za-z0-9_]*)", expression.strip()) if matched is None: @@ -221,11 +214,10 @@ class SupersetContextTemplatesMixin: return None return candidate - # [/DEF:SupersetContextTemplatesMixin._extract_primary_jinja_identifier:Function] + # #endregion SupersetContextTemplatesMixin._extract_primary_jinja_identifier - # [DEF:SupersetContextTemplatesMixin._normalize_default_literal:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize literal default fragments from template helper calls into JSON-safe values. + # #region SupersetContextTemplatesMixin._normalize_default_literal [C:2] [TYPE Function] + # @BRIEF Normalize literal default fragments from template helper calls into JSON-safe values. def _normalize_default_literal(self, literal: Optional[str]) -> Any: normalized_literal = str(literal or "").strip() if not normalized_literal: @@ -249,7 +241,7 @@ class SupersetContextTemplatesMixin: except ValueError: return normalized_literal - # [/DEF:SupersetContextTemplatesMixin._normalize_default_literal:Function] + # #endregion SupersetContextTemplatesMixin._normalize_default_literal -# [/DEF:SupersetContextTemplatesMixin:Class] +# #endregion SupersetContextTemplatesMixin diff --git a/backend/src/dependencies.py b/backend/src/dependencies.py index a49e11d2..7a759dbc 100755 --- a/backend/src/dependencies.py +++ b/backend/src/dependencies.py @@ -1,18 +1,16 @@ -# [DEF:AppDependencies:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: dependency, injection, singleton, factory, auth, jwt -# @PURPOSE: Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports. -# @LAYER: Core -# @RELATION: Used by main app and API routers to get access to shared instances. -# @RELATION: CALLS ->[CleanReleaseRepository] -# @RELATION: CALLS ->[ConfigManager] -# @RELATION: CALLS ->[PluginLoader] -# @RELATION: CALLS ->[SchedulerService] -# @RELATION: CALLS ->[TaskManager] -# @RELATION: CALLS ->[get_all_plugin_configs] -# @RELATION: CALLS ->[get_db] -# @RELATION: CALLS ->[info] -# @RELATION: CALLS ->[init_db] +# #region AppDependencies [C:3] [TYPE Module] [SEMANTICS dependency, injection, singleton, factory, auth, jwt] +# @BRIEF Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports. +# @LAYER Core +# @RELATION Used by main app and API routers to get access to shared instances. +# @RELATION CALLS -> [CleanReleaseRepository] +# @RELATION CALLS -> [ConfigManager] +# @RELATION CALLS -> [PluginLoader] +# @RELATION CALLS -> [SchedulerService] +# @RELATION CALLS -> [TaskManager] +# @RELATION CALLS -> [get_all_plugin_configs] +# @RELATION CALLS -> [get_db] +# @RELATION CALLS -> [info] +# @RELATION CALLS -> [init_db] from pathlib import Path from typing import Optional @@ -60,12 +58,11 @@ scheduler_service: Optional[SchedulerService] = None resource_service: Optional[ResourceService] = None -# [DEF:get_config_manager:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for ConfigManager. -# @PRE: Global config_manager must be initialized. -# @POST: Returns shared ConfigManager instance. -# @RETURN: ConfigManager - The shared config manager instance. +# #region get_config_manager [C:1] [TYPE Function] +# @BRIEF Dependency injector for ConfigManager. +# @PRE Global config_manager must be initialized. +# @POST Returns shared ConfigManager instance. +# @RETURN ConfigManager - The shared config manager instance. def get_config_manager() -> ConfigManager: """Dependency injector for ConfigManager.""" global config_manager @@ -75,7 +72,7 @@ def get_config_manager() -> ConfigManager: return config_manager -# [/DEF:get_config_manager:Function] +# #endregion get_config_manager plugin_dir = Path(__file__).parent / "plugins" @@ -85,12 +82,11 @@ plugin_dir = Path(__file__).parent / "plugins" # initialize them inside the dependency functions. -# [DEF:get_plugin_loader:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for PluginLoader. -# @PRE: Global plugin_loader must be initialized. -# @POST: Returns shared PluginLoader instance. -# @RETURN: PluginLoader - The shared plugin loader instance. +# #region get_plugin_loader [C:1] [TYPE Function] +# @BRIEF Dependency injector for PluginLoader. +# @PRE Global plugin_loader must be initialized. +# @POST Returns shared PluginLoader instance. +# @RETURN PluginLoader - The shared plugin loader instance. def get_plugin_loader() -> PluginLoader: """Dependency injector for PluginLoader.""" global plugin_loader @@ -103,15 +99,14 @@ def get_plugin_loader() -> PluginLoader: return plugin_loader -# [/DEF:get_plugin_loader:Function] +# #endregion get_plugin_loader -# [DEF:get_task_manager:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for TaskManager. -# @PRE: Global task_manager must be initialized. -# @POST: Returns shared TaskManager instance. -# @RETURN: TaskManager - The shared task manager instance. +# #region get_task_manager [C:1] [TYPE Function] +# @BRIEF Dependency injector for TaskManager. +# @PRE Global task_manager must be initialized. +# @POST Returns shared TaskManager instance. +# @RETURN TaskManager - The shared task manager instance. def get_task_manager() -> TaskManager: """Dependency injector for TaskManager.""" global task_manager @@ -121,15 +116,14 @@ def get_task_manager() -> TaskManager: return task_manager -# [/DEF:get_task_manager:Function] +# #endregion get_task_manager -# [DEF:get_scheduler_service:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for SchedulerService. -# @PRE: Global scheduler_service must be initialized. -# @POST: Returns shared SchedulerService instance. -# @RETURN: SchedulerService - The shared scheduler service instance. +# #region get_scheduler_service [C:1] [TYPE Function] +# @BRIEF Dependency injector for SchedulerService. +# @PRE Global scheduler_service must be initialized. +# @POST Returns shared SchedulerService instance. +# @RETURN SchedulerService - The shared scheduler service instance. def get_scheduler_service() -> SchedulerService: """Dependency injector for SchedulerService.""" global scheduler_service @@ -139,15 +133,14 @@ def get_scheduler_service() -> SchedulerService: return scheduler_service -# [/DEF:get_scheduler_service:Function] +# #endregion get_scheduler_service -# [DEF:get_resource_service:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for ResourceService. -# @PRE: Global resource_service must be initialized. -# @POST: Returns shared ResourceService instance. -# @RETURN: ResourceService - The shared resource service instance. +# #region get_resource_service [C:1] [TYPE Function] +# @BRIEF Dependency injector for ResourceService. +# @PRE Global resource_service must be initialized. +# @POST Returns shared ResourceService instance. +# @RETURN ResourceService - The shared resource service instance. def get_resource_service() -> ResourceService: """Dependency injector for ResourceService.""" global resource_service @@ -157,42 +150,39 @@ def get_resource_service() -> ResourceService: return resource_service -# [/DEF:get_resource_service:Function] +# #endregion get_resource_service -# [DEF:get_mapping_service:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for MappingService. -# @PRE: Global config_manager must be initialized. -# @POST: Returns new MappingService instance. -# @RETURN: MappingService - A new mapping service instance. +# #region get_mapping_service [C:1] [TYPE Function] +# @BRIEF Dependency injector for MappingService. +# @PRE Global config_manager must be initialized. +# @POST Returns new MappingService instance. +# @RETURN MappingService - A new mapping service instance. def get_mapping_service() -> MappingService: """Dependency injector for MappingService.""" return MappingService(get_config_manager()) -# [/DEF:get_mapping_service:Function] +# #endregion get_mapping_service _clean_release_repository = CleanReleaseRepository() -# [DEF:get_clean_release_repository:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Legacy compatibility shim for CleanReleaseRepository. -# @POST: Returns a shared CleanReleaseRepository instance. +# #region get_clean_release_repository [C:1] [TYPE Function] +# @BRIEF Legacy compatibility shim for CleanReleaseRepository. +# @POST Returns a shared CleanReleaseRepository instance. def get_clean_release_repository() -> CleanReleaseRepository: """Legacy compatibility shim for CleanReleaseRepository.""" return _clean_release_repository -# [/DEF:get_clean_release_repository:Function] +# #endregion get_clean_release_repository -# [DEF:get_clean_release_facade:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Dependency injector for CleanReleaseFacade. -# @POST: Returns a facade instance with a fresh DB session. +# #region get_clean_release_facade [C:1] [TYPE Function] +# @BRIEF Dependency injector for CleanReleaseFacade. +# @POST Returns a facade instance with a fresh DB session. def get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade: candidate_repo = CandidateRepository(db) artifact_repo = ArtifactRepository(db) @@ -218,22 +208,20 @@ def get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade: ) -# [/DEF:get_clean_release_facade:Function] +# #endregion get_clean_release_facade -# [DEF:oauth2_scheme:Variable] -# @RELATION: DEPENDS_ON -> OAuth2PasswordBearer -# @COMPLEXITY: 1 -# @PURPOSE: OAuth2 password bearer scheme for token extraction. +# #region oauth2_scheme [C:1] [TYPE Variable] +# @BRIEF OAuth2 password bearer scheme for token extraction. +# @RELATION DEPENDS_ON -> [OAuth2PasswordBearer] oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") -# [/DEF:oauth2_scheme:Variable] +# #endregion oauth2_scheme -# [DEF:get_current_user:Function] -# @RELATION: CALLS -> AuthRepository -# @COMPLEXITY: 3 -# @PURPOSE: Dependency for retrieving currently authenticated user from a JWT. -# @PRE: JWT token provided in Authorization header. -# @POST: Returns User object if token is valid. +# #region get_current_user [C:3] [TYPE Function] +# @BRIEF Dependency for retrieving currently authenticated user from a JWT. +# @PRE JWT token provided in Authorization header. +# @POST Returns User object if token is valid. +# @RELATION CALLS -> [AuthRepository] # @THROW: HTTPException 401 if token is invalid or user not found. # @PARAM: token (str) - Extracted JWT token. # @PARAM: db (Session) - Auth database session. @@ -260,15 +248,14 @@ def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db return user -# [/DEF:get_current_user:Function] +# #endregion get_current_user -# [DEF:has_permission:Function] -# @RELATION: CALLS -> AuthRepository -# @COMPLEXITY: 3 -# @PURPOSE: Dependency for checking if the current user has a specific permission. -# @PRE: User is authenticated. -# @POST: Returns True if user has permission. +# #region has_permission [C:3] [TYPE Function] +# @BRIEF Dependency for checking if the current user has a specific permission. +# @PRE User is authenticated. +# @POST Returns True if user has permission. +# @RELATION CALLS -> [AuthRepository] # @THROW: HTTPException 403 if permission is denied. # @PARAM: resource (str) - The resource identifier. # @PARAM: action (str) - The action identifier (READ, EXECUTE, WRITE). @@ -301,6 +288,6 @@ def has_permission(resource: str, action: str): return permission_checker -# [/DEF:has_permission:Function] +# #endregion has_permission -# [/DEF:AppDependencies:Module] +# #endregion AppDependencies diff --git a/backend/src/models/__tests__/test_clean_release.py b/backend/src/models/__tests__/test_clean_release.py index 14e6e8d0..2304fbbd 100644 --- a/backend/src/models/__tests__/test_clean_release.py +++ b/backend/src/models/__tests__/test_clean_release.py @@ -1,7 +1,7 @@ -# [DEF:TestCleanReleaseModels:Module] -# @RELATION: VERIFIES -> [CleanReleaseModels] -# @PURPOSE: Contract testing for Clean Release models -# [/DEF:TestCleanReleaseModels:Module] +# #region TestCleanReleaseModels [TYPE Module] +# @BRIEF Contract testing for Clean Release models +# @RELATION VERIFIES -> [CleanReleaseModels] +# #endregion TestCleanReleaseModels import pytest from datetime import datetime @@ -38,21 +38,21 @@ def valid_candidate_data(): } -# [DEF:test_release_candidate_valid:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that a valid release candidate can be instantiated. +# #region test_release_candidate_valid [TYPE Function] +# @BRIEF Verify that a valid release candidate can be instantiated. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_release_candidate_valid(valid_candidate_data): rc = ReleaseCandidate(**valid_candidate_data) assert rc.candidate_id == "RC-001" assert rc.status == ReleaseCandidateStatus.DRAFT -# [/DEF:test_release_candidate_valid:Function] +# #endregion test_release_candidate_valid -# [DEF:test_release_candidate_empty_id:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that a release candidate with an empty ID is rejected. +# #region test_release_candidate_empty_id [TYPE Function] +# @BRIEF Verify that a release candidate with an empty ID is rejected. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_release_candidate_empty_id(valid_candidate_data): valid_candidate_data["candidate_id"] = " " with pytest.raises(ValueError, match="candidate_id must be non-empty"): @@ -60,7 +60,7 @@ def test_release_candidate_empty_id(valid_candidate_data): # @TEST_FIXTURE: valid_enterprise_policy -# [/DEF:test_release_candidate_empty_id:Function] +# #endregion test_release_candidate_empty_id @pytest.fixture @@ -78,21 +78,21 @@ def valid_policy_data(): # @TEST_INVARIANT: policy_purity -# [DEF:test_enterprise_policy_valid:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that a valid enterprise policy is accepted. +# #region test_enterprise_policy_valid [TYPE Function] +# @BRIEF Verify that a valid enterprise policy is accepted. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_enterprise_policy_valid(valid_policy_data): policy = CleanProfilePolicy(**valid_policy_data) assert policy.external_source_forbidden is True # @TEST_EDGE: enterprise_policy_missing_prohibited -# [/DEF:test_enterprise_policy_valid:Function] +# #endregion test_enterprise_policy_valid -# [DEF:test_enterprise_policy_missing_prohibited:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that an enterprise policy without prohibited categories is rejected. +# #region test_enterprise_policy_missing_prohibited [TYPE Function] +# @BRIEF Verify that an enterprise policy without prohibited categories is rejected. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_enterprise_policy_missing_prohibited(valid_policy_data): valid_policy_data["prohibited_artifact_categories"] = [] with pytest.raises( @@ -103,12 +103,12 @@ def test_enterprise_policy_missing_prohibited(valid_policy_data): # @TEST_EDGE: enterprise_policy_external_allowed -# [/DEF:test_enterprise_policy_missing_prohibited:Function] +# #endregion test_enterprise_policy_missing_prohibited -# [DEF:test_enterprise_policy_external_allowed:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that an enterprise policy allowing external sources is rejected. +# #region test_enterprise_policy_external_allowed [TYPE Function] +# @BRIEF Verify that an enterprise policy allowing external sources is rejected. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_enterprise_policy_external_allowed(valid_policy_data): valid_policy_data["external_source_forbidden"] = False with pytest.raises( @@ -120,12 +120,12 @@ def test_enterprise_policy_external_allowed(valid_policy_data): # @TEST_INVARIANT: manifest_consistency # @TEST_EDGE: manifest_count_mismatch -# [/DEF:test_enterprise_policy_external_allowed:Function] +# #endregion test_enterprise_policy_external_allowed -# [DEF:test_manifest_count_mismatch:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify that a manifest with count mismatches is rejected. +# #region test_manifest_count_mismatch [TYPE Function] +# @BRIEF Verify that a manifest with count mismatches is rejected. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_manifest_count_mismatch(): summary = ManifestSummary( included_count=1, excluded_count=0, prohibited_detected_count=0 @@ -165,12 +165,12 @@ def test_manifest_count_mismatch(): # @TEST_INVARIANT: run_integrity # @TEST_EDGE: compliant_run_stage_fail -# [/DEF:test_manifest_count_mismatch:Function] +# #endregion test_manifest_count_mismatch -# [DEF:test_compliant_run_validation:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify compliant run validation logic and mandatory stage checks. +# #region test_compliant_run_validation [TYPE Function] +# @BRIEF Verify compliant run validation logic and mandatory stage checks. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_compliant_run_validation(): base_run = { "check_run_id": "run1", @@ -211,12 +211,12 @@ def test_compliant_run_validation(): ComplianceCheckRun(**base_run) -# [/DEF:test_compliant_run_validation:Function] +# #endregion test_compliant_run_validation -# [DEF:test_report_validation:Function] -# @RELATION: BINDS_TO -> [TestCleanReleaseModels] -# @PURPOSE: Verify compliance report validation based on status and violation counts. +# #region test_report_validation [TYPE Function] +# @BRIEF Verify compliance report validation based on status and violation counts. +# @RELATION BINDS_TO -> [TestCleanReleaseModels] def test_report_validation(): # Valid blocked report ComplianceReport( @@ -246,4 +246,4 @@ def test_report_validation(): ) -# [/DEF:test_report_validation:Function] +# #endregion test_report_validation diff --git a/backend/src/models/__tests__/test_models.py b/backend/src/models/__tests__/test_models.py index fd3e321a..b7166526 100644 --- a/backend/src/models/__tests__/test_models.py +++ b/backend/src/models/__tests__/test_models.py @@ -1,8 +1,7 @@ -# [DEF:test_models:Module] -# @COMPLEXITY: 1 -# @PURPOSE: Unit tests for data models -# @LAYER: Domain -# @RELATION: VERIFIES -> [ModelsPackage] +# #region test_models [C:1] [TYPE Module] +# @BRIEF Unit tests for data models +# @LAYER Domain +# @RELATION VERIFIES -> [ModelsPackage] import sys from pathlib import Path @@ -14,11 +13,11 @@ from src.core.config_models import Environment from src.core.logger import belief_scope -# [DEF:test_environment_model:Function] -# @RELATION: BINDS_TO -> test_models -# @PURPOSE: Tests that Environment model correctly stores values. -# @PRE: Environment class is available. -# @POST: Values are verified. +# #region test_environment_model [TYPE Function] +# @BRIEF Tests that Environment model correctly stores values. +# @PRE Environment class is available. +# @POST Values are verified. +# @RELATION BINDS_TO -> [test_models] def test_environment_model(): with belief_scope("test_environment_model"): env = Environment( @@ -33,7 +32,7 @@ def test_environment_model(): assert env.url == "http://localhost:8088/api/v1" -# [/DEF:test_environment_model:Function] +# #endregion test_environment_model -# [/DEF:test_models:Module] +# #endregion test_models diff --git a/backend/src/models/__tests__/test_report_models.py b/backend/src/models/__tests__/test_report_models.py index b39a9892..bee3302e 100644 --- a/backend/src/models/__tests__/test_report_models.py +++ b/backend/src/models/__tests__/test_report_models.py @@ -1,8 +1,7 @@ -# [DEF:test_report_models:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for report Pydantic models and their validators -# @LAYER: Domain +# #region test_report_models [C:3] [TYPE Module] +# @BRIEF Unit tests for report Pydantic models and their validators +# @LAYER Domain +# @RELATION BELONGS_TO -> [SrcRoot] import sys from pathlib import Path @@ -232,4 +231,4 @@ class TestReportDetailView: assert detail.diagnostics["cause"] == "timeout" assert "Retry" in detail.next_actions -# [/DEF:test_report_models:Module] +# #endregion test_report_models diff --git a/backend/src/models/assistant.py b/backend/src/models/assistant.py index 62dda5b1..df87ccff 100644 --- a/backend/src/models/assistant.py +++ b/backend/src/models/assistant.py @@ -1,10 +1,8 @@ -# [DEF:AssistantModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: assistant, audit, confirmation, chat -# @PURPOSE: SQLAlchemy models for assistant audit trail and confirmation tokens. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> MappingModels -# @INVARIANT: Assistant records preserve immutable ids and creation timestamps. +# #region AssistantModels [C:3] [TYPE Module] [SEMANTICS assistant, audit, confirmation, chat] +# @BRIEF SQLAlchemy models for assistant audit trail and confirmation tokens. +# @LAYER Domain +# @INVARIANT Assistant records preserve immutable ids and creation timestamps. +# @RELATION DEPENDS_ON -> [MappingModels] from datetime import datetime @@ -13,12 +11,11 @@ from sqlalchemy import Column, String, DateTime, JSON, Text from .mapping import Base -# [DEF:AssistantAuditRecord:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Store audit decisions and outcomes produced by assistant command handling. -# @RELATION: INHERITS -> MappingModels -# @PRE: user_id must identify the actor for every record. -# @POST: Audit payload remains available for compliance and debugging. +# #region AssistantAuditRecord [C:3] [TYPE Class] +# @BRIEF Store audit decisions and outcomes produced by assistant command handling. +# @PRE user_id must identify the actor for every record. +# @POST Audit payload remains available for compliance and debugging. +# @RELATION INHERITS -> [MappingModels] class AssistantAuditRecord(Base): __tablename__ = "assistant_audit" @@ -32,15 +29,14 @@ class AssistantAuditRecord(Base): created_at = Column(DateTime, default=datetime.utcnow, nullable=False) -# [/DEF:AssistantAuditRecord:Class] +# #endregion AssistantAuditRecord -# [DEF:AssistantMessageRecord:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Persist chat history entries for assistant conversations. -# @RELATION: INHERITS -> MappingModels -# @PRE: user_id, conversation_id, role and text must be present. -# @POST: Message row can be queried in chronological order. +# #region AssistantMessageRecord [C:3] [TYPE Class] +# @BRIEF Persist chat history entries for assistant conversations. +# @PRE user_id, conversation_id, role and text must be present. +# @POST Message row can be queried in chronological order. +# @RELATION INHERITS -> [MappingModels] class AssistantMessageRecord(Base): __tablename__ = "assistant_messages" @@ -56,15 +52,14 @@ class AssistantMessageRecord(Base): created_at = Column(DateTime, default=datetime.utcnow, nullable=False) -# [/DEF:AssistantMessageRecord:Class] +# #endregion AssistantMessageRecord -# [DEF:AssistantConfirmationRecord:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Persist risky operation confirmation tokens with lifecycle state. -# @RELATION: INHERITS -> MappingModels -# @PRE: intent/dispatch and expiry timestamp must be provided. -# @POST: State transitions can be tracked and audited. +# #region AssistantConfirmationRecord [C:3] [TYPE Class] +# @BRIEF Persist risky operation confirmation tokens with lifecycle state. +# @PRE intent/dispatch and expiry timestamp must be provided. +# @POST State transitions can be tracked and audited. +# @RELATION INHERITS -> [MappingModels] class AssistantConfirmationRecord(Base): __tablename__ = "assistant_confirmations" @@ -79,5 +74,5 @@ class AssistantConfirmationRecord(Base): consumed_at = Column(DateTime, nullable=True) -# [/DEF:AssistantConfirmationRecord:Class] -# [/DEF:AssistantModels:Module] +# #endregion AssistantConfirmationRecord +# #endregion AssistantModels diff --git a/backend/src/models/auth.py b/backend/src/models/auth.py index 3d722a24..09303904 100644 --- a/backend/src/models/auth.py +++ b/backend/src/models/auth.py @@ -1,11 +1,9 @@ -# [DEF:AuthModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: auth, models, user, role, permission, sqlalchemy -# @PURPOSE: SQLAlchemy models for multi-user authentication and authorization. -# @LAYER: Domain -# @RELATION: INHERITS_FROM -> [Base] +# #region AuthModels [C:3] [TYPE Module] [SEMANTICS auth, models, user, role, permission, sqlalchemy] +# @BRIEF SQLAlchemy models for multi-user authentication and authorization. +# @LAYER Domain +# @INVARIANT Usernames and emails must be unique. +# @RELATION INHERITS_FROM -> [Base] # -# @INVARIANT: Usernames and emails must be unique. # [SECTION: IMPORTS] import uuid @@ -16,46 +14,46 @@ from .mapping import Base # [/SECTION] -# [DEF:generate_uuid:Function] -# @PURPOSE: Generates a unique UUID string. -# @POST: Returns a string representation of a new UUID. -# @RELATION: DEPENDS_ON -> [uuid] +# #region generate_uuid [TYPE Function] +# @BRIEF Generates a unique UUID string. +# @POST Returns a string representation of a new UUID. +# @RELATION DEPENDS_ON -> [uuid] def generate_uuid(): return str(uuid.uuid4()) -# [/DEF:generate_uuid:Function] +# #endregion generate_uuid -# [DEF:user_roles:Table] -# @PURPOSE: Association table for many-to-many relationship between Users and Roles. -# @RELATION: DEPENDS_ON -> [Base] -# @RELATION: DEPENDS_ON -> [User] -# @RELATION: DEPENDS_ON -> [Role] +# #region user_roles [TYPE Table] +# @BRIEF Association table for many-to-many relationship between Users and Roles. +# @RELATION DEPENDS_ON -> [Base] +# @RELATION DEPENDS_ON -> [User] +# @RELATION DEPENDS_ON -> [Role] user_roles = Table( "user_roles", Base.metadata, Column("user_id", String, ForeignKey("users.id"), primary_key=True), Column("role_id", String, ForeignKey("roles.id"), primary_key=True), ) -# [/DEF:user_roles:Table] +# #endregion user_roles -# [DEF:role_permissions:Table] -# @PURPOSE: Association table for many-to-many relationship between Roles and Permissions. -# @RELATION: DEPENDS_ON -> [Base] -# @RELATION: DEPENDS_ON -> [Role] -# @RELATION: DEPENDS_ON -> [Permission] +# #region role_permissions [TYPE Table] +# @BRIEF Association table for many-to-many relationship between Roles and Permissions. +# @RELATION DEPENDS_ON -> [Base] +# @RELATION DEPENDS_ON -> [Role] +# @RELATION DEPENDS_ON -> [Permission] role_permissions = Table( "role_permissions", Base.metadata, Column("role_id", String, ForeignKey("roles.id"), primary_key=True), Column("permission_id", String, ForeignKey("permissions.id"), primary_key=True), ) -# [/DEF:role_permissions:Table] +# #endregion role_permissions -# [DEF:User:Class] -# @PURPOSE: Represents an identity that can authenticate to the system. -# @RELATION: HAS_MANY -> [Role] +# #region User [TYPE Class] +# @BRIEF Represents an identity that can authenticate to the system. +# @RELATION HAS_MANY -> [Role] class User(Base): __tablename__ = "users" @@ -73,13 +71,13 @@ class User(Base): roles = relationship("Role", secondary=user_roles, back_populates="users") -# [/DEF:User:Class] +# #endregion User -# [DEF:Role:Class] -# @PURPOSE: Represents a collection of permissions. -# @RELATION: HAS_MANY -> [User] -# @RELATION: HAS_MANY -> [Permission] +# #region Role [TYPE Class] +# @BRIEF Represents a collection of permissions. +# @RELATION HAS_MANY -> [User] +# @RELATION HAS_MANY -> [Permission] class Role(Base): __tablename__ = "roles" @@ -93,12 +91,12 @@ class Role(Base): ) -# [/DEF:Role:Class] +# #endregion Role -# [DEF:Permission:Class] -# @PURPOSE: Represents a specific capability within the system. -# @RELATION: HAS_MANY -> [Role] +# #region Permission [TYPE Class] +# @BRIEF Represents a specific capability within the system. +# @RELATION HAS_MANY -> [Role] class Permission(Base): __tablename__ = "permissions" @@ -111,12 +109,12 @@ class Permission(Base): ) -# [/DEF:Permission:Class] +# #endregion Permission -# [DEF:ADGroupMapping:Class] -# @PURPOSE: Maps an Active Directory group to a local System Role. -# @RELATION: DEPENDS_ON -> [Role] +# #region ADGroupMapping [TYPE Class] +# @BRIEF Maps an Active Directory group to a local System Role. +# @RELATION DEPENDS_ON -> [Role] class ADGroupMapping(Base): __tablename__ = "ad_group_mappings" @@ -127,6 +125,6 @@ class ADGroupMapping(Base): role = relationship("Role") -# [/DEF:ADGroupMapping:Class] +# #endregion ADGroupMapping -# [/DEF:AuthModels:Module] +# #endregion AuthModels diff --git a/backend/src/models/auth.py.bak b/backend/src/models/auth.py.bak new file mode 100644 index 00000000..06ee73d9 --- /dev/null +++ b/backend/src/models/auth.py.bak @@ -0,0 +1,134 @@ +# [DEF:AuthModels:Module] +# @COMPLEXITY: 3 +# @SEMANTICS: auth, models, user, role, permission, sqlalchemy +# @PURPOSE: SQLAlchemy models for multi-user authentication and authorization. +# @LAYER: Domain +# @RELATION: INHERITS_FROM -> [Base] + +# [SECTION: IMPORTS] +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Table +from sqlalchemy.orm import relationship +from .mapping import Base +# [/SECTION] + + +# [DEF:generate_uuid:Function] +# @COMPLEXITY: 1 +def generate_uuid(): + return str(uuid.uuid4()) + + +# [/DEF:generate_uuid:Function] + +# [DEF:user_roles:Class] +# @COMPLEXITY: 3 +# @BRIEF: Association table for many-to-many relationship between Users and Roles. +# @RELATION: DEPENDS_ON -> [Base] +# @RELATION: DEPENDS_ON -> [User] +# @RELATION: DEPENDS_ON -> [Role] +user_roles = Table( + "user_roles", + Base.metadata, + Column("user_id", String, ForeignKey("users.id"), primary_key=True), + Column("role_id", String, ForeignKey("roles.id"), primary_key=True), +) +# [/DEF:user_roles:Class] + +# [DEF:role_permissions:Class] +# @COMPLEXITY: 3 +# @BRIEF: Association table for many-to-many relationship between Roles and Permissions. +# @RELATION: DEPENDS_ON -> [Base] +# @RELATION: DEPENDS_ON -> [Role] +# @RELATION: DEPENDS_ON -> [Permission] +role_permissions = Table( + "role_permissions", + Base.metadata, + Column("role_id", String, ForeignKey("roles.id"), primary_key=True), + Column("permission_id", String, ForeignKey("permissions.id"), primary_key=True), +) +# [/DEF:role_permissions:Class] + + +# [DEF:User:Class] +# @COMPLEXITY: 3 +# @BRIEF: Represents an identity that can authenticate to the system. +# @RELATION: HAS_MANY -> [Role] +class User(Base): + __tablename__ = "users" + + id = Column(String, primary_key=True, default=generate_uuid) + username = Column(String, unique=True, index=True, nullable=False) + email = Column(String, unique=True, index=True, nullable=True) + password_hash = Column(String, nullable=True) + full_name = Column(String, nullable=True) + auth_source = Column(String, default="LOCAL") # LOCAL or ADFS + is_active = Column(Boolean, default=True) + is_ad_user = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + last_login = Column(DateTime, nullable=True) + + roles = relationship("Role", secondary=user_roles, back_populates="users") + + +# [/DEF:User:Class] + + +# [DEF:Role:Class] +# @COMPLEXITY: 3 +# @BRIEF: Represents a collection of permissions. +# @RELATION: HAS_MANY -> [User] +# @RELATION: HAS_MANY -> [Permission] +class Role(Base): + __tablename__ = "roles" + + id = Column(String, primary_key=True, default=generate_uuid) + name = Column(String, unique=True, index=True, nullable=False) + description = Column(String, nullable=True) + + users = relationship("User", secondary=user_roles, back_populates="roles") + permissions = relationship( + "Permission", secondary=role_permissions, back_populates="roles" + ) + + +# [/DEF:Role:Class] + + +# [DEF:Permission:Class] +# @COMPLEXITY: 3 +# @BRIEF: Represents a specific capability within the system. +# @RELATION: HAS_MANY -> [Role] +class Permission(Base): + __tablename__ = "permissions" + + id = Column(String, primary_key=True, default=generate_uuid) + resource = Column(String, nullable=False) # e.g. "plugin:backup" + action = Column(String, nullable=False) # e.g. "READ", "EXECUTE", "WRITE" + + roles = relationship( + "Role", secondary=role_permissions, back_populates="permissions" + ) + + +# [/DEF:Permission:Class] + + +# [DEF:ADGroupMapping:Class] +# @COMPLEXITY: 3 +# @BRIEF: Maps an Active Directory group to a local System Role. +# @RELATION: DEPENDS_ON -> [Role] +class ADGroupMapping(Base): + __tablename__ = "ad_group_mappings" + + id = Column(String, primary_key=True, default=generate_uuid) + ad_group = Column(String, unique=True, index=True, nullable=False) + role_id = Column(String, ForeignKey("roles.id"), nullable=False) + + role = relationship("Role") + + +# [/DEF:ADGroupMapping:Class] + +# [/DEF:AuthModels:Module] diff --git a/backend/src/models/clean_release.py b/backend/src/models/clean_release.py index b9c458c2..ed172607 100644 --- a/backend/src/models/clean_release.py +++ b/backend/src/models/clean_release.py @@ -1,14 +1,12 @@ -# [DEF:CleanReleaseModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, models, lifecycle, compliance, evidence, immutability -# @PURPOSE: Define canonical clean release domain entities and lifecycle guards. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> MappingModels -# @PRE: Base mapping model and release enums are available. -# @POST: Provides SQLAlchemy and dataclass definitions for governance domain. -# @SIDE_EFFECT: None (schema definition). -# @DATA_CONTRACT: Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport] -# @INVARIANT: Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected. +# #region CleanReleaseModels [C:3] [TYPE Module] [SEMANTICS clean-release, models, lifecycle, compliance, evidence, immutability] +# @BRIEF Define canonical clean release domain entities and lifecycle guards. +# @LAYER Domain +# @PRE Base mapping model and release enums are available. +# @POST Provides SQLAlchemy and dataclass definitions for governance domain. +# @SIDE_EFFECT None (schema definition). +# @DATA_CONTRACT Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport] +# @INVARIANT Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected. +# @RELATION DEPENDS_ON -> [MappingModels] from datetime import datetime from dataclasses import dataclass @@ -25,65 +23,65 @@ from ..services.clean_release.enums import ( ) from ..services.clean_release.exceptions import IllegalTransitionError -# [DEF:ExecutionMode:Class] -# @PURPOSE: Backward-compatible execution mode enum for legacy TUI/orchestrator tests. +# #region ExecutionMode [TYPE Class] +# @BRIEF Backward-compatible execution mode enum for legacy TUI/orchestrator tests. class ExecutionMode(str, Enum): TUI = "TUI" API = "API" SCHEDULER = "SCHEDULER" -# [/DEF:ExecutionMode:Class] +# #endregion ExecutionMode -# [DEF:CheckFinalStatus:Class] -# @PURPOSE: Backward-compatible final status enum for legacy TUI/orchestrator tests. +# #region CheckFinalStatus [TYPE Class] +# @BRIEF Backward-compatible final status enum for legacy TUI/orchestrator tests. class CheckFinalStatus(str, Enum): COMPLIANT = "COMPLIANT" BLOCKED = "BLOCKED" FAILED = "FAILED" RUNNING = "RUNNING" -# [/DEF:CheckFinalStatus:Class] +# #endregion CheckFinalStatus -# [DEF:CheckStageName:Class] -# @PURPOSE: Backward-compatible stage name enum for legacy TUI/orchestrator tests. +# #region CheckStageName [TYPE Class] +# @BRIEF Backward-compatible stage name enum for legacy TUI/orchestrator tests. class CheckStageName(str, Enum): DATA_PURITY = "DATA_PURITY" INTERNAL_SOURCES_ONLY = "INTERNAL_SOURCES_ONLY" NO_EXTERNAL_ENDPOINTS = "NO_EXTERNAL_ENDPOINTS" MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY" -# [/DEF:CheckStageName:Class] +# #endregion CheckStageName -# [DEF:CheckStageStatus:Class] -# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests. +# #region CheckStageStatus [TYPE Class] +# @BRIEF Backward-compatible stage status enum for legacy TUI/orchestrator tests. class CheckStageStatus(str, Enum): PASS = "PASS" FAIL = "FAIL" SKIPPED = "SKIPPED" RUNNING = "RUNNING" -# [/DEF:CheckStageStatus:Class] +# #endregion CheckStageStatus -# [DEF:CheckStageResult:Class] -# @PURPOSE: Backward-compatible stage result container for legacy TUI/orchestrator tests. +# #region CheckStageResult [TYPE Class] +# @BRIEF Backward-compatible stage result container for legacy TUI/orchestrator tests. @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class CheckStageResult: stage: CheckStageName status: CheckStageStatus details: str = "" -# [/DEF:CheckStageResult:Class] +# #endregion CheckStageResult -# [DEF:ProfileType:Class] -# @PURPOSE: Backward-compatible profile enum for legacy TUI bootstrap logic. +# #region ProfileType [TYPE Class] +# @BRIEF Backward-compatible profile enum for legacy TUI bootstrap logic. class ProfileType(str, Enum): ENTERPRISE_CLEAN = "enterprise-clean" -# [/DEF:ProfileType:Class] +# #endregion ProfileType -# [DEF:RegistryStatus:Class] -# @PURPOSE: Backward-compatible registry status enum for legacy TUI bootstrap logic. +# #region RegistryStatus [TYPE Class] +# @BRIEF Backward-compatible registry status enum for legacy TUI bootstrap logic. class RegistryStatus(str, Enum): ACTIVE = "ACTIVE" INACTIVE = "INACTIVE" -# [/DEF:RegistryStatus:Class] +# #endregion RegistryStatus -# [DEF:ReleaseCandidateStatus:Class] -# @PURPOSE: Backward-compatible release candidate status enum for legacy TUI. +# #region ReleaseCandidateStatus [TYPE Class] +# @BRIEF Backward-compatible release candidate status enum for legacy TUI. class ReleaseCandidateStatus(str, Enum): DRAFT = CandidateStatus.DRAFT.value PREPARED = CandidateStatus.PREPARED.value @@ -97,10 +95,10 @@ class ReleaseCandidateStatus(str, Enum): APPROVED = CandidateStatus.APPROVED.value PUBLISHED = CandidateStatus.PUBLISHED.value REVOKED = CandidateStatus.REVOKED.value -# [/DEF:ReleaseCandidateStatus:Class] +# #endregion ReleaseCandidateStatus -# [DEF:ResourceSourceEntry:Class] -# @PURPOSE: Backward-compatible source entry model for legacy TUI bootstrap logic. +# #region ResourceSourceEntry [TYPE Class] +# @BRIEF Backward-compatible source entry model for legacy TUI bootstrap logic. @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class ResourceSourceEntry: source_id: str @@ -108,10 +106,10 @@ class ResourceSourceEntry: protocol: str purpose: str enabled: bool = True -# [/DEF:ResourceSourceEntry:Class] +# #endregion ResourceSourceEntry -# [DEF:ResourceSourceRegistry:Class] -# @PURPOSE: Backward-compatible source registry model for legacy TUI bootstrap logic. +# #region ResourceSourceRegistry [TYPE Class] +# @BRIEF Backward-compatible source registry model for legacy TUI bootstrap logic. @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class ResourceSourceRegistry: registry_id: str @@ -139,10 +137,10 @@ class ResourceSourceRegistry: @property def id(self) -> str: return self.registry_id -# [/DEF:ResourceSourceRegistry:Class] +# #endregion ResourceSourceRegistry -# [DEF:CleanProfilePolicy:Class] -# @PURPOSE: Backward-compatible policy model for legacy TUI bootstrap logic. +# #region CleanProfilePolicy [TYPE Class] +# @BRIEF Backward-compatible policy model for legacy TUI bootstrap logic. @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class CleanProfilePolicy: policy_id: str @@ -180,10 +178,10 @@ class CleanProfilePolicy: @property def registry_snapshot_id(self) -> str: return self.internal_source_registry_ref -# [/DEF:CleanProfilePolicy:Class] +# #endregion CleanProfilePolicy -# [DEF:ComplianceCheckRun:Class] -# @PURPOSE: Backward-compatible run model for legacy TUI typing/import compatibility. +# #region ComplianceCheckRun [TYPE Class] +# @BRIEF Backward-compatible run model for legacy TUI typing/import compatibility. @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class ComplianceCheckRun: check_run_id: str @@ -227,12 +225,12 @@ class ComplianceCheckRun: if self.final_status == CheckFinalStatus.BLOCKED: return RunStatus.FAILED return RunStatus.SUCCEEDED -# [/DEF:ComplianceCheckRun:Class] +# #endregion ComplianceCheckRun -# [DEF:ReleaseCandidate:Class] -# @PURPOSE: Represents the release unit being prepared and governed. -# @PRE: id, version, source_snapshot_ref are non-empty. -# @POST: status advances only through legal transitions. +# #region ReleaseCandidate [TYPE Class] +# @BRIEF Represents the release unit being prepared and governed. +# @PRE id, version, source_snapshot_ref are non-empty. +# @POST status advances only through legal transitions. class ReleaseCandidate(Base): __tablename__ = "clean_release_candidates" @@ -291,10 +289,10 @@ class ReleaseCandidate(Base): if new_status not in allowed.get(current_status, []): raise IllegalTransitionError(f"Forbidden transition from {current_status} to {new_status}") self.status = new_status.value -# [/DEF:ReleaseCandidate:Class] +# #endregion ReleaseCandidate -# [DEF:CandidateArtifact:Class] -# @PURPOSE: Represents one artifact associated with a release candidate. +# #region CandidateArtifact [TYPE Class] +# @BRIEF Represents one artifact associated with a release candidate. class CandidateArtifact(Base): __tablename__ = "clean_release_artifacts" @@ -308,9 +306,9 @@ class CandidateArtifact(Base): source_uri = Column(String, nullable=True) source_host = Column(String, nullable=True) metadata_json = Column(JSON, default=dict) -# [/DEF:CandidateArtifact:Class] +# #endregion CandidateArtifact -# [DEF:ManifestItem:Class] +# #region ManifestItem [TYPE Class] @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class ManifestItem: path: str @@ -318,19 +316,19 @@ class ManifestItem: classification: ClassificationType reason: str checksum: Optional[str] = None -# [/DEF:ManifestItem:Class] +# #endregion ManifestItem -# [DEF:ManifestSummary:Class] +# #region ManifestSummary [TYPE Class] @pydantic_dataclass(config=ConfigDict(validate_assignment=True)) class ManifestSummary: included_count: int excluded_count: int prohibited_detected_count: int -# [/DEF:ManifestSummary:Class] +# #endregion ManifestSummary -# [DEF:DistributionManifest:Class] -# @PURPOSE: Immutable snapshot of the candidate payload. -# @INVARIANT: Immutable after creation. +# #region DistributionManifest [TYPE Class] +# @BRIEF Immutable snapshot of the candidate payload. +# @INVARIANT Immutable after creation. class DistributionManifest(Base): __tablename__ = "clean_release_manifests" @@ -414,10 +412,10 @@ class DistributionManifest(Base): excluded_count=int(payload.get("excluded_count", 0)), prohibited_detected_count=int(payload.get("prohibited_detected_count", 0)), ) -# [/DEF:DistributionManifest:Class] +# #endregion DistributionManifest -# [DEF:SourceRegistrySnapshot:Class] -# @PURPOSE: Immutable registry snapshot for allowed sources. +# #region SourceRegistrySnapshot [TYPE Class] +# @BRIEF Immutable registry snapshot for allowed sources. class SourceRegistrySnapshot(Base): __tablename__ = "clean_release_registry_snapshots" @@ -429,10 +427,10 @@ class SourceRegistrySnapshot(Base): allowed_schemes = Column(JSON, nullable=False) # List[str] allowed_source_types = Column(JSON, nullable=False) # List[str] immutable = Column(Boolean, default=True) -# [/DEF:SourceRegistrySnapshot:Class] +# #endregion SourceRegistrySnapshot -# [DEF:CleanPolicySnapshot:Class] -# @PURPOSE: Immutable policy snapshot used to evaluate a run. +# #region CleanPolicySnapshot [TYPE Class] +# @BRIEF Immutable policy snapshot used to evaluate a run. class CleanPolicySnapshot(Base): __tablename__ = "clean_release_policy_snapshots" @@ -443,10 +441,10 @@ class CleanPolicySnapshot(Base): content_json = Column(JSON, nullable=False) registry_snapshot_id = Column(String, ForeignKey("clean_release_registry_snapshots.id"), nullable=False) immutable = Column(Boolean, default=True) -# [/DEF:CleanPolicySnapshot:Class] +# #endregion CleanPolicySnapshot -# [DEF:ComplianceRun:Class] -# @PURPOSE: Operational record for one compliance execution. +# #region ComplianceRun [TYPE Class] +# @BRIEF Operational record for one compliance execution. class ComplianceRun(Base): __tablename__ = "clean_release_compliance_runs" @@ -468,10 +466,10 @@ class ComplianceRun(Base): @property def check_run_id(self) -> str: return self.id -# [/DEF:ComplianceRun:Class] +# #endregion ComplianceRun -# [DEF:ComplianceStageRun:Class] -# @PURPOSE: Stage-level execution record inside a run. +# #region ComplianceStageRun [TYPE Class] +# @BRIEF Stage-level execution record inside a run. class ComplianceStageRun(Base): __tablename__ = "clean_release_compliance_stage_runs" @@ -483,28 +481,28 @@ class ComplianceStageRun(Base): finished_at = Column(DateTime, nullable=True) decision = Column(String, nullable=True) # ComplianceDecision details_json = Column(JSON, default=dict) -# [/DEF:ComplianceStageRun:Class] +# #endregion ComplianceStageRun -# [DEF:ViolationSeverity:Class] -# @PURPOSE: Backward-compatible violation severity enum for legacy clean-release tests. +# #region ViolationSeverity [TYPE Class] +# @BRIEF Backward-compatible violation severity enum for legacy clean-release tests. class ViolationSeverity(str, Enum): CRITICAL = "CRITICAL" MAJOR = "MAJOR" MINOR = "MINOR" -# [/DEF:ViolationSeverity:Class] +# #endregion ViolationSeverity -# [DEF:ViolationCategory:Class] -# @PURPOSE: Backward-compatible violation category enum for legacy clean-release tests. +# #region ViolationCategory [TYPE Class] +# @BRIEF Backward-compatible violation category enum for legacy clean-release tests. class ViolationCategory(str, Enum): DATA_PURITY = "DATA_PURITY" EXTERNAL_SOURCE = "EXTERNAL_SOURCE" SOURCE_ISOLATION = "SOURCE_ISOLATION" MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY" EXTERNAL_ENDPOINT = "EXTERNAL_ENDPOINT" -# [/DEF:ViolationCategory:Class] +# #endregion ViolationCategory -# [DEF:ComplianceViolation:Class] -# @PURPOSE: Violation produced by a stage. +# #region ComplianceViolation [TYPE Class] +# @BRIEF Violation produced by a stage. class ComplianceViolation(Base): __tablename__ = "clean_release_compliance_violations" @@ -577,11 +575,11 @@ class ComplianceViolation(Base): @property def blocked_release(self) -> bool: return bool((self.evidence_json or {}).get("blocked_release", False)) -# [/DEF:ComplianceViolation:Class] +# #endregion ComplianceViolation -# [DEF:ComplianceReport:Class] -# @PURPOSE: Immutable result derived from a completed run. -# @INVARIANT: Immutable after creation. +# #region ComplianceReport [TYPE Class] +# @BRIEF Immutable result derived from a completed run. +# @INVARIANT Immutable after creation. class ComplianceReport(Base): __tablename__ = "clean_release_compliance_reports" @@ -651,10 +649,10 @@ class ComplianceReport(Base): @property def blocking_violations_count(self) -> int: return int((self.summary_json or {}).get("blocking_violations_count", 0)) -# [/DEF:ComplianceReport:Class] +# #endregion ComplianceReport -# [DEF:ApprovalDecision:Class] -# @PURPOSE: Approval or rejection bound to a candidate and report. +# #region ApprovalDecision [TYPE Class] +# @BRIEF Approval or rejection bound to a candidate and report. class ApprovalDecision(Base): __tablename__ = "clean_release_approval_decisions" @@ -665,10 +663,10 @@ class ApprovalDecision(Base): decided_by = Column(String, nullable=False) decided_at = Column(DateTime, default=datetime.utcnow) comment = Column(String, nullable=True) -# [/DEF:ApprovalDecision:Class] +# #endregion ApprovalDecision -# [DEF:PublicationRecord:Class] -# @PURPOSE: Publication or revocation record. +# #region PublicationRecord [TYPE Class] +# @BRIEF Publication or revocation record. class PublicationRecord(Base): __tablename__ = "clean_release_publication_records" @@ -680,10 +678,10 @@ class PublicationRecord(Base): target_channel = Column(String, nullable=False) publication_ref = Column(String, nullable=True) status = Column(String, default=PublicationStatus.ACTIVE) -# [/DEF:PublicationRecord:Class] +# #endregion PublicationRecord -# [DEF:CleanReleaseAuditLog:Class] -# @PURPOSE: Represents a persistent audit log entry for clean release actions. +# #region CleanReleaseAuditLog [TYPE Class] +# @BRIEF Represents a persistent audit log entry for clean release actions. import uuid class CleanReleaseAuditLog(Base): __tablename__ = "clean_release_audit_logs" @@ -694,6 +692,6 @@ class CleanReleaseAuditLog(Base): actor = Column(String, nullable=False) timestamp = Column(DateTime, default=datetime.utcnow) details_json = Column(JSON, default=dict) -# [/DEF:CleanReleaseAuditLog:Class] +# #endregion CleanReleaseAuditLog -# [/DEF:CleanReleaseModels:Module] \ No newline at end of file +# #endregion CleanReleaseModels diff --git a/backend/src/models/config.py b/backend/src/models/config.py index 06677e34..84a56f46 100644 --- a/backend/src/models/config.py +++ b/backend/src/models/config.py @@ -1,12 +1,10 @@ -# [DEF:ConfigModels:Module] +# #region ConfigModels [C:3] [TYPE Module] [SEMANTICS database, config, settings, sqlalchemy, notification] +# @BRIEF Defines SQLAlchemy persistence models for application and notification configuration records. +# @LAYER Domain +# @INVARIANT Configuration payload and notification credentials must remain persisted as non-null JSON documents. +# @RELATION DEPENDS_ON -> [MappingModels:Base] # -# @COMPLEXITY: 3 -# @SEMANTICS: database, config, settings, sqlalchemy, notification -# @PURPOSE: Defines SQLAlchemy persistence models for application and notification configuration records. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] -> [MappingModels:Base] -# @INVARIANT: Configuration payload and notification credentials must remain persisted as non-null JSON documents. from sqlalchemy import Column, String, DateTime, JSON, Boolean from sqlalchemy.sql import func @@ -14,12 +12,12 @@ from sqlalchemy.sql import func from .mapping import Base -# [DEF:AppConfigRecord:Class] -# @PURPOSE: Stores persisted application configuration as a single authoritative record model. -# @PRE: SQLAlchemy declarative Base is initialized and table metadata registration is active. -# @POST: ORM table 'app_configurations' exposes id, payload, and updated_at fields with declared nullability/default semantics. -# @SIDE_EFFECT: Registers ORM mapping metadata during module import. -# @DATA_CONTRACT: Input -> persistence row {id:str, payload:json, updated_at:datetime}; Output -> AppConfigRecord ORM entity. +# #region AppConfigRecord [TYPE Class] +# @BRIEF Stores persisted application configuration as a single authoritative record model. +# @PRE SQLAlchemy declarative Base is initialized and table metadata registration is active. +# @POST ORM table 'app_configurations' exposes id, payload, and updated_at fields with declared nullability/default semantics. +# @SIDE_EFFECT Registers ORM mapping metadata during module import. +# @DATA_CONTRACT Input -> persistence row {id:str, payload:json, updated_at:datetime}; Output -> AppConfigRecord ORM entity. class AppConfigRecord(Base): __tablename__ = "app_configurations" @@ -28,14 +26,14 @@ class AppConfigRecord(Base): updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) -# [/DEF:AppConfigRecord:Class] +# #endregion AppConfigRecord -# [DEF:NotificationConfig:Class] -# @PURPOSE: Stores persisted provider-level notification configuration and encrypted credentials metadata. -# @PRE: SQLAlchemy declarative Base is initialized and uuid generation is available at instance creation time. -# @POST: ORM table 'notification_configs' exposes id, type, name, credentials, is_active, created_at, updated_at fields with declared constraints/defaults. -# @SIDE_EFFECT: Registers ORM mapping metadata during module import; may generate UUID values for new entity instances. -# @DATA_CONTRACT: Input -> persistence row {id:str, type:str, name:str, credentials:json, is_active:bool, created_at:datetime, updated_at:datetime}; Output -> NotificationConfig ORM entity. +# #region NotificationConfig [TYPE Class] +# @BRIEF Stores persisted provider-level notification configuration and encrypted credentials metadata. +# @PRE SQLAlchemy declarative Base is initialized and uuid generation is available at instance creation time. +# @POST ORM table 'notification_configs' exposes id, type, name, credentials, is_active, created_at, updated_at fields with declared constraints/defaults. +# @SIDE_EFFECT Registers ORM mapping metadata during module import; may generate UUID values for new entity instances. +# @DATA_CONTRACT Input -> persistence row {id:str, type:str, name:str, credentials:json, is_active:bool, created_at:datetime, updated_at:datetime}; Output -> NotificationConfig ORM entity. class NotificationConfig(Base): __tablename__ = "notification_configs" @@ -46,8 +44,8 @@ class NotificationConfig(Base): is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) -# [/DEF:NotificationConfig:Class] +# #endregion NotificationConfig import uuid -# [/DEF:ConfigModels:Module] +# #endregion ConfigModels diff --git a/backend/src/models/connection.py b/backend/src/models/connection.py index bb3f45d6..a358dc4a 100644 --- a/backend/src/models/connection.py +++ b/backend/src/models/connection.py @@ -1,12 +1,10 @@ -# [DEF:ConnectionModels:Module] +# #region ConnectionModels [C:1] [TYPE Module] [SEMANTICS database, connection, configuration, sqlalchemy, sqlite] +# @BRIEF Defines the database schema for external database connection configurations. +# @LAYER Domain +# @INVARIANT All primary keys are UUID strings. +# @RELATION DEPENDS_ON -> [sqlalchemy] # -# @COMPLEXITY: 1 -# @SEMANTICS: database, connection, configuration, sqlalchemy, sqlite -# @PURPOSE: Defines the database schema for external database connection configurations. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> sqlalchemy # -# @INVARIANT: All primary keys are UUID strings. # [SECTION: IMPORTS] from sqlalchemy import Column, String, Integer, DateTime @@ -15,9 +13,8 @@ from .mapping import Base import uuid # [/SECTION] -# [DEF:ConnectionConfig:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Stores credentials for external databases used for column mapping. +# #region ConnectionConfig [C:1] [TYPE Class] +# @BRIEF Stores credentials for external databases used for column mapping. class ConnectionConfig(Base): __tablename__ = "connection_configs" @@ -31,6 +28,6 @@ class ConnectionConfig(Base): password = Column(String, nullable=True) # Encrypted/Obfuscated password created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) -# [/DEF:ConnectionConfig:Class] +# #endregion ConnectionConfig -# [/DEF:ConnectionModels:Module] \ No newline at end of file +# #endregion ConnectionModels diff --git a/backend/src/models/dashboard.py b/backend/src/models/dashboard.py index 72d12105..3e7a4225 100644 --- a/backend/src/models/dashboard.py +++ b/backend/src/models/dashboard.py @@ -1,32 +1,28 @@ -# [DEF:DashboardModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: dashboard, model, metadata, migration -# @PURPOSE: Defines data models for dashboard metadata and selection. -# @LAYER: Model -# @RELATION: USED_BY -> MigrationApi +# #region DashboardModels [C:3] [TYPE Module] [SEMANTICS dashboard, model, metadata, migration] +# @BRIEF Defines data models for dashboard metadata and selection. +# @LAYER Model +# @RELATION USED_BY -> [MigrationApi] from pydantic import BaseModel from typing import List -# [DEF:DashboardMetadata:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents a dashboard available for migration. +# #region DashboardMetadata [C:1] [TYPE Class] +# @BRIEF Represents a dashboard available for migration. class DashboardMetadata(BaseModel): id: int title: str last_modified: str status: str -# [/DEF:DashboardMetadata:Class] +# #endregion DashboardMetadata -# [DEF:DashboardSelection:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents the user's selection of dashboards to migrate. +# #region DashboardSelection [C:1] [TYPE Class] +# @BRIEF Represents the user's selection of dashboards to migrate. class DashboardSelection(BaseModel): selected_ids: List[int] source_env_id: str target_env_id: str replace_db_config: bool = False fix_cross_filters: bool = True -# [/DEF:DashboardSelection:Class] +# #endregion DashboardSelection -# [/DEF:DashboardModels:Module] \ No newline at end of file +# #endregion DashboardModels diff --git a/backend/src/models/dataset_review.py b/backend/src/models/dataset_review.py index 873cbb85..aa6e0696 100644 --- a/backend/src/models/dataset_review.py +++ b/backend/src/models/dataset_review.py @@ -1,20 +1,18 @@ -# [DEF:DatasetReviewModels:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: dataset_review, session, profile, findings, semantics, clarification, execution, sqlalchemy -# @PURPOSE: Thin facade re-exporting all dataset review domain models from the decomposed sub-package. -# @LAYER: Domain -# @RELATION: EXPORTS -> [DatasetReviewEnums:Module] -# @RELATION: EXPORTS -> [DatasetReviewSessionModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewProfileModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewFindingModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewSemanticModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewFilterModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewMappingModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewClarificationModels:Module] -# @RELATION: EXPORTS -> [DatasetReviewExecutionModels:Module] -# @INVARIANT: All public model classes and enums remain importable from `src.models.dataset_review` without changes. -# @RATIONALE: Original 984-line monolith violated INV_7 (400-line module limit). Decomposed into domain-focused sub-modules while preserving backward-compatible import paths. -# @REJECTED: Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk. +# #region DatasetReviewModels [C:2] [TYPE Module] [SEMANTICS dataset_review, session, profile, findings, semantics, clarification, execution, sqlalchemy] +# @BRIEF Thin facade re-exporting all dataset review domain models from the decomposed sub-package. +# @LAYER Domain +# @INVARIANT All public model classes and enums remain importable from `src.models.dataset_review` without changes. +# @RELATION EXPORTS -> [DatasetReviewEnums:Module] +# @RELATION EXPORTS -> [DatasetReviewSessionModels:Module] +# @RELATION EXPORTS -> [DatasetReviewProfileModels:Module] +# @RELATION EXPORTS -> [DatasetReviewFindingModels:Module] +# @RELATION EXPORTS -> [DatasetReviewSemanticModels:Module] +# @RELATION EXPORTS -> [DatasetReviewFilterModels:Module] +# @RELATION EXPORTS -> [DatasetReviewMappingModels:Module] +# @RELATION EXPORTS -> [DatasetReviewClarificationModels:Module] +# @RELATION EXPORTS -> [DatasetReviewExecutionModels:Module] +# @RATIONALE Original 984-line monolith violated INV_7 (400-line module limit). Decomposed into domain-focused sub-modules while preserving backward-compatible import paths. +# @REJECTED Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk. from src.models.dataset_review_pkg._enums import ( # noqa: F401 SessionStatus, @@ -78,4 +76,4 @@ from src.models.dataset_review_pkg._execution_models import ( # noqa: F401 SessionEvent, ExportArtifact, ) -# [/DEF:DatasetReviewModels:Module] +# #endregion DatasetReviewModels diff --git a/backend/src/models/dataset_review_pkg/__init__.py b/backend/src/models/dataset_review_pkg/__init__.py index a5824e7e..fa6a627c 100644 --- a/backend/src/models/dataset_review_pkg/__init__.py +++ b/backend/src/models/dataset_review_pkg/__init__.py @@ -1,8 +1,6 @@ -# [DEF:DatasetReviewModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: dataset_review, session, profile, findings, semantics, clarification, execution, sqlalchemy -# @PURPOSE: Re-export all dataset review domain models from decomposed sub-modules for backward-compatible imports. -# @LAYER: Domain +# #region DatasetReviewModels [C:3] [TYPE Module] [SEMANTICS dataset_review, session, profile, findings, semantics, clarification, execution, sqlalchemy] +# @BRIEF Re-export all dataset review domain models from decomposed sub-modules for backward-compatible imports. +# @LAYER Domain from src.models.dataset_review_pkg._enums import ( SessionStatus, @@ -119,4 +117,4 @@ __all__ = [ "SessionEvent", "ExportArtifact", ] -# [/DEF:DatasetReviewModels:Module] +# #endregion DatasetReviewModels diff --git a/backend/src/models/dataset_review_pkg/_clarification_models.py b/backend/src/models/dataset_review_pkg/_clarification_models.py index 76e3e41b..08411b32 100644 --- a/backend/src/models/dataset_review_pkg/_clarification_models.py +++ b/backend/src/models/dataset_review_pkg/_clarification_models.py @@ -1,10 +1,9 @@ -# [DEF:DatasetReviewClarificationModels:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Clarification session, question, option, and answer models for guided review flow. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] -# @INVARIANT: Only one active clarification question may exist at a time per session. +# #region DatasetReviewClarificationModels [C:3] [TYPE Module] +# @BRIEF Clarification session, question, option, and answer models for guided review flow. +# @LAYER Domain +# @INVARIANT Only one active clarification question may exist at a time per session. +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -29,10 +28,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:ClarificationSession:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One clarification session aggregate owning questions and tracking resolution progress. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region ClarificationSession [C:2] [TYPE Class] +# @BRIEF One clarification session aggregate owning questions and tracking resolution progress. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class ClarificationSession(Base): __tablename__ = "clarification_sessions" @@ -51,13 +49,12 @@ class ClarificationSession(Base): questions = relationship("ClarificationQuestion", back_populates="clarification_session", cascade="all, delete-orphan") -# [/DEF:ClarificationSession:Class] +# #endregion ClarificationSession -# [DEF:ClarificationQuestion:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One clarification question with priority ordering, options, and state machine. -# @RELATION: DEPENDS_ON -> [ClarificationSession] +# #region ClarificationQuestion [C:2] [TYPE Class] +# @BRIEF One clarification question with priority ordering, options, and state machine. +# @RELATION DEPENDS_ON -> [ClarificationSession] class ClarificationQuestion(Base): __tablename__ = "clarification_questions" @@ -77,13 +74,12 @@ class ClarificationQuestion(Base): answer = relationship("ClarificationAnswer", back_populates="question", uselist=False, cascade="all, delete-orphan") -# [/DEF:ClarificationQuestion:Class] +# #endregion ClarificationQuestion -# [DEF:ClarificationOption:Class] -# @COMPLEXITY: 1 -# @PURPOSE: One selectable option for a clarification question with recommendation flag. -# @RELATION: DEPENDS_ON -> [ClarificationQuestion] +# #region ClarificationOption [C:1] [TYPE Class] +# @BRIEF One selectable option for a clarification question with recommendation flag. +# @RELATION DEPENDS_ON -> [ClarificationQuestion] class ClarificationOption(Base): __tablename__ = "clarification_options" @@ -97,13 +93,12 @@ class ClarificationOption(Base): question = relationship("ClarificationQuestion", back_populates="options") -# [/DEF:ClarificationOption:Class] +# #endregion ClarificationOption -# [DEF:ClarificationAnswer:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One persisted clarification answer with impact summary and feedback tracking. -# @RELATION: DEPENDS_ON -> [ClarificationQuestion] +# #region ClarificationAnswer [C:2] [TYPE Class] +# @BRIEF One persisted clarification answer with impact summary and feedback tracking. +# @RELATION DEPENDS_ON -> [ClarificationQuestion] class ClarificationAnswer(Base): __tablename__ = "clarification_answers" @@ -119,7 +114,7 @@ class ClarificationAnswer(Base): question = relationship("ClarificationQuestion", back_populates="answer") -# [/DEF:ClarificationAnswer:Class] +# #endregion ClarificationAnswer -# [/DEF:DatasetReviewClarificationModels:Module] +# #endregion DatasetReviewClarificationModels diff --git a/backend/src/models/dataset_review_pkg/_enums.py b/backend/src/models/dataset_review_pkg/_enums.py index 839dc8d4..a0063acc 100644 --- a/backend/src/models/dataset_review_pkg/_enums.py +++ b/backend/src/models/dataset_review_pkg/_enums.py @@ -1,15 +1,13 @@ -# [DEF:DatasetReviewEnums:Module] -# @COMPLEXITY: 2 -# @PURPOSE: All enumeration types for the dataset review domain, grouped for stable cross-module reuse. -# @LAYER: Domain -# @INVARIANT: Enum values are string-based for JSON serialization compatibility. +# #region DatasetReviewEnums [C:2] [TYPE Module] +# @BRIEF All enumeration types for the dataset review domain, grouped for stable cross-module reuse. +# @LAYER Domain +# @INVARIANT Enum values are string-based for JSON serialization compatibility. import enum -# [DEF:SessionStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status of a dataset review session. +# #region SessionStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status of a dataset review session. class SessionStatus(str, enum.Enum): ACTIVE = "active" PAUSED = "paused" @@ -18,12 +16,11 @@ class SessionStatus(str, enum.Enum): CANCELLED = "cancelled" -# [/DEF:SessionStatus:Class] +# #endregion SessionStatus -# [DEF:SessionPhase:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Ordered phase progression for dataset review orchestration. +# #region SessionPhase [C:1] [TYPE Class] +# @BRIEF Ordered phase progression for dataset review orchestration. class SessionPhase(str, enum.Enum): INTAKE = "intake" RECOVERY = "recovery" @@ -36,12 +33,11 @@ class SessionPhase(str, enum.Enum): POST_RUN = "post_run" -# [/DEF:SessionPhase:Class] +# #endregion SessionPhase -# [DEF:ReadinessState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Granular readiness indicator driving the recommended-action UX flow. +# #region ReadinessState [C:1] [TYPE Class] +# @BRIEF Granular readiness indicator driving the recommended-action UX flow. class ReadinessState(str, enum.Enum): EMPTY = "empty" IMPORTING = "importing" @@ -58,12 +54,11 @@ class ReadinessState(str, enum.Enum): RECOVERY_REQUIRED = "recovery_required" -# [/DEF:ReadinessState:Class] +# #endregion ReadinessState -# [DEF:RecommendedAction:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Next-action guidance derived from the current readiness state. +# #region RecommendedAction [C:1] [TYPE Class] +# @BRIEF Next-action guidance derived from the current readiness state. class RecommendedAction(str, enum.Enum): IMPORT_FROM_SUPERSET = "import_from_superset" REVIEW_DOCUMENTATION = "review_documentation" @@ -78,24 +73,22 @@ class RecommendedAction(str, enum.Enum): EXPORT_OUTPUTS = "export_outputs" -# [/DEF:RecommendedAction:Class] +# #endregion RecommendedAction -# [DEF:SessionCollaboratorRole:Class] -# @COMPLEXITY: 1 -# @PURPOSE: RBAC role for session collaborators. +# #region SessionCollaboratorRole [C:1] [TYPE Class] +# @BRIEF RBAC role for session collaborators. class SessionCollaboratorRole(str, enum.Enum): VIEWER = "viewer" REVIEWER = "reviewer" APPROVER = "approver" -# [/DEF:SessionCollaboratorRole:Class] +# #endregion SessionCollaboratorRole -# [DEF:BusinessSummarySource:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Provenance of the dataset business summary text. +# #region BusinessSummarySource [C:1] [TYPE Class] +# @BRIEF Provenance of the dataset business summary text. class BusinessSummarySource(str, enum.Enum): CONFIRMED = "confirmed" IMPORTED = "imported" @@ -104,12 +97,11 @@ class BusinessSummarySource(str, enum.Enum): MANUAL_OVERRIDE = "manual_override" -# [/DEF:BusinessSummarySource:Class] +# #endregion BusinessSummarySource -# [DEF:ConfidenceState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Confidence level for dataset profile completeness. +# #region ConfidenceState [C:1] [TYPE Class] +# @BRIEF Confidence level for dataset profile completeness. class ConfidenceState(str, enum.Enum): CONFIRMED = "confirmed" MOSTLY_CONFIRMED = "mostly_confirmed" @@ -118,12 +110,11 @@ class ConfidenceState(str, enum.Enum): UNRESOLVED = "unresolved" -# [/DEF:ConfidenceState:Class] +# #endregion ConfidenceState -# [DEF:FindingArea:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Domain area classification for validation findings. +# #region FindingArea [C:1] [TYPE Class] +# @BRIEF Domain area classification for validation findings. class FindingArea(str, enum.Enum): SOURCE_INTAKE = "source_intake" DATASET_PROFILE = "dataset_profile" @@ -136,24 +127,22 @@ class FindingArea(str, enum.Enum): AUDIT = "audit" -# [/DEF:FindingArea:Class] +# #endregion FindingArea -# [DEF:FindingSeverity:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Severity classification for validation findings. +# #region FindingSeverity [C:1] [TYPE Class] +# @BRIEF Severity classification for validation findings. class FindingSeverity(str, enum.Enum): BLOCKING = "blocking" WARNING = "warning" INFORMATIONAL = "informational" -# [/DEF:FindingSeverity:Class] +# #endregion FindingSeverity -# [DEF:ResolutionState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Resolution status for validation findings and clarification items. +# #region ResolutionState [C:1] [TYPE Class] +# @BRIEF Resolution status for validation findings and clarification items. class ResolutionState(str, enum.Enum): OPEN = "open" RESOLVED = "resolved" @@ -163,12 +152,11 @@ class ResolutionState(str, enum.Enum): EXPERT_REVIEW = "expert_review" -# [/DEF:ResolutionState:Class] +# #endregion ResolutionState -# [DEF:SemanticSourceType:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Classification of semantic enrichment source origins. +# #region SemanticSourceType [C:1] [TYPE Class] +# @BRIEF Classification of semantic enrichment source origins. class SemanticSourceType(str, enum.Enum): UPLOADED_FILE = "uploaded_file" CONNECTED_DICTIONARY = "connected_dictionary" @@ -177,12 +165,11 @@ class SemanticSourceType(str, enum.Enum): AI_GENERATED = "ai_generated" -# [/DEF:SemanticSourceType:Class] +# #endregion SemanticSourceType -# [DEF:TrustLevel:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Trust classification for semantic source reliability. +# #region TrustLevel [C:1] [TYPE Class] +# @BRIEF Trust classification for semantic source reliability. class TrustLevel(str, enum.Enum): TRUSTED = "trusted" RECOMMENDED = "recommended" @@ -190,12 +177,11 @@ class TrustLevel(str, enum.Enum): GENERATED = "generated" -# [/DEF:TrustLevel:Class] +# #endregion TrustLevel -# [DEF:SemanticSourceStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status for semantic source application. +# #region SemanticSourceStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status for semantic source application. class SemanticSourceStatus(str, enum.Enum): AVAILABLE = "available" SELECTED = "selected" @@ -205,12 +191,11 @@ class SemanticSourceStatus(str, enum.Enum): FAILED = "failed" -# [/DEF:SemanticSourceStatus:Class] +# #endregion SemanticSourceStatus -# [DEF:FieldKind:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Kind classification for semantic field entries. +# #region FieldKind [C:1] [TYPE Class] +# @BRIEF Kind classification for semantic field entries. class FieldKind(str, enum.Enum): COLUMN = "column" METRIC = "metric" @@ -218,12 +203,11 @@ class FieldKind(str, enum.Enum): PARAMETER = "parameter" -# [/DEF:FieldKind:Class] +# #endregion FieldKind -# [DEF:FieldProvenance:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Provenance tracking for semantic field value origin. +# #region FieldProvenance [C:1] [TYPE Class] +# @BRIEF Provenance tracking for semantic field value origin. class FieldProvenance(str, enum.Enum): DICTIONARY_EXACT = "dictionary_exact" REFERENCE_IMPORTED = "reference_imported" @@ -233,12 +217,11 @@ class FieldProvenance(str, enum.Enum): UNRESOLVED = "unresolved" -# [/DEF:FieldProvenance:Class] +# #endregion FieldProvenance -# [DEF:CandidateMatchType:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Match type classification for semantic candidates. +# #region CandidateMatchType [C:1] [TYPE Class] +# @BRIEF Match type classification for semantic candidates. class CandidateMatchType(str, enum.Enum): EXACT = "exact" REFERENCE = "reference" @@ -246,12 +229,11 @@ class CandidateMatchType(str, enum.Enum): GENERATED = "generated" -# [/DEF:CandidateMatchType:Class] +# #endregion CandidateMatchType -# [DEF:CandidateStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status for semantic candidate proposals. +# #region CandidateStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status for semantic candidate proposals. class CandidateStatus(str, enum.Enum): PROPOSED = "proposed" ACCEPTED = "accepted" @@ -259,12 +241,11 @@ class CandidateStatus(str, enum.Enum): SUPERSEDED = "superseded" -# [/DEF:CandidateStatus:Class] +# #endregion CandidateStatus -# [DEF:FilterSource:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Origin classification for imported filters. +# #region FilterSource [C:1] [TYPE Class] +# @BRIEF Origin classification for imported filters. class FilterSource(str, enum.Enum): SUPERSET_NATIVE = "superset_native" SUPERSET_URL = "superset_url" @@ -274,12 +255,11 @@ class FilterSource(str, enum.Enum): INFERRED = "inferred" -# [/DEF:FilterSource:Class] +# #endregion FilterSource -# [DEF:FilterConfidenceState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Confidence classification for imported filter values. +# #region FilterConfidenceState [C:1] [TYPE Class] +# @BRIEF Confidence classification for imported filter values. class FilterConfidenceState(str, enum.Enum): CONFIRMED = "confirmed" IMPORTED = "imported" @@ -288,12 +268,11 @@ class FilterConfidenceState(str, enum.Enum): UNRESOLVED = "unresolved" -# [/DEF:FilterConfidenceState:Class] +# #endregion FilterConfidenceState -# [DEF:FilterRecoveryStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Recovery quality status for imported filters. +# #region FilterRecoveryStatus [C:1] [TYPE Class] +# @BRIEF Recovery quality status for imported filters. class FilterRecoveryStatus(str, enum.Enum): RECOVERED = "recovered" PARTIAL = "partial" @@ -301,12 +280,11 @@ class FilterRecoveryStatus(str, enum.Enum): CONFLICTED = "conflicted" -# [/DEF:FilterRecoveryStatus:Class] +# #endregion FilterRecoveryStatus -# [DEF:VariableKind:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Kind classification for template variables. +# #region VariableKind [C:1] [TYPE Class] +# @BRIEF Kind classification for template variables. class VariableKind(str, enum.Enum): NATIVE_FILTER = "native_filter" PARAMETER = "parameter" @@ -314,12 +292,11 @@ class VariableKind(str, enum.Enum): UNKNOWN = "unknown" -# [/DEF:VariableKind:Class] +# #endregion VariableKind -# [DEF:MappingStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status for template variable mapping. +# #region MappingStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status for template variable mapping. class MappingStatus(str, enum.Enum): UNMAPPED = "unmapped" PROPOSED = "proposed" @@ -328,12 +305,11 @@ class MappingStatus(str, enum.Enum): INVALID = "invalid" -# [/DEF:MappingStatus:Class] +# #endregion MappingStatus -# [DEF:MappingMethod:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Method classification for execution mapping creation. +# #region MappingMethod [C:1] [TYPE Class] +# @BRIEF Method classification for execution mapping creation. class MappingMethod(str, enum.Enum): DIRECT_MATCH = "direct_match" HEURISTIC_MATCH = "heuristic_match" @@ -341,24 +317,22 @@ class MappingMethod(str, enum.Enum): MANUAL_OVERRIDE = "manual_override" -# [/DEF:MappingMethod:Class] +# #endregion MappingMethod -# [DEF:MappingWarningLevel:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Warning severity for execution mapping quality indicators. +# #region MappingWarningLevel [C:1] [TYPE Class] +# @BRIEF Warning severity for execution mapping quality indicators. class MappingWarningLevel(str, enum.Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" -# [/DEF:MappingWarningLevel:Class] +# #endregion MappingWarningLevel -# [DEF:ApprovalState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Approval lifecycle for execution mapping gate checks. +# #region ApprovalState [C:1] [TYPE Class] +# @BRIEF Approval lifecycle for execution mapping gate checks. class ApprovalState(str, enum.Enum): PENDING = "pending" APPROVED = "approved" @@ -366,12 +340,11 @@ class ApprovalState(str, enum.Enum): NOT_REQUIRED = "not_required" -# [/DEF:ApprovalState:Class] +# #endregion ApprovalState -# [DEF:ClarificationStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status for clarification sessions. +# #region ClarificationStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status for clarification sessions. class ClarificationStatus(str, enum.Enum): PENDING = "pending" ACTIVE = "active" @@ -380,12 +353,11 @@ class ClarificationStatus(str, enum.Enum): CANCELLED = "cancelled" -# [/DEF:ClarificationStatus:Class] +# #endregion ClarificationStatus -# [DEF:QuestionState:Class] -# @COMPLEXITY: 1 -# @PURPOSE: State machine for individual clarification questions. +# #region QuestionState [C:1] [TYPE Class] +# @BRIEF State machine for individual clarification questions. class QuestionState(str, enum.Enum): OPEN = "open" ANSWERED = "answered" @@ -394,12 +366,11 @@ class QuestionState(str, enum.Enum): SUPERSEDED = "superseded" -# [/DEF:QuestionState:Class] +# #endregion QuestionState -# [DEF:AnswerKind:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Classification of clarification answer types. +# #region AnswerKind [C:1] [TYPE Class] +# @BRIEF Classification of clarification answer types. class AnswerKind(str, enum.Enum): SELECTED = "selected" CUSTOM = "custom" @@ -407,12 +378,11 @@ class AnswerKind(str, enum.Enum): EXPERT_REVIEW = "expert_review" -# [/DEF:AnswerKind:Class] +# #endregion AnswerKind -# [DEF:PreviewStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Lifecycle status for compiled SQL previews. +# #region PreviewStatus [C:1] [TYPE Class] +# @BRIEF Lifecycle status for compiled SQL previews. class PreviewStatus(str, enum.Enum): PENDING = "pending" READY = "ready" @@ -420,36 +390,33 @@ class PreviewStatus(str, enum.Enum): STALE = "stale" -# [/DEF:PreviewStatus:Class] +# #endregion PreviewStatus -# [DEF:LaunchStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Outcome status for dataset launch handoff. +# #region LaunchStatus [C:1] [TYPE Class] +# @BRIEF Outcome status for dataset launch handoff. class LaunchStatus(str, enum.Enum): STARTED = "started" SUCCESS = "success" FAILED = "failed" -# [/DEF:LaunchStatus:Class] +# #endregion LaunchStatus -# [DEF:ArtifactType:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Type classification for export artifacts. +# #region ArtifactType [C:1] [TYPE Class] +# @BRIEF Type classification for export artifacts. class ArtifactType(str, enum.Enum): DOCUMENTATION = "documentation" VALIDATION_REPORT = "validation_report" RUN_SUMMARY = "run_summary" -# [/DEF:ArtifactType:Class] +# #endregion ArtifactType -# [DEF:ArtifactFormat:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Format classification for export artifact output. +# #region ArtifactFormat [C:1] [TYPE Class] +# @BRIEF Format classification for export artifact output. class ArtifactFormat(str, enum.Enum): JSON = "json" MARKDOWN = "markdown" @@ -457,7 +424,7 @@ class ArtifactFormat(str, enum.Enum): PDF = "pdf" -# [/DEF:ArtifactFormat:Class] +# #endregion ArtifactFormat -# [/DEF:DatasetReviewEnums:Module] +# #endregion DatasetReviewEnums diff --git a/backend/src/models/dataset_review_pkg/_execution_models.py b/backend/src/models/dataset_review_pkg/_execution_models.py index 4e2b6301..8c50f928 100644 --- a/backend/src/models/dataset_review_pkg/_execution_models.py +++ b/backend/src/models/dataset_review_pkg/_execution_models.py @@ -1,9 +1,8 @@ -# [DEF:DatasetReviewExecutionModels:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Compiled preview, run context, session event, and export artifact models for execution and audit. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] +# #region DatasetReviewExecutionModels [C:3] [TYPE Module] +# @BRIEF Compiled preview, run context, session event, and export artifact models for execution and audit. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -28,10 +27,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:CompiledPreview:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One compiled SQL preview snapshot with fingerprint for staleness detection. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region CompiledPreview [C:2] [TYPE Class] +# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class CompiledPreview(Base): __tablename__ = "compiled_previews" @@ -53,13 +51,12 @@ class CompiledPreview(Base): session = relationship("DatasetReviewSession", back_populates="previews") -# [/DEF:CompiledPreview:Class] +# #endregion CompiledPreview -# [DEF:DatasetRunContext:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region DatasetRunContext [C:2] [TYPE Class] +# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class DatasetRunContext(Base): __tablename__ = "dataset_run_contexts" @@ -83,13 +80,12 @@ class DatasetRunContext(Base): session = relationship("DatasetReviewSession", back_populates="run_contexts") -# [/DEF:DatasetRunContext:Class] +# #endregion DatasetRunContext -# [DEF:SessionEvent:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One persisted audit event for dataset review session mutations. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region SessionEvent [C:2] [TYPE Class] +# @BRIEF One persisted audit event for dataset review session mutations. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class SessionEvent(Base): __tablename__ = "session_events" @@ -111,13 +107,12 @@ class SessionEvent(Base): actor = relationship("User") -# [/DEF:SessionEvent:Class] +# #endregion SessionEvent -# [DEF:ExportArtifact:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One persisted export artifact reference for documentation and validation outputs. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region ExportArtifact [C:2] [TYPE Class] +# @BRIEF One persisted export artifact reference for documentation and validation outputs. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class ExportArtifact(Base): __tablename__ = "export_artifacts" @@ -134,7 +129,7 @@ class ExportArtifact(Base): session = relationship("DatasetReviewSession", back_populates="export_artifacts") -# [/DEF:ExportArtifact:Class] +# #endregion ExportArtifact -# [/DEF:DatasetReviewExecutionModels:Module] +# #endregion DatasetReviewExecutionModels diff --git a/backend/src/models/dataset_review_pkg/_filter_models.py b/backend/src/models/dataset_review_pkg/_filter_models.py index bf255f8c..220f8c04 100644 --- a/backend/src/models/dataset_review_pkg/_filter_models.py +++ b/backend/src/models/dataset_review_pkg/_filter_models.py @@ -1,9 +1,8 @@ -# [DEF:DatasetReviewFilterModels:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Imported filter and template variable models for Superset context recovery and execution mapping. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] +# #region DatasetReviewFilterModels [C:3] [TYPE Module] +# @BRIEF Imported filter and template variable models for Superset context recovery and execution mapping. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -30,10 +29,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:ImportedFilter:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Recovered Superset filter with confidence and recovery status tracking. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region ImportedFilter [C:2] [TYPE Class] +# @BRIEF Recovered Superset filter with confidence and recovery status tracking. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class ImportedFilter(Base): __tablename__ = "imported_filters" @@ -59,13 +57,12 @@ class ImportedFilter(Base): session = relationship("DatasetReviewSession", back_populates="imported_filters") -# [/DEF:ImportedFilter:Class] +# #endregion ImportedFilter -# [DEF:TemplateVariable:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Discovered template variable from dataset SQL with mapping status tracking. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region TemplateVariable [C:2] [TYPE Class] +# @BRIEF Discovered template variable from dataset SQL with mapping status tracking. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class TemplateVariable(Base): __tablename__ = "template_variables" @@ -89,7 +86,7 @@ class TemplateVariable(Base): session = relationship("DatasetReviewSession", back_populates="template_variables") -# [/DEF:TemplateVariable:Class] +# #endregion TemplateVariable -# [/DEF:DatasetReviewFilterModels:Module] +# #endregion DatasetReviewFilterModels diff --git a/backend/src/models/dataset_review_pkg/_finding_models.py b/backend/src/models/dataset_review_pkg/_finding_models.py index 53d8f9bd..23a61672 100644 --- a/backend/src/models/dataset_review_pkg/_finding_models.py +++ b/backend/src/models/dataset_review_pkg/_finding_models.py @@ -1,9 +1,8 @@ -# [DEF:DatasetReviewFindingModels:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Validation finding model for tracking blocking, warning, and informational issues during review. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] +# #region DatasetReviewFindingModels [C:2] [TYPE Module] +# @BRIEF Validation finding model for tracking blocking, warning, and informational issues during review. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -26,10 +25,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:ValidationFinding:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Structured finding record for dataset review validation issues with resolution tracking. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region ValidationFinding [C:2] [TYPE Class] +# @BRIEF Structured finding record for dataset review validation issues with resolution tracking. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class ValidationFinding(Base): __tablename__ = "validation_findings" @@ -53,7 +51,7 @@ class ValidationFinding(Base): session = relationship("DatasetReviewSession", back_populates="findings") -# [/DEF:ValidationFinding:Class] +# #endregion ValidationFinding -# [/DEF:DatasetReviewFindingModels:Module] +# #endregion DatasetReviewFindingModels diff --git a/backend/src/models/dataset_review_pkg/_mapping_models.py b/backend/src/models/dataset_review_pkg/_mapping_models.py index 8a100e65..6fccadb7 100644 --- a/backend/src/models/dataset_review_pkg/_mapping_models.py +++ b/backend/src/models/dataset_review_pkg/_mapping_models.py @@ -1,9 +1,8 @@ -# [DEF:DatasetReviewMappingModels:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Execution mapping model linking imported filters to template variables with approval gates. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] +# #region DatasetReviewMappingModels [C:2] [TYPE Module] +# @BRIEF Execution mapping model linking imported filters to template variables with approval gates. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -28,11 +27,10 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:ExecutionMapping:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One filter-to-variable mapping with approval gate, effective value, and transformation metadata. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] -# @INVARIANT: Explicit approval is required before launch when requires_explicit_approval is true. +# #region ExecutionMapping [C:2] [TYPE Class] +# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata. +# @INVARIANT Explicit approval is required before launch when requires_explicit_approval is true. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class ExecutionMapping(Base): __tablename__ = "execution_mappings" @@ -55,7 +53,7 @@ class ExecutionMapping(Base): session = relationship("DatasetReviewSession", back_populates="execution_mappings") -# [/DEF:ExecutionMapping:Class] +# #endregion ExecutionMapping -# [/DEF:DatasetReviewMappingModels:Module] +# #endregion DatasetReviewMappingModels diff --git a/backend/src/models/dataset_review_pkg/_profile_models.py b/backend/src/models/dataset_review_pkg/_profile_models.py index f978837a..5035dfbf 100644 --- a/backend/src/models/dataset_review_pkg/_profile_models.py +++ b/backend/src/models/dataset_review_pkg/_profile_models.py @@ -1,9 +1,8 @@ -# [DEF:DatasetReviewProfileModels:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Dataset profile model capturing business summary, confidence, and completeness metadata. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] +# #region DatasetReviewProfileModels [C:2] [TYPE Module] +# @BRIEF Dataset profile model capturing business summary, confidence, and completeness metadata. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -27,10 +26,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:DatasetProfile:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region DatasetProfile [C:2] [TYPE Class] +# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class DatasetProfile(Base): __tablename__ = "dataset_profiles" @@ -62,7 +60,7 @@ class DatasetProfile(Base): session = relationship("DatasetReviewSession", back_populates="profile") -# [/DEF:DatasetProfile:Class] +# #endregion DatasetProfile -# [/DEF:DatasetReviewProfileModels:Module] +# #endregion DatasetReviewProfileModels diff --git a/backend/src/models/dataset_review_pkg/_semantic_models.py b/backend/src/models/dataset_review_pkg/_semantic_models.py index e4d97f93..0d0e737d 100644 --- a/backend/src/models/dataset_review_pkg/_semantic_models.py +++ b/backend/src/models/dataset_review_pkg/_semantic_models.py @@ -1,10 +1,9 @@ -# [DEF:DatasetReviewSemanticModels:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] -# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values. +# #region DatasetReviewSemanticModels [C:3] [TYPE Module] +# @BRIEF Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment. +# @LAYER Domain +# @INVARIANT Manual overrides are never silently replaced by imported, inferred, or AI-generated values. +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -34,10 +33,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:SemanticSource:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Registered semantic enrichment source with trust level and application status. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region SemanticSource [C:2] [TYPE Class] +# @BRIEF Registered semantic enrichment source with trust level and application status. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class SemanticSource(Base): __tablename__ = "semantic_sources" @@ -61,15 +59,14 @@ class SemanticSource(Base): session = relationship("DatasetReviewSession", back_populates="semantic_sources") -# [/DEF:SemanticSource:Class] +# #endregion SemanticSource -# [DEF:SemanticFieldEntry:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Per-field semantic metadata entry with provenance tracking, lock state, and candidate set. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] -# @RELATION: DEPENDS_ON -> [SemanticCandidate] -# @INVARIANT: Locked fields preserve their active value regardless of later candidate proposals. +# #region SemanticFieldEntry [C:3] [TYPE Class] +# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set. +# @INVARIANT Locked fields preserve their active value regardless of later candidate proposals. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] +# @RELATION DEPENDS_ON -> [SemanticCandidate] class SemanticFieldEntry(Base): __tablename__ = "semantic_field_entries" @@ -104,13 +101,12 @@ class SemanticFieldEntry(Base): ) -# [/DEF:SemanticFieldEntry:Class] +# #endregion SemanticFieldEntry -# [DEF:SemanticCandidate:Class] -# @COMPLEXITY: 2 -# @PURPOSE: One proposed semantic value for a field entry, ranked by match type and confidence. -# @RELATION: DEPENDS_ON -> [SemanticFieldEntry] +# #region SemanticCandidate [C:2] [TYPE Class] +# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence. +# @RELATION DEPENDS_ON -> [SemanticFieldEntry] class SemanticCandidate(Base): __tablename__ = "semantic_candidates" @@ -133,7 +129,7 @@ class SemanticCandidate(Base): field = relationship("SemanticFieldEntry", back_populates="candidates") -# [/DEF:SemanticCandidate:Class] +# #endregion SemanticCandidate -# [/DEF:DatasetReviewSemanticModels:Module] +# #endregion DatasetReviewSemanticModels diff --git a/backend/src/models/dataset_review_pkg/_session_models.py b/backend/src/models/dataset_review_pkg/_session_models.py index 2e421ca2..aea8031e 100644 --- a/backend/src/models/dataset_review_pkg/_session_models.py +++ b/backend/src/models/dataset_review_pkg/_session_models.py @@ -1,10 +1,9 @@ -# [DEF:DatasetReviewSessionModels:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Session aggregate root and collaborator models for dataset review orchestration. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module] -# @RELATION: DEPENDS_ON -> [MappingModels] -# @INVARIANT: Session and profile entities are strictly scoped to an authenticated user. +# #region DatasetReviewSessionModels [C:3] [TYPE Module] +# @BRIEF Session aggregate root and collaborator models for dataset review orchestration. +# @LAYER Domain +# @INVARIANT Session and profile entities are strictly scoped to an authenticated user. +# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] +# @RELATION DEPENDS_ON -> [MappingModels] import uuid from datetime import datetime @@ -29,10 +28,9 @@ from src.models.dataset_review_pkg._enums import ( ) -# [DEF:SessionCollaborator:Class] -# @COMPLEXITY: 2 -# @PURPOSE: RBAC collaborator record linking a user to a dataset review session with a specific role. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] +# #region SessionCollaborator [C:2] [TYPE Class] +# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] class SessionCollaborator(Base): __tablename__ = "session_collaborators" @@ -48,26 +46,25 @@ class SessionCollaborator(Base): user = relationship("User") -# [/DEF:SessionCollaborator:Class] +# #endregion SessionCollaborator -# [DEF:DatasetReviewSession:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions. -# @RELATION: DEPENDS_ON -> [SessionCollaborator] -# @RELATION: DEPENDS_ON -> [DatasetProfile] -# @RELATION: DEPENDS_ON -> [ValidationFinding] -# @RELATION: DEPENDS_ON -> [SemanticSource] -# @RELATION: DEPENDS_ON -> [SemanticFieldEntry] -# @RELATION: DEPENDS_ON -> [ImportedFilter] -# @RELATION: DEPENDS_ON -> [TemplateVariable] -# @RELATION: DEPENDS_ON -> [ExecutionMapping] -# @RELATION: DEPENDS_ON -> [ClarificationSession] -# @RELATION: DEPENDS_ON -> [CompiledPreview] -# @RELATION: DEPENDS_ON -> [DatasetRunContext] -# @RELATION: DEPENDS_ON -> [ExportArtifact] -# @RELATION: DEPENDS_ON -> [SessionEvent] -# @INVARIANT: Optimistic-lock version column prevents lost-update races on concurrent mutations. +# #region DatasetReviewSession [C:3] [TYPE Class] +# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions. +# @INVARIANT Optimistic-lock version column prevents lost-update races on concurrent mutations. +# @RELATION DEPENDS_ON -> [SessionCollaborator] +# @RELATION DEPENDS_ON -> [DatasetProfile] +# @RELATION DEPENDS_ON -> [ValidationFinding] +# @RELATION DEPENDS_ON -> [SemanticSource] +# @RELATION DEPENDS_ON -> [SemanticFieldEntry] +# @RELATION DEPENDS_ON -> [ImportedFilter] +# @RELATION DEPENDS_ON -> [TemplateVariable] +# @RELATION DEPENDS_ON -> [ExecutionMapping] +# @RELATION DEPENDS_ON -> [ClarificationSession] +# @RELATION DEPENDS_ON -> [CompiledPreview] +# @RELATION DEPENDS_ON -> [DatasetRunContext] +# @RELATION DEPENDS_ON -> [ExportArtifact] +# @RELATION DEPENDS_ON -> [SessionEvent] class DatasetReviewSession(Base): __tablename__ = "dataset_review_sessions" @@ -150,7 +147,7 @@ class DatasetReviewSession(Base): ) -# [/DEF:DatasetReviewSession:Class] +# #endregion DatasetReviewSession -# [/DEF:DatasetReviewSessionModels:Module] +# #endregion DatasetReviewSessionModels diff --git a/backend/src/models/filter_state.py b/backend/src/models/filter_state.py index 707b0292..932c642d 100644 --- a/backend/src/models/filter_state.py +++ b/backend/src/models/filter_state.py @@ -1,10 +1,8 @@ -# [DEF:FilterStateModels:Module] +# #region FilterStateModels [C:2] [TYPE Module] [SEMANTICS superset, native, filters, pydantic, models, dataclasses] +# @BRIEF Pydantic models for Superset native filter state extraction and restoration. +# @LAYER Models +# @RELATION DEPENDS_ON -> [pydantic] # -# @COMPLEXITY: 2 -# @SEMANTICS: superset, native, filters, pydantic, models, dataclasses -# @PURPOSE: Pydantic models for Superset native filter state extraction and restoration. -# @LAYER: Models -# @RELATION: [DEPENDS_ON] ->[pydantic] # [SECTION: IMPORTS] from typing import Any, Dict, List, Optional @@ -12,10 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field # [/SECTION] -# [DEF:FilterState:Model] -# @COMPLEXITY: 2 -# @PURPOSE: Represents the state of a single native filter. -# @DATA_CONTRACT: Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState] +# #region FilterState [C:2] [TYPE Model] +# @BRIEF Represents the state of a single native filter. +# @DATA_CONTRACT Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState] class FilterState(BaseModel): """Single native filter state with extraFormData, filterState, and ownState.""" @@ -24,13 +21,12 @@ class FilterState(BaseModel): extraFormData: Dict[str, Any] = Field(default_factory=dict, description="Extra form data for the filter") filterState: Dict[str, Any] = Field(default_factory=dict, description="Current filter state") ownState: Dict[str, Any] = Field(default_factory=dict, description="Own state of the filter") -# [/DEF:FilterState:Model] +# #endregion FilterState -# [DEF:NativeFilterDataMask:Model] -# @COMPLEXITY: 2 -# @PURPOSE: Represents the dataMask containing all native filter states. -# @DATA_CONTRACT: Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask] +# #region NativeFilterDataMask [C:2] [TYPE Model] +# @BRIEF Represents the dataMask containing all native filter states. +# @DATA_CONTRACT Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask] class NativeFilterDataMask(BaseModel): """Container for all native filter states in a dashboard.""" @@ -48,13 +44,12 @@ class NativeFilterDataMask(BaseModel): if filter_state: return filter_state.extraFormData return {} -# [/DEF:NativeFilterDataMask:Model] +# #endregion NativeFilterDataMask -# [DEF:ParsedNativeFilters:Model] -# @COMPLEXITY: 2 -# @PURPOSE: Result of parsing native filters from permalink or native_filters_key. -# @DATA_CONTRACT: Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters] +# #region ParsedNativeFilters [C:2] [TYPE Model] +# @BRIEF Result of parsing native filters from permalink or native_filters_key. +# @DATA_CONTRACT Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters] class ParsedNativeFilters(BaseModel): """Result of extracting native filters from a Superset URL.""" @@ -76,13 +71,12 @@ class ParsedNativeFilters(BaseModel): def get_filter_count(self) -> int: """Get the number of filters extracted.""" return len(self.dataMask) -# [/DEF:ParsedNativeFilters:Model] +# #endregion ParsedNativeFilters -# [DEF:DashboardURLFilterExtraction:Model] -# @COMPLEXITY: 2 -# @PURPOSE: Result of parsing a complete dashboard URL for filter information. -# @DATA_CONTRACT: Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction] +# #region DashboardURLFilterExtraction [C:2] [TYPE Model] +# @BRIEF Result of parsing a complete dashboard URL for filter information. +# @DATA_CONTRACT Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction] class DashboardURLFilterExtraction(BaseModel): """Result of parsing a Superset dashboard URL to extract filter state.""" @@ -94,13 +88,12 @@ class DashboardURLFilterExtraction(BaseModel): filters: ParsedNativeFilters = Field(default_factory=ParsedNativeFilters, description="Extracted filter data") success: bool = Field(default=True, description="Whether extraction was successful") error: Optional[str] = Field(default=None, description="Error message if extraction failed") -# [/DEF:DashboardURLFilterExtraction:Model] +# #endregion DashboardURLFilterExtraction -# [DEF:ExtraFormDataMerge:Model] -# @COMPLEXITY: 2 -# @PURPOSE: Configuration for merging extraFormData from different sources. -# @DATA_CONTRACT: Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge] +# #region ExtraFormDataMerge [C:2] [TYPE Model] +# @BRIEF Configuration for merging extraFormData from different sources. +# @DATA_CONTRACT Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge] class ExtraFormDataMerge(BaseModel): """Configuration for merging extraFormData between original and new filter values.""" @@ -145,7 +138,7 @@ class ExtraFormDataMerge(BaseModel): result[key] = new_value return result -# [/DEF:ExtraFormDataMerge:Model] +# #endregion ExtraFormDataMerge -# [/DEF:FilterStateModels:Module] \ No newline at end of file +# #endregion FilterStateModels diff --git a/backend/src/models/git.py b/backend/src/models/git.py index f30a6008..b87e1527 100644 --- a/backend/src/models/git.py +++ b/backend/src/models/git.py @@ -1,4 +1,4 @@ -# [DEF:GitModels:Module] +# #region GitModels [TYPE Module] import enum from datetime import datetime @@ -21,9 +21,8 @@ class SyncStatus(str, enum.Enum): DIRTY = "DIRTY" CONFLICT = "CONFLICT" -# [DEF:GitServerConfig:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Configuration for a Git server connection. +# #region GitServerConfig [C:1] [TYPE Class] +# @BRIEF Configuration for a Git server connection. class GitServerConfig(Base): __tablename__ = "git_server_configs" @@ -36,11 +35,10 @@ class GitServerConfig(Base): default_branch = Column(String(255), default="main") status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN) last_validated = Column(DateTime, default=datetime.utcnow) -# [/DEF:GitServerConfig:Class] +# #endregion GitServerConfig -# [DEF:GitRepository:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Tracking for a local Git repository linked to a dashboard. +# #region GitRepository [C:1] [TYPE Class] +# @BRIEF Tracking for a local Git repository linked to a dashboard. class GitRepository(Base): __tablename__ = "git_repositories" @@ -51,11 +49,10 @@ class GitRepository(Base): local_path = Column(String(255), nullable=False) current_branch = Column(String(255), default="dev") sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN) -# [/DEF:GitRepository:Class] +# #endregion GitRepository -# [DEF:DeploymentEnvironment:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Target Superset environments for dashboard deployment. +# #region DeploymentEnvironment [C:1] [TYPE Class] +# @BRIEF Target Superset environments for dashboard deployment. class DeploymentEnvironment(Base): __tablename__ = "deployment_environments" @@ -64,6 +61,6 @@ class DeploymentEnvironment(Base): superset_url = Column(String(255), nullable=False) superset_token = Column(String(255), nullable=False) is_active = Column(Boolean, default=True) -# [/DEF:DeploymentEnvironment:Class] +# #endregion DeploymentEnvironment -# [/DEF:GitModels:Module] +# #endregion GitModels diff --git a/backend/src/models/llm.py b/backend/src/models/llm.py index c93d56c6..b106f17f 100644 --- a/backend/src/models/llm.py +++ b/backend/src/models/llm.py @@ -1,9 +1,7 @@ -# [DEF:LlmModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: llm, models, sqlalchemy, persistence -# @PURPOSE: SQLAlchemy models for LLM provider configuration and validation results. -# @LAYER: Domain -# @RELATION: INHERITS_FROM -> MappingModels:Base +# #region LlmModels [C:3] [TYPE Module] [SEMANTICS llm, models, sqlalchemy, persistence] +# @BRIEF SQLAlchemy models for LLM provider configuration and validation results. +# @LAYER Domain +# @RELATION INHERITS_FROM -> [MappingModels:Base] from sqlalchemy import Column, String, Boolean, DateTime, JSON, Text, Time, ForeignKey from datetime import datetime @@ -13,8 +11,8 @@ from .mapping import Base def generate_uuid(): return str(uuid.uuid4()) -# [DEF:ValidationPolicy:Class] -# @PURPOSE: Defines a scheduled rule for validating a group of dashboards within an execution window. +# #region ValidationPolicy [TYPE Class] +# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window. class ValidationPolicy(Base): __tablename__ = "validation_policies" @@ -31,10 +29,10 @@ class ValidationPolicy(Base): alert_condition = Column(String, default="FAIL_ONLY") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) -# [/DEF:ValidationPolicy:Class] +# #endregion ValidationPolicy -# [DEF:LLMProvider:Class] -# @PURPOSE: SQLAlchemy model for LLM provider configuration. +# #region LLMProvider [TYPE Class] +# @BRIEF SQLAlchemy model for LLM provider configuration. class LLMProvider(Base): __tablename__ = "llm_providers" @@ -46,10 +44,10 @@ class LLMProvider(Base): default_model = Column(String, nullable=False) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) -# [/DEF:LLMProvider:Class] +# #endregion LLMProvider -# [DEF:ValidationRecord:Class] -# @PURPOSE: SQLAlchemy model for dashboard validation history. +# #region ValidationRecord [TYPE Class] +# @BRIEF SQLAlchemy model for dashboard validation history. class ValidationRecord(Base): __tablename__ = "llm_validation_results" @@ -63,6 +61,6 @@ class ValidationRecord(Base): issues = Column(JSON, nullable=False) summary = Column(Text, nullable=False) raw_response = Column(Text, nullable=True) -# [/DEF:ValidationRecord:Class] +# #endregion ValidationRecord -# [/DEF:LlmModels:Module] \ No newline at end of file +# #endregion LlmModels diff --git a/backend/src/models/mapping.py b/backend/src/models/mapping.py index 54a4a88b..cea0e1fc 100644 --- a/backend/src/models/mapping.py +++ b/backend/src/models/mapping.py @@ -1,13 +1,11 @@ -# [DEF:MappingModels:Module] +# #region MappingModels [C:3] [TYPE Module] [SEMANTICS database, mapping, environment, migration, sqlalchemy, sqlite] +# @BRIEF Defines the database schema for environment metadata and database mappings using SQLAlchemy. +# @LAYER Domain +# @INVARIANT All primary keys are UUID strings. +# @RELATION DEPENDS_ON -> [sqlalchemy] # -# @COMPLEXITY: 3 -# @SEMANTICS: database, mapping, environment, migration, sqlalchemy, sqlite -# @PURPOSE: Defines the database schema for environment metadata and database mappings using SQLAlchemy. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> sqlalchemy # -# @INVARIANT: All primary keys are UUID strings. # @CONSTRAINT: source_env_id and target_env_id must be valid environment IDs. # [SECTION: IMPORTS] @@ -20,31 +18,28 @@ import enum Base = declarative_base() -# [DEF:ResourceType:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Enumeration of possible Superset resource types for ID mapping. +# #region ResourceType [C:1] [TYPE Class] +# @BRIEF Enumeration of possible Superset resource types for ID mapping. class ResourceType(str, enum.Enum): CHART = "chart" DATASET = "dataset" DASHBOARD = "dashboard" -# [/DEF:ResourceType:Class] +# #endregion ResourceType -# [DEF:MigrationStatus:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Enumeration of possible migration job statuses. +# #region MigrationStatus [C:1] [TYPE Class] +# @BRIEF Enumeration of possible migration job statuses. class MigrationStatus(enum.Enum): PENDING = "PENDING" RUNNING = "RUNNING" COMPLETED = "COMPLETED" FAILED = "FAILED" AWAITING_MAPPING = "AWAITING_MAPPING" -# [/DEF:MigrationStatus:Class] +# #endregion MigrationStatus -# [DEF:Environment:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Represents a Superset instance environment. -# @RELATION: DEPENDS_ON -> MappingModels +# #region Environment [C:3] [TYPE Class] +# @BRIEF Represents a Superset instance environment. +# @RELATION DEPENDS_ON -> [MappingModels] class Environment(Base): __tablename__ = "environments" @@ -52,11 +47,10 @@ class Environment(Base): name = Column(String, nullable=False) url = Column(String, nullable=False) credentials_id = Column(String, nullable=False) -# [/DEF:Environment:Class] +# #endregion Environment -# [DEF:DatabaseMapping:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Represents a mapping between source and target databases. +# #region DatabaseMapping [C:3] [TYPE Class] +# @BRIEF Represents a mapping between source and target databases. class DatabaseMapping(Base): __tablename__ = "database_mappings" @@ -68,11 +62,10 @@ class DatabaseMapping(Base): source_db_name = Column(String, nullable=False) target_db_name = Column(String, nullable=False) engine = Column(String, nullable=True) -# [/DEF:DatabaseMapping:Class] +# #endregion DatabaseMapping -# [DEF:MigrationJob:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Represents a single migration execution job. +# #region MigrationJob [C:2] [TYPE Class] +# @BRIEF Represents a single migration execution job. class MigrationJob(Base): __tablename__ = "migration_jobs" @@ -82,11 +75,10 @@ class MigrationJob(Base): status = Column(SQLEnum(MigrationStatus), default=MigrationStatus.PENDING) replace_db = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) -# [/DEF:MigrationJob:Class] +# #endregion MigrationJob -# [DEF:ResourceMapping:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Maps a universal UUID for a resource to its actual ID on a specific environment. +# #region ResourceMapping [C:3] [TYPE Class] +# @BRIEF Maps a universal UUID for a resource to its actual ID on a specific environment. # @TEST_DATA: resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'} # @RELATION: DEPENDS_ON -> MappingModels class ResourceMapping(Base): @@ -99,7 +91,6 @@ class ResourceMapping(Base): remote_integer_id = Column(String, nullable=False) # Stored as string to handle potentially large or composite IDs safely, though Superset usually uses integers. resource_name = Column(String, nullable=True) # Used for UI display last_synced_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) -# [/DEF:ResourceMapping:Class] - -# [/DEF:MappingModels:Module] +# #endregion ResourceMapping +# #endregion MappingModels diff --git a/backend/src/models/profile.py b/backend/src/models/profile.py index eeb17f21..2524b9bc 100644 --- a/backend/src/models/profile.py +++ b/backend/src/models/profile.py @@ -1,14 +1,12 @@ -# [DEF:ProfileModels:Module] +# #region ProfileModels [C:3] [TYPE Module] [SEMANTICS profile, preferences, persistence, user, dashboard-filter, git, ui-preferences, sqlalchemy] +# @BRIEF Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences. +# @LAYER Domain +# @INVARIANT Exactly one preference row exists per user_id. +# @INVARIANT Sensitive Git token is stored encrypted and never returned in plaintext. +# @RELATION DEPENDS_ON -> [AuthModels] +# @RELATION INHERITS_FROM -> [MappingModels:Base] # -# @COMPLEXITY: 3 -# @SEMANTICS: profile, preferences, persistence, user, dashboard-filter, git, ui-preferences, sqlalchemy -# @PURPOSE: Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [AuthModels] -# @RELATION: INHERITS_FROM -> [MappingModels:Base] # -# @INVARIANT: Exactly one preference row exists per user_id. -# @INVARIANT: Sensitive Git token is stored encrypted and never returned in plaintext. # [SECTION: IMPORTS] import uuid @@ -19,10 +17,9 @@ from .mapping import Base # [/SECTION] -# [DEF:UserDashboardPreference:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Stores Superset username binding and default "my dashboards" toggle for one authenticated user. -# @RELATION: INHERITS -> MappingModels:Base +# #region UserDashboardPreference [C:3] [TYPE Class] +# @BRIEF Stores Superset username binding and default "my dashboards" toggle for one authenticated user. +# @RELATION INHERITS -> [MappingModels:Base] class UserDashboardPreference(Base): __tablename__ = "user_dashboard_preferences" @@ -56,6 +53,6 @@ class UserDashboardPreference(Base): ) user = relationship("User") -# [/DEF:UserDashboardPreference:Class] +# #endregion UserDashboardPreference -# [/DEF:ProfileModels:Module] +# #endregion ProfileModels diff --git a/backend/src/models/report.py b/backend/src/models/report.py index 5490a790..e7b27e34 100644 --- a/backend/src/models/report.py +++ b/backend/src/models/report.py @@ -1,14 +1,12 @@ -# [DEF:ReportModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: reports, models, pydantic, normalization, pagination -# @PURPOSE: Canonical report schemas for unified task reporting across heterogeneous task types. -# @LAYER: Domain -# @PRE: Pydantic library and task manager models are available. -# @POST: Provides validated schemas for cross-plugin reporting and UI consumption. -# @SIDE_EFFECT: None (schema definition). -# @DATA_CONTRACT: Model[TaskReport, ReportCollection, ReportDetailView] -# @RELATION: [DEPENDS_ON] -> [TaskModels] -# @INVARIANT: Canonical report fields are always present for every report item. +# #region ReportModels [C:3] [TYPE Module] [SEMANTICS reports, models, pydantic, normalization, pagination] +# @BRIEF Canonical report schemas for unified task reporting across heterogeneous task types. +# @LAYER Domain +# @PRE Pydantic library and task manager models are available. +# @POST Provides validated schemas for cross-plugin reporting and UI consumption. +# @SIDE_EFFECT None (schema definition). +# @DATA_CONTRACT Model[TaskReport, ReportCollection, ReportDetailView] +# @INVARIANT Canonical report fields are always present for every report item. +# @RELATION DEPENDS_ON -> [TaskModels] # [SECTION: IMPORTS] from datetime import datetime @@ -19,12 +17,10 @@ from pydantic import BaseModel, Field, field_validator, model_validator # [/SECTION] -# [DEF:TaskType:Class] -# @COMPLEXITY: 3 -# @INVARIANT: Must contain valid generic task type mappings. -# @RELATION: DEPENDS_ON -> ReportModels -# @SEMANTICS: enum, type, task -# @PURPOSE: Supported normalized task report types. +# #region TaskType [C:3] [TYPE Class] [SEMANTICS enum, type, task] +# @BRIEF Supported normalized task report types. +# @INVARIANT Must contain valid generic task type mappings. +# @RELATION DEPENDS_ON -> [ReportModels] class TaskType(str, Enum): LLM_VERIFICATION = "llm_verification" BACKUP = "backup" @@ -34,15 +30,13 @@ class TaskType(str, Enum): UNKNOWN = "unknown" -# [/DEF:TaskType:Class] +# #endregion TaskType -# [DEF:ReportStatus:Class] -# @COMPLEXITY: 3 -# @INVARIANT: TaskStatus enum mapping logic holds. -# @SEMANTICS: enum, status, task -# @PURPOSE: Supported normalized report status values. -# @RELATION: DEPENDS_ON -> ReportModels +# #region ReportStatus [C:3] [TYPE Class] [SEMANTICS enum, status, task] +# @BRIEF Supported normalized report status values. +# @INVARIANT TaskStatus enum mapping logic holds. +# @RELATION DEPENDS_ON -> [ReportModels] class ReportStatus(str, Enum): SUCCESS = "success" FAILED = "failed" @@ -50,14 +44,12 @@ class ReportStatus(str, Enum): PARTIAL = "partial" -# [/DEF:ReportStatus:Class] +# #endregion ReportStatus -# [DEF:ErrorContext:Class] -# @COMPLEXITY: 3 -# @INVARIANT: The properties accurately describe error state. -# @SEMANTICS: error, context, payload -# @PURPOSE: Error and recovery context for failed/partial reports. +# #region ErrorContext [C:3] [TYPE Class] [SEMANTICS error, context, payload] +# @BRIEF Error and recovery context for failed/partial reports. +# @INVARIANT The properties accurately describe error state. # # @TEST_CONTRACT: ErrorContextModel -> # { @@ -78,14 +70,12 @@ class ErrorContext(BaseModel): next_actions: List[str] = Field(default_factory=list) -# [/DEF:ErrorContext:Class] +# #endregion ErrorContext -# [DEF:TaskReport:Class] -# @COMPLEXITY: 3 -# @INVARIANT: Must represent canonical task record attributes. -# @SEMANTICS: report, model, summary -# @PURPOSE: Canonical normalized report envelope for one task execution. +# #region TaskReport [C:3] [TYPE Class] [SEMANTICS report, model, summary] +# @BRIEF Canonical normalized report envelope for one task execution. +# @INVARIANT Must represent canonical task record attributes. # # @TEST_CONTRACT: TaskReportModel -> # { @@ -138,14 +128,12 @@ class TaskReport(BaseModel): return value.strip() -# [/DEF:TaskReport:Class] +# #endregion TaskReport -# [DEF:ReportQuery:Class] -# @COMPLEXITY: 3 -# @INVARIANT: Time and pagination queries are mutually consistent. -# @SEMANTICS: query, filter, search -# @PURPOSE: Query object for server-side report filtering, sorting, and pagination. +# #region ReportQuery [C:3] [TYPE Class] [SEMANTICS query, filter, search] +# @BRIEF Query object for server-side report filtering, sorting, and pagination. +# @INVARIANT Time and pagination queries are mutually consistent. # # @TEST_CONTRACT: ReportQueryModel -> # { @@ -199,14 +187,12 @@ class ReportQuery(BaseModel): return self -# [/DEF:ReportQuery:Class] +# #endregion ReportQuery -# [DEF:ReportCollection:Class] -# @COMPLEXITY: 3 -# @INVARIANT: Represents paginated data correctly. -# @SEMANTICS: collection, pagination -# @PURPOSE: Paginated collection of normalized task reports. +# #region ReportCollection [C:3] [TYPE Class] [SEMANTICS collection, pagination] +# @BRIEF Paginated collection of normalized task reports. +# @INVARIANT Represents paginated data correctly. # # @TEST_CONTRACT: ReportCollectionModel -> # { @@ -227,14 +213,12 @@ class ReportCollection(BaseModel): applied_filters: ReportQuery -# [/DEF:ReportCollection:Class] +# #endregion ReportCollection -# [DEF:ReportDetailView:Class] -# @COMPLEXITY: 3 -# @INVARIANT: Incorporates a report and logs correctly. -# @SEMANTICS: view, detail, logs -# @PURPOSE: Detailed report representation including diagnostics and recovery actions. +# #region ReportDetailView [C:3] [TYPE Class] [SEMANTICS view, detail, logs] +# @BRIEF Detailed report representation including diagnostics and recovery actions. +# @INVARIANT Incorporates a report and logs correctly. # # @TEST_CONTRACT: ReportDetailViewModel -> # { @@ -251,6 +235,6 @@ class ReportDetailView(BaseModel): next_actions: List[str] = Field(default_factory=list) -# [/DEF:ReportDetailView:Class] +# #endregion ReportDetailView -# [/DEF:ReportModels:Module] +# #endregion ReportModels diff --git a/backend/src/models/storage.py b/backend/src/models/storage.py index 1f223e7f..5800b2c1 100644 --- a/backend/src/models/storage.py +++ b/backend/src/models/storage.py @@ -1,21 +1,19 @@ -# [DEF:StorageModels:Module] +# #region StorageModels [TYPE Module] from datetime import datetime from enum import Enum from typing import Optional from pydantic import BaseModel, Field -# [DEF:FileCategory:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Enumeration of supported file categories in the storage system. +# #region FileCategory [C:1] [TYPE Class] +# @BRIEF Enumeration of supported file categories in the storage system. class FileCategory(str, Enum): BACKUP = "backups" REPOSITORY = "repositorys" -# [/DEF:FileCategory:Class] +# #endregion FileCategory -# [DEF:StorageConfig:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns. +# #region StorageConfig [C:1] [TYPE Class] +# @BRIEF Configuration model for the storage system, defining paths and naming patterns. class StorageConfig(BaseModel): root_path: str = Field(default="backups", description="Absolute path to the storage root directory.") backup_path: str = Field(default="backups", description="Subpath for backups.") @@ -23,11 +21,10 @@ class StorageConfig(BaseModel): backup_structure_pattern: str = Field(default="{category}/", description="Pattern for backup directory structure.") repo_structure_pattern: str = Field(default="{category}/", description="Pattern for repository directory structure.") filename_pattern: str = Field(default="{name}_{timestamp}", description="Pattern for filenames.") -# [/DEF:StorageConfig:Class] +# #endregion StorageConfig -# [DEF:StoredFile:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Data model representing metadata for a file stored in the system. +# #region StoredFile [C:1] [TYPE Class] +# @BRIEF Data model representing metadata for a file stored in the system. class StoredFile(BaseModel): name: str = Field(..., description="Name of the file (including extension).") path: str = Field(..., description="Relative path from storage root.") @@ -35,6 +32,6 @@ class StoredFile(BaseModel): created_at: datetime = Field(..., description="Creation timestamp.") category: FileCategory = Field(..., description="Category of the file.") mime_type: Optional[str] = Field(None, description="MIME type of the file.") -# [/DEF:StoredFile:Class] +# #endregion StoredFile -# [/DEF:StorageModels:Module] \ No newline at end of file +# #endregion StorageModels diff --git a/backend/src/models/task.py b/backend/src/models/task.py index c436c69f..22d9ad8d 100644 --- a/backend/src/models/task.py +++ b/backend/src/models/task.py @@ -1,12 +1,10 @@ -# [DEF:TaskModels:Module] +# #region TaskModels [C:1] [TYPE Module] [SEMANTICS database, task, record, sqlalchemy, sqlite] +# @BRIEF Defines the database schema for task execution records. +# @LAYER Domain +# @INVARIANT All primary keys are UUID strings. +# @RELATION DEPENDS_ON -> [sqlalchemy] # -# @COMPLEXITY: 1 -# @SEMANTICS: database, task, record, sqlalchemy, sqlite -# @PURPOSE: Defines the database schema for task execution records. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> sqlalchemy # -# @INVARIANT: All primary keys are UUID strings. # [SECTION: IMPORTS] from sqlalchemy import Column, String, DateTime, JSON, ForeignKey, Text, Integer, Index @@ -15,9 +13,8 @@ from .mapping import Base import uuid # [/SECTION] -# [DEF:TaskRecord:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents a persistent record of a task execution. +# #region TaskRecord [C:1] [TYPE Class] +# @BRIEF Represents a persistent record of a task execution. class TaskRecord(Base): __tablename__ = "task_records" @@ -32,13 +29,12 @@ class TaskRecord(Base): result = Column(JSON, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) params = Column(JSON, nullable=True) -# [/DEF:TaskRecord:Class] +# #endregion TaskRecord -# [DEF:TaskLogRecord:Class] -# @PURPOSE: Represents a single persistent log entry for a task. -# @COMPLEXITY: 3 -# @RELATION: DEPENDS_ON -> TaskRecord -# @INVARIANT: Each log entry belongs to exactly one task. +# #region TaskLogRecord [C:3] [TYPE Class] +# @BRIEF Represents a single persistent log entry for a task. +# @INVARIANT Each log entry belongs to exactly one task. +# @RELATION DEPENDS_ON -> [TaskRecord] # # @TEST_CONTRACT: TaskLogCreate -> # { @@ -111,6 +107,6 @@ class TaskLogRecord(Base): Index('ix_task_logs_task_level', 'task_id', 'level'), Index('ix_task_logs_task_source', 'task_id', 'source'), ) -# [/DEF:TaskLogRecord:Class] +# #endregion TaskLogRecord -# [/DEF:TaskModels:Module] \ No newline at end of file +# #endregion TaskModels diff --git a/backend/src/plugins/__init__.py b/backend/src/plugins/__init__.py index f96e7af1..d975cd22 100644 --- a/backend/src/plugins/__init__.py +++ b/backend/src/plugins/__init__.py @@ -1,3 +1,3 @@ -# [DEF:src.plugins:Package] -# @PURPOSE: Plugin package root for dynamic discovery and runtime imports. -# [/DEF:src.plugins:Package] +# #region src.plugins [TYPE Package] +# @BRIEF Plugin package root for dynamic discovery and runtime imports. +# #endregion src.plugins diff --git a/backend/src/plugins/backup.py b/backend/src/plugins/backup.py index e9353ba5..1e196f99 100755 --- a/backend/src/plugins/backup.py +++ b/backend/src/plugins/backup.py @@ -1,185 +1,184 @@ -# [DEF:BackupPlugin:Module] -# @SEMANTICS: backup, superset, automation, dashboard, plugin -# @PURPOSE: A plugin that provides functionality to back up Superset dashboards. -# @LAYER: App -# @RELATION: IMPLEMENTS -> PluginBase -# @RELATION: DEPENDS_ON -> superset_tool.client -# @RELATION: DEPENDS_ON -> superset_tool.utils -# @RELATION: USES -> TaskContext - -from typing import Dict, Any, Optional -from pathlib import Path -from requests.exceptions import RequestException - -from ..core.plugin_base import PluginBase -from ..core.logger import belief_scope, logger as app_logger -from ..core.superset_client import SupersetClient -from ..core.utils.network import SupersetAPIError -from ..core.utils.fileio import ( - save_and_unpack_dashboard, - archive_exports, - sanitize_filename, - consolidate_archive_folders, - remove_empty_directories, - RetentionPolicy -) -from ..dependencies import get_config_manager -from ..core.task_manager.context import TaskContext - -# [DEF:BackupPlugin:Class] -# @PURPOSE: Implementation of the backup plugin logic. -class BackupPlugin(PluginBase): - """ - A plugin to back up Superset dashboards. - """ - - @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the backup plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string ID. - # @RETURN: str - "superset-backup" - def id(self) -> str: - with belief_scope("id"): - return "superset-backup" - # [/DEF:id:Function] - - @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the backup plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string name. - # @RETURN: str - Plugin name. - def name(self) -> str: - with belief_scope("name"): - return "Superset Dashboard Backup" - # [/DEF:name:Function] - - @property - # [DEF:description:Function] - # @PURPOSE: Returns a description of the backup plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string description. - # @RETURN: str - Plugin description. - def description(self) -> str: - with belief_scope("description"): - return "Backs up all dashboards from a Superset instance." - # [/DEF:description:Function] - - @property - # [DEF:version:Function] - # @PURPOSE: Returns the version of the backup plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string version. - # @RETURN: str - "1.0.0" - def version(self) -> str: - with belief_scope("version"): - return "1.0.0" - # [/DEF:version:Function] - - @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the backup plugin. - # @RETURN: str - "/tools/backups" - def ui_route(self) -> str: - with belief_scope("ui_route"): - return "/tools/backups" - # [/DEF:ui_route:Function] - - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for backup plugin parameters. - # @PRE: Plugin instance exists. - # @POST: Returns dictionary schema. - # @RETURN: Dict[str, Any] - JSON schema. - def get_schema(self) -> Dict[str, Any]: - with belief_scope("get_schema"): - config_manager = get_config_manager() - envs = [e.name for e in config_manager.get_environments()] - config_manager.get_config().settings.storage.root_path - - return { - "type": "object", - "properties": { - "env": { - "type": "string", - "title": "Environment", - "description": "The Superset environment to back up.", - "enum": envs if envs else [], - }, - }, - "required": ["env"], - } - # [/DEF:get_schema:Function] - - # [DEF:execute:Function] - # @PURPOSE: Executes the dashboard backup logic with TaskContext support. - # @PARAM: params (Dict[str, Any]) - Backup parameters (env, backup_path, dashboard_ids). - # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. - # @PRE: Target environment must be configured. params must be a dictionary. - # @POST: All dashboards are exported and archived. - async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None): - with belief_scope("execute"): - config_manager = get_config_manager() - - # Support both parameter names: environment_id (for task creation) and env (for direct calls) - env_id = params.get("environment_id") or params.get("env") - dashboard_ids = params.get("dashboard_ids") or params.get("dashboards") - - # Log the incoming parameters for debugging - log = context.logger if context else app_logger - log.info(f"Backup parameters received: env_id={env_id}, dashboard_ids={dashboard_ids}") - - # Resolve environment name if environment_id is provided - if env_id: - env_config = next((e for e in config_manager.get_environments() if e.id == env_id), None) - if env_config: - params["env"] = env_config.name - - env = params.get("env") - if not env: - raise KeyError("env") - - log.info(f"Backup started for environment: {env}, selected dashboards: {dashboard_ids}") - - storage_settings = config_manager.get_config().settings.storage - # Use 'backups' subfolder within the storage root - backup_path = Path(storage_settings.root_path) / "backups" - - # Use TaskContext logger if available, otherwise fall back to app_logger - log = context.logger if context else app_logger - - # Create sub-loggers for different components - superset_log = log.with_source("superset_api") if context else log - storage_log = log.with_source("storage") if context else log - - log.info(f"Starting backup for environment: {env}") - +# #region BackupPlugin [TYPE Module] [SEMANTICS backup, superset, automation, dashboard, plugin] +# @BRIEF A plugin that provides functionality to back up Superset dashboards. +# @LAYER App +# @RELATION IMPLEMENTS -> [PluginBase] +# @RELATION DEPENDS_ON -> [superset_tool.client] +# @RELATION DEPENDS_ON -> [superset_tool.utils] +# @RELATION USES -> [TaskContext] + +from typing import Dict, Any, Optional +from pathlib import Path +from requests.exceptions import RequestException + +from ..core.plugin_base import PluginBase +from ..core.logger import belief_scope, logger as app_logger +from ..core.superset_client import SupersetClient +from ..core.utils.network import SupersetAPIError +from ..core.utils.fileio import ( + save_and_unpack_dashboard, + archive_exports, + sanitize_filename, + consolidate_archive_folders, + remove_empty_directories, + RetentionPolicy +) +from ..dependencies import get_config_manager +from ..core.task_manager.context import TaskContext + +# #region BackupPlugin [TYPE Class] +# @BRIEF Implementation of the backup plugin logic. +class BackupPlugin(PluginBase): + """ + A plugin to back up Superset dashboards. + """ + + @property + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the backup plugin. + # @PRE Plugin instance exists. + # @POST Returns string ID. + # @RETURN str - "superset-backup" + def id(self) -> str: + with belief_scope("id"): + return "superset-backup" + # #endregion id + + @property + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the backup plugin. + # @PRE Plugin instance exists. + # @POST Returns string name. + # @RETURN str - Plugin name. + def name(self) -> str: + with belief_scope("name"): + return "Superset Dashboard Backup" + # #endregion name + + @property + # #region description [TYPE Function] + # @BRIEF Returns a description of the backup plugin. + # @PRE Plugin instance exists. + # @POST Returns string description. + # @RETURN str - Plugin description. + def description(self) -> str: + with belief_scope("description"): + return "Backs up all dashboards from a Superset instance." + # #endregion description + + @property + # #region version [TYPE Function] + # @BRIEF Returns the version of the backup plugin. + # @PRE Plugin instance exists. + # @POST Returns string version. + # @RETURN str - "1.0.0" + def version(self) -> str: + with belief_scope("version"): + return "1.0.0" + # #endregion version + + @property + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the backup plugin. + # @RETURN str - "/tools/backups" + def ui_route(self) -> str: + with belief_scope("ui_route"): + return "/tools/backups" + # #endregion ui_route + + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for backup plugin parameters. + # @PRE Plugin instance exists. + # @POST Returns dictionary schema. + # @RETURN Dict[str, Any] - JSON schema. + def get_schema(self) -> Dict[str, Any]: + with belief_scope("get_schema"): + config_manager = get_config_manager() + envs = [e.name for e in config_manager.get_environments()] + config_manager.get_config().settings.storage.root_path + + return { + "type": "object", + "properties": { + "env": { + "type": "string", + "title": "Environment", + "description": "The Superset environment to back up.", + "enum": envs if envs else [], + }, + }, + "required": ["env"], + } + # #endregion get_schema + + # #region execute [TYPE Function] + # @BRIEF Executes the dashboard backup logic with TaskContext support. + # @PARAM: params (Dict[str, Any]) - Backup parameters (env, backup_path, dashboard_ids). + # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. + # @PRE: Target environment must be configured. params must be a dictionary. + # @POST: All dashboards are exported and archived. + async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None): + with belief_scope("execute"): + config_manager = get_config_manager() + + # Support both parameter names: environment_id (for task creation) and env (for direct calls) + env_id = params.get("environment_id") or params.get("env") + dashboard_ids = params.get("dashboard_ids") or params.get("dashboards") + + # Log the incoming parameters for debugging + log = context.logger if context else app_logger + log.info(f"Backup parameters received: env_id={env_id}, dashboard_ids={dashboard_ids}") + + # Resolve environment name if environment_id is provided + if env_id: + env_config = next((e for e in config_manager.get_environments() if e.id == env_id), None) + if env_config: + params["env"] = env_config.name + + env = params.get("env") + if not env: + raise KeyError("env") + + log.info(f"Backup started for environment: {env}, selected dashboards: {dashboard_ids}") + + storage_settings = config_manager.get_config().settings.storage + # Use 'backups' subfolder within the storage root + backup_path = Path(storage_settings.root_path) / "backups" + + # Use TaskContext logger if available, otherwise fall back to app_logger + log = context.logger if context else app_logger + + # Create sub-loggers for different components + superset_log = log.with_source("superset_api") if context else log + storage_log = log.with_source("storage") if context else log + + log.info(f"Starting backup for environment: {env}") + try: config_manager = get_config_manager() if not config_manager.has_environments(): raise ValueError("No Superset environments configured. Please add an environment in Settings.") - - env_config = config_manager.get_environment(env) - if not env_config: - raise ValueError(f"Environment '{env}' not found in configuration.") - - client = SupersetClient(env_config) - - # Get all dashboards - all_dashboard_count, all_dashboard_meta = client.get_dashboards() - superset_log.info(f"Found {all_dashboard_count} total dashboards in environment") - - # Filter dashboards if specific IDs are provided - if dashboard_ids: - dashboard_ids_int = [int(did) for did in dashboard_ids] - dashboard_meta = [db for db in all_dashboard_meta if db.get('id') in dashboard_ids_int] - dashboard_count = len(dashboard_meta) - superset_log.info(f"Filtered to {dashboard_count} selected dashboards: {dashboard_ids_int}") - else: - dashboard_count = all_dashboard_count - superset_log.info("No dashboard filter applied - backing up all dashboards") - dashboard_meta = all_dashboard_meta - + + env_config = config_manager.get_environment(env) + if not env_config: + raise ValueError(f"Environment '{env}' not found in configuration.") + + client = SupersetClient(env_config) + + # Get all dashboards + all_dashboard_count, all_dashboard_meta = client.get_dashboards() + superset_log.info(f"Found {all_dashboard_count} total dashboards in environment") + + # Filter dashboards if specific IDs are provided + if dashboard_ids: + dashboard_ids_int = [int(did) for did in dashboard_ids] + dashboard_meta = [db for db in all_dashboard_meta if db.get('id') in dashboard_ids_int] + dashboard_count = len(dashboard_meta) + superset_log.info(f"Filtered to {dashboard_count} selected dashboards: {dashboard_ids_int}") + else: + dashboard_count = all_dashboard_count + superset_log.info("No dashboard filter applied - backing up all dashboards") + dashboard_meta = all_dashboard_meta + if dashboard_count == 0: log.info("No dashboards to back up") return { @@ -201,26 +200,26 @@ class BackupPlugin(PluginBase): dashboard_title = db.get('dashboard_title', 'Unknown Dashboard') if not dashboard_id: continue - - # Report progress - progress_pct = (idx / total) * 100 - log.progress(f"Backing up dashboard: {dashboard_title}", percent=progress_pct) - - try: - dashboard_base_dir_name = sanitize_filename(f"{dashboard_title}") - dashboard_dir = backup_path / env.upper() / dashboard_base_dir_name - dashboard_dir.mkdir(parents=True, exist_ok=True) - - zip_content, filename = client.export_dashboard(dashboard_id) - superset_log.debug(f"Exported dashboard: {dashboard_title}") - - save_and_unpack_dashboard( - zip_content=zip_content, - original_filename=filename, - output_dir=dashboard_dir, - unpack=False - ) - + + # Report progress + progress_pct = (idx / total) * 100 + log.progress(f"Backing up dashboard: {dashboard_title}", percent=progress_pct) + + try: + dashboard_base_dir_name = sanitize_filename(f"{dashboard_title}") + dashboard_dir = backup_path / env.upper() / dashboard_base_dir_name + dashboard_dir.mkdir(parents=True, exist_ok=True) + + zip_content, filename = client.export_dashboard(dashboard_id) + superset_log.debug(f"Exported dashboard: {dashboard_title}") + + save_and_unpack_dashboard( + zip_content=zip_content, + original_filename=filename, + output_dir=dashboard_dir, + unpack=False + ) + archive_exports(str(dashboard_dir), policy=RetentionPolicy()) storage_log.debug(f"Archived dashboard: {dashboard_title}") backed_up_dashboards.append({ @@ -256,6 +255,6 @@ class BackupPlugin(PluginBase): except (RequestException, IOError, KeyError) as e: log.error(f"Fatal error during backup for {env}: {e}") raise e - # [/DEF:execute:Function] -# [/DEF:BackupPlugin:Class] -# [/DEF:BackupPlugin:Module] + # #endregion execute +# #endregion BackupPlugin +# #endregion BackupPlugin diff --git a/backend/src/plugins/debug.py b/backend/src/plugins/debug.py index 53ea701b..3c19c045 100644 --- a/backend/src/plugins/debug.py +++ b/backend/src/plugins/debug.py @@ -1,9 +1,8 @@ -# [DEF:DebugPluginModule:Module] -# @SEMANTICS: plugin, debug, api, database, superset -# @PURPOSE: Implements a plugin for system diagnostics and debugging Superset API responses. -# @LAYER: Plugins -# @RELATION: Inherits from PluginBase. Uses SupersetClient from core. -# @RELATION: USES -> TaskContext +# #region DebugPluginModule [TYPE Module] [SEMANTICS plugin, debug, api, database, superset] +# @BRIEF Implements a plugin for system diagnostics and debugging Superset API responses. +# @LAYER Plugins +# @RELATION Inherits from PluginBase. Uses SupersetClient from core. +# @RELATION USES -> [TaskContext] # [SECTION: IMPORTS] from typing import Dict, Any, Optional @@ -13,71 +12,71 @@ from ..core.logger import logger, belief_scope from ..core.task_manager.context import TaskContext # [/SECTION] -# [DEF:DebugPlugin:Class] -# @PURPOSE: Plugin for system diagnostics and debugging. +# #region DebugPlugin [TYPE Class] +# @BRIEF Plugin for system diagnostics and debugging. class DebugPlugin(PluginBase): """ Plugin for system diagnostics and debugging. """ @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the debug plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string ID. - # @RETURN: str - "system-debug" + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the debug plugin. + # @PRE Plugin instance exists. + # @POST Returns string ID. + # @RETURN str - "system-debug" def id(self) -> str: with belief_scope("id"): return "system-debug" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the debug plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string name. - # @RETURN: str - Plugin name. + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the debug plugin. + # @PRE Plugin instance exists. + # @POST Returns string name. + # @RETURN str - Plugin name. def name(self) -> str: with belief_scope("name"): return "System Debug" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns a description of the debug plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string description. - # @RETURN: str - Plugin description. + # #region description [TYPE Function] + # @BRIEF Returns a description of the debug plugin. + # @PRE Plugin instance exists. + # @POST Returns string description. + # @RETURN str - Plugin description. def description(self) -> str: with belief_scope("description"): return "Run system diagnostics and debug Superset API responses." - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the version of the debug plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string version. - # @RETURN: str - "1.0.0" + # #region version [TYPE Function] + # @BRIEF Returns the version of the debug plugin. + # @PRE Plugin instance exists. + # @POST Returns string version. + # @RETURN str - "1.0.0" def version(self) -> str: with belief_scope("version"): return "1.0.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the debug plugin. - # @RETURN: str - "/tools/debug" + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the debug plugin. + # @RETURN str - "/tools/debug" def ui_route(self) -> str: with belief_scope("ui_route"): return "/tools/debug" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for the debug plugin parameters. - # @PRE: Plugin instance exists. - # @POST: Returns dictionary schema. - # @RETURN: Dict[str, Any] - JSON schema. + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for the debug plugin parameters. + # @PRE Plugin instance exists. + # @POST Returns dictionary schema. + # @RETURN Dict[str, Any] - JSON schema. def get_schema(self) -> Dict[str, Any]: with belief_scope("get_schema"): return { @@ -112,10 +111,10 @@ class DebugPlugin(PluginBase): }, "required": ["action"] } - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:execute:Function] - # @PURPOSE: Executes the debug logic with TaskContext support. + # #region execute [TYPE Function] + # @BRIEF Executes the debug logic with TaskContext support. # @PARAM: params (Dict[str, Any]) - Debug parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: action must be provided in params. @@ -139,12 +138,12 @@ class DebugPlugin(PluginBase): else: debug_log.error(f"Unknown action: {action}") raise ValueError(f"Unknown action: {action}") - # [/DEF:execute:Function] + # #endregion execute - # [DEF:_test_db_api:Function] - # @PURPOSE: Tests database API connectivity for source and target environments. - # @PRE: source_env and target_env params exist in params. - # @POST: Returns DB counts for both envs. + # #region _test_db_api [TYPE Function] + # @BRIEF Tests database API connectivity for source and target environments. + # @PRE source_env and target_env params exist in params. + # @POST Returns DB counts for both envs. # @PARAM: params (Dict) - Plugin parameters. # @PARAM: log - Logger instance for superset_api source. # @RETURN: Dict - Comparison results. @@ -177,12 +176,12 @@ class DebugPlugin(PluginBase): } return results - # [/DEF:_test_db_api:Function] + # #endregion _test_db_api - # [DEF:_get_dataset_structure:Function] - # @PURPOSE: Retrieves the structure of a dataset. - # @PRE: env and dataset_id params exist in params. - # @POST: Returns dataset JSON structure. + # #region _get_dataset_structure [TYPE Function] + # @BRIEF Retrieves the structure of a dataset. + # @PRE env and dataset_id params exist in params. + # @POST Returns dataset JSON structure. # @PARAM: params (Dict) - Plugin parameters. # @PARAM: log - Logger instance for superset_api source. # @RETURN: Dict - Dataset structure. @@ -209,7 +208,7 @@ class DebugPlugin(PluginBase): dataset_response = client.get_dataset(dataset_id) log.debug(f"Retrieved dataset structure for {dataset_id}") return dataset_response.get('result') or {} - # [/DEF:_get_dataset_structure:Function] + # #endregion _get_dataset_structure -# [/DEF:DebugPlugin:Class] -# [/DEF:DebugPluginModule:Module] \ No newline at end of file +# #endregion DebugPlugin +# #endregion DebugPluginModule diff --git a/backend/src/plugins/git/__init__.py b/backend/src/plugins/git/__init__.py index 8c3ab6c5..98e13cba 100644 --- a/backend/src/plugins/git/__init__.py +++ b/backend/src/plugins/git/__init__.py @@ -1,3 +1,3 @@ -# [DEF:GitPluginExt:Package] -# @PURPOSE: Git plugin extension package root. -# [/DEF:GitPluginExt:Package] +# #region GitPluginExt [TYPE Package] +# @BRIEF Git plugin extension package root. +# #endregion GitPluginExt diff --git a/backend/src/plugins/git/llm_extension.py b/backend/src/plugins/git/llm_extension.py index 93cf9cfd..3526a4f7 100644 --- a/backend/src/plugins/git/llm_extension.py +++ b/backend/src/plugins/git/llm_extension.py @@ -1,9 +1,7 @@ -# [DEF:backend/src/plugins/git/llm_extension:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: git, llm, commit -# @PURPOSE: LLM-based extensions for the Git plugin, specifically for commit message generation. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [LLMClient] +# #region backend/src/plugins/git/llm_extension [C:3] [TYPE Module] [SEMANTICS git, llm, commit] +# @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [LLMClient] from typing import List from tenacity import retry, stop_after_attempt, wait_exponential @@ -11,14 +9,14 @@ from ..llm_analysis.service import LLMClient from ...core.logger import belief_scope, logger from ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt -# [DEF:GitLLMExtension:Class] -# @PURPOSE: Provides LLM capabilities to the Git plugin. +# #region GitLLMExtension [TYPE Class] +# @BRIEF Provides LLM capabilities to the Git plugin. class GitLLMExtension: def __init__(self, client: LLMClient): self.client = client - # [DEF:suggest_commit_message:Function] - # @PURPOSE: Generates a suggested commit message based on a diff and history. + # #region suggest_commit_message [TYPE Function] + # @BRIEF Generates a suggested commit message based on a diff and history. # @PARAM: diff (str) - The git diff of staged changes. # @PARAM: history (List[str]) - Recent commit messages for context. # @RETURN: str - The suggested commit message. @@ -58,7 +56,7 @@ class GitLLMExtension: return "Update dashboard configurations (LLM generation failed)" return response.choices[0].message.content.strip() - # [/DEF:suggest_commit_message:Function] -# [/DEF:GitLLMExtension:Class] + # #endregion suggest_commit_message +# #endregion GitLLMExtension -# [/DEF:backend/src/plugins/git/llm_extension:Module] +# #endregion backend/src/plugins/git/llm_extension diff --git a/backend/src/plugins/git_plugin.py b/backend/src/plugins/git_plugin.py index 1b69a931..c1dd9950 100644 --- a/backend/src/plugins/git_plugin.py +++ b/backend/src/plugins/git_plugin.py @@ -1,15 +1,14 @@ -# [DEF:GitPluginModule:Module] +# #region GitPluginModule [TYPE Module] [SEMANTICS git, plugin, dashboard, version_control, sync, deploy] +# @BRIEF Предоставляет плагин для версионирования и развертывания дашбордов Superset. +# @LAYER Plugin +# @INVARIANT Все операции с Git должны выполняться через GitService. +# @RELATION INHERITS_FROM -> [src.core.plugin_base.PluginBase] +# @RELATION USES -> [src.services.git_service.GitService] +# @RELATION USES -> [src.core.superset_client.SupersetClient] +# @RELATION USES -> [src.core.config_manager.ConfigManager] +# @RELATION USES -> [TaskContext] # -# @SEMANTICS: git, plugin, dashboard, version_control, sync, deploy -# @PURPOSE: Предоставляет плагин для версионирования и развертывания дашбордов Superset. -# @LAYER: Plugin -# @RELATION: INHERITS_FROM -> src.core.plugin_base.PluginBase -# @RELATION: USES -> src.services.git_service.GitService -# @RELATION: USES -> src.core.superset_client.SupersetClient -# @RELATION: USES -> src.core.config_manager.ConfigManager -# @RELATION: USES -> TaskContext # -# @INVARIANT: Все операции с Git должны выполняться через GitService. # @CONSTRAINT: Плагин работает только с распакованными YAML-экспортами Superset. # [SECTION: IMPORTS] @@ -32,14 +31,14 @@ from src.core.database import SessionLocal from src.core.mapping_service import IdMappingService # [/SECTION] -# [DEF:GitPlugin:Class] -# @PURPOSE: Реализация плагина Git Integration для управления версиями дашбордов. +# #region GitPlugin [TYPE Class] +# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов. class GitPlugin(PluginBase): - # [DEF:__init__:Function] - # @PURPOSE: Инициализирует плагин и его зависимости. - # @PRE: config.json exists or shared config_manager is available. - # @POST: Инициализированы git_service и config_manager. + # #region __init__ [TYPE Function] + # @BRIEF Инициализирует плагин и его зависимости. + # @PRE config.json exists or shared config_manager is available. + # @POST Инициализированы git_service и config_manager. def __init__(self): with belief_scope("GitPlugin.__init__"): log.reason("Initializing GitPlugin.") @@ -64,62 +63,62 @@ class GitPlugin(PluginBase): self.config_manager = ConfigManager(config_path) log.reflect(f"GitPlugin initialized with {config_path}") - # [/DEF:__init__:Function] + # #endregion __init__ @property - # [DEF:id:Function] - # @PURPOSE: Returns the plugin identifier. - # @PRE: GitPlugin is initialized. - # @POST: Returns 'git-integration'. + # #region id [TYPE Function] + # @BRIEF Returns the plugin identifier. + # @PRE GitPlugin is initialized. + # @POST Returns 'git-integration'. def id(self) -> str: with belief_scope("GitPlugin.id"): return "git-integration" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the plugin name. - # @PRE: GitPlugin is initialized. - # @POST: Returns the human-readable name. + # #region name [TYPE Function] + # @BRIEF Returns the plugin name. + # @PRE GitPlugin is initialized. + # @POST Returns the human-readable name. def name(self) -> str: with belief_scope("GitPlugin.name"): return "Git Integration" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns the plugin description. - # @PRE: GitPlugin is initialized. - # @POST: Returns the plugin's purpose description. + # #region description [TYPE Function] + # @BRIEF Returns the plugin description. + # @PRE GitPlugin is initialized. + # @POST Returns the plugin's purpose description. def description(self) -> str: with belief_scope("GitPlugin.description"): return "Version control for Superset dashboards" - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the plugin version. - # @PRE: GitPlugin is initialized. - # @POST: Returns the version string. + # #region version [TYPE Function] + # @BRIEF Returns the plugin version. + # @PRE GitPlugin is initialized. + # @POST Returns the version string. def version(self) -> str: with belief_scope("GitPlugin.version"): return "0.1.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the git plugin. - # @RETURN: str - "/git" + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the git plugin. + # @RETURN str - "/git" def ui_route(self) -> str: with belief_scope("GitPlugin.ui_route"): return "/git" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина. - # @PRE: GitPlugin is initialized. - # @POST: Returns a JSON schema dictionary. - # @RETURN: Dict[str, Any] - Схема параметров. + # #region get_schema [TYPE Function] + # @BRIEF Возвращает JSON-схему параметров для выполнения задач плагина. + # @PRE GitPlugin is initialized. + # @POST Returns a JSON schema dictionary. + # @RETURN Dict[str, Any] - Схема параметров. def get_schema(self) -> Dict[str, Any]: with belief_scope("GitPlugin.get_schema"): return { @@ -132,12 +131,12 @@ class GitPlugin(PluginBase): }, "required": ["operation", "dashboard_id"] } - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:initialize:Function] - # @PURPOSE: Выполняет начальную настройку плагина. - # @PRE: GitPlugin is initialized. - # @POST: Плагин готов к выполнению задач. + # #region initialize [TYPE Function] + # @BRIEF Выполняет начальную настройку плагина. + # @PRE GitPlugin is initialized. + # @POST Плагин готов к выполнению задач. async def initialize(self): with belief_scope("GitPlugin.initialize"): log.reason("Initializing Git Integration Plugin logic") @@ -399,6 +398,6 @@ class GitPlugin(PluginBase): raise ValueError("No environments configured. Please add a Superset Environment in Settings.") # [/DEF:_get_env:Function] - # [/DEF:initialize:Function] -# [/DEF:GitPlugin:Class] -# [/DEF:GitPluginModule:Module] \ No newline at end of file + # #endregion initialize +# #endregion GitPlugin +# #endregion GitPluginModule diff --git a/backend/src/plugins/llm_analysis/__init__.py b/backend/src/plugins/llm_analysis/__init__.py index 4064146c..91a5efa3 100644 --- a/backend/src/plugins/llm_analysis/__init__.py +++ b/backend/src/plugins/llm_analysis/__init__.py @@ -1,4 +1,4 @@ -# [DEF:backend/src/plugins/llm_analysis/__init__.py:Module] +# #region backend/src/plugins/llm_analysis/__init__.py [TYPE Module] """ LLM Analysis Plugin for automated dashboard validation and dataset documentation. @@ -8,4 +8,4 @@ from .plugin import DashboardValidationPlugin, DocumentationPlugin __all__ = ['DashboardValidationPlugin', 'DocumentationPlugin'] -# [/DEF:backend/src/plugins/llm_analysis/__init__.py:Module] +# #endregion backend/src/plugins/llm_analysis/__init__.py diff --git a/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py b/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py index 6e38c85c..d53c958e 100644 --- a/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py +++ b/backend/src/plugins/llm_analysis/__tests__/test_client_headers.py @@ -1,18 +1,16 @@ -# [DEF:TestClientHeaders:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, llm-client, openrouter, headers -# @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers. +# #region TestClientHeaders [C:3] [TYPE Module] [SEMANTICS tests, llm-client, openrouter, headers] +# @BRIEF Verify OpenRouter client initialization includes provider-specific headers. +# @RELATION BELONGS_TO -> [SrcRoot] from src.plugins.llm_analysis.models import LLMProviderType from src.plugins.llm_analysis.service import LLMClient -# [DEF:test_openrouter_client_includes_referer_and_title_headers:Function] -# @RELATION: BINDS_TO -> TestClientHeaders -# @PURPOSE: OpenRouter requests should carry site/app attribution headers for compatibility. -# @PRE: Client is initialized for OPENROUTER provider. -# @POST: Async client headers include Authorization, HTTP-Referer, and X-Title. +# #region test_openrouter_client_includes_referer_and_title_headers [TYPE Function] +# @BRIEF OpenRouter requests should carry site/app attribution headers for compatibility. +# @PRE Client is initialized for OPENROUTER provider. +# @POST Async client headers include Authorization, HTTP-Referer, and X-Title. +# @RELATION BINDS_TO -> [TestClientHeaders] def test_openrouter_client_includes_referer_and_title_headers(monkeypatch): monkeypatch.setenv("OPENROUTER_SITE_URL", "http://localhost:8000") monkeypatch.setenv("OPENROUTER_APP_NAME", "ss-tools-test") @@ -28,5 +26,5 @@ def test_openrouter_client_includes_referer_and_title_headers(monkeypatch): assert headers["Authorization"] == "Bearer sk-test-provider-key-123456" assert headers["HTTP-Referer"] == "http://localhost:8000" assert headers["X-Title"] == "ss-tools-test" -# [/DEF:test_openrouter_client_includes_referer_and_title_headers:Function] -# [/DEF:TestClientHeaders:Module] +# #endregion test_openrouter_client_includes_referer_and_title_headers +# #endregion TestClientHeaders diff --git a/backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py b/backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py index 93c48373..3369162c 100644 --- a/backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py +++ b/backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py @@ -1,19 +1,17 @@ -# [DEF:TestScreenshotService:Module] -# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression -# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits. +# #region TestScreenshotService [C:3] [TYPE Module] [SEMANTICS tests, screenshot-service, navigation, timeout-regression] +# @BRIEF Protect dashboard screenshot navigation from brittle networkidle waits. +# @RELATION VERIFIES -> [src.plugins.llm_analysis.service.ScreenshotService] import pytest from src.plugins.llm_analysis.service import ScreenshotService -# [DEF:test_iter_login_roots_includes_child_frames:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Login discovery must search embedded auth frames, not only the main page. -# @PRE: Page exposes child frames list. -# @POST: Returned roots include page plus child frames in order. +# #region test_iter_login_roots_includes_child_frames [TYPE Function] +# @BRIEF Login discovery must search embedded auth frames, not only the main page. +# @PRE Page exposes child frames list. +# @POST Returned roots include page plus child frames in order. +# @RELATION BINDS_TO -> [TestScreenshotService] def test_iter_login_roots_includes_child_frames(): frame_a = object() frame_b = object() @@ -25,14 +23,14 @@ def test_iter_login_roots_includes_child_frames(): assert roots == [fake_page, frame_a, frame_b] -# [/DEF:test_iter_login_roots_includes_child_frames:Function] +# #endregion test_iter_login_roots_includes_child_frames -# [DEF:test_response_looks_like_login_page_detects_login_markup:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Direct login fallback must reject responses that render the login screen again. -# @PRE: Response body contains stable login-page markers. -# @POST: Helper returns True so caller treats fallback as failed authentication. +# #region test_response_looks_like_login_page_detects_login_markup [TYPE Function] +# @BRIEF Direct login fallback must reject responses that render the login screen again. +# @PRE Response body contains stable login-page markers. +# @POST Helper returns True so caller treats fallback as failed authentication. +# @RELATION BINDS_TO -> [TestScreenshotService] def test_response_looks_like_login_page_detects_login_markup(): service = ScreenshotService(env=type("Env", (), {})()) @@ -52,14 +50,14 @@ def test_response_looks_like_login_page_detects_login_markup(): assert result is True -# [/DEF:test_response_looks_like_login_page_detects_login_markup:Function] +# #endregion test_response_looks_like_login_page_detects_login_markup -# [DEF:test_find_first_visible_locator_skips_hidden_first_match:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Locator helper must not reject a selector collection just because its first element is hidden. -# @PRE: First matched element is hidden and second matched element is visible. -# @POST: Helper returns the second visible candidate. +# #region test_find_first_visible_locator_skips_hidden_first_match [TYPE Function] +# @BRIEF Locator helper must not reject a selector collection just because its first element is hidden. +# @PRE First matched element is hidden and second matched element is visible. +# @POST Helper returns the second visible candidate. +# @RELATION BINDS_TO -> [TestScreenshotService] @pytest.mark.anyio async def test_find_first_visible_locator_skips_hidden_first_match(): class _FakeElement: @@ -93,14 +91,14 @@ async def test_find_first_visible_locator_skips_hidden_first_match(): assert result.label == "visible" -# [/DEF:test_find_first_visible_locator_skips_hidden_first_match:Function] +# #endregion test_find_first_visible_locator_skips_hidden_first_match -# [DEF:test_submit_login_via_form_post_uses_browser_context_request:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Fallback login must submit hidden fields and credentials through the context request cookie jar. -# @PRE: Login DOM exposes csrf hidden field and request context returns authenticated HTML. -# @POST: Helper returns True and request payload contains csrf_token plus credentials plus request options. +# #region test_submit_login_via_form_post_uses_browser_context_request [TYPE Function] +# @BRIEF Fallback login must submit hidden fields and credentials through the context request cookie jar. +# @PRE Login DOM exposes csrf hidden field and request context returns authenticated HTML. +# @POST Helper returns True and request payload contains csrf_token plus credentials plus request options. +# @RELATION BINDS_TO -> [TestScreenshotService] @pytest.mark.anyio async def test_submit_login_via_form_post_uses_browser_context_request(): class _FakeInput: @@ -204,14 +202,14 @@ async def test_submit_login_via_form_post_uses_browser_context_request(): ] -# [/DEF:test_submit_login_via_form_post_uses_browser_context_request:Function] +# #endregion test_submit_login_via_form_post_uses_browser_context_request -# [DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Fallback login must treat non-login 302 redirect as success without waiting for redirect target. -# @PRE: Request response is 302 with Location outside login path. -# @POST: Helper returns True. +# #region test_submit_login_via_form_post_accepts_authenticated_redirect [TYPE Function] +# @BRIEF Fallback login must treat non-login 302 redirect as success without waiting for redirect target. +# @PRE Request response is 302 with Location outside login path. +# @POST Helper returns True. +# @RELATION BINDS_TO -> [TestScreenshotService] @pytest.mark.anyio async def test_submit_login_via_form_post_accepts_authenticated_redirect(): class _FakeInput: @@ -279,14 +277,14 @@ async def test_submit_login_via_form_post_accepts_authenticated_redirect(): assert result is True -# [/DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function] +# #endregion test_submit_login_via_form_post_accepts_authenticated_redirect -# [DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Fallback login must fail when POST response still contains login form content. -# @PRE: Login DOM exposes csrf hidden field and request response renders login markup. -# @POST: Helper returns False. +# #region test_submit_login_via_form_post_rejects_login_markup_response [TYPE Function] +# @BRIEF Fallback login must fail when POST response still contains login form content. +# @PRE Login DOM exposes csrf hidden field and request response renders login markup. +# @POST Helper returns False. +# @RELATION BINDS_TO -> [TestScreenshotService] @pytest.mark.anyio async def test_submit_login_via_form_post_rejects_login_markup_response(): class _FakeInput: @@ -362,14 +360,14 @@ async def test_submit_login_via_form_post_rejects_login_markup_response(): assert result is False -# [/DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function] +# #endregion test_submit_login_via_form_post_rejects_login_markup_response -# [DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function] -# @RELATION: BINDS_TO ->[TestScreenshotService] -# @PURPOSE: Pages with unstable primary wait must retry with fallback wait strategy. -# @PRE: First page.goto call raises; second succeeds. -# @POST: Helper returns second response and attempts both wait modes in order. +# #region test_goto_resilient_falls_back_from_domcontentloaded_to_load [TYPE Function] +# @BRIEF Pages with unstable primary wait must retry with fallback wait strategy. +# @PRE First page.goto call raises; second succeeds. +# @POST Helper returns second response and attempts both wait modes in order. +# @RELATION BINDS_TO -> [TestScreenshotService] @pytest.mark.anyio async def test_goto_resilient_falls_back_from_domcontentloaded_to_load(): class _FakePage: @@ -400,5 +398,5 @@ async def test_goto_resilient_falls_back_from_domcontentloaded_to_load(): ] -# [/DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function] -# [/DEF:TestScreenshotService:Module] +# #endregion test_goto_resilient_falls_back_from_domcontentloaded_to_load +# #endregion TestScreenshotService diff --git a/backend/src/plugins/llm_analysis/__tests__/test_service.py b/backend/src/plugins/llm_analysis/__tests__/test_service.py index c975b5cd..0a032163 100644 --- a/backend/src/plugins/llm_analysis/__tests__/test_service.py +++ b/backend/src/plugins/llm_analysis/__tests__/test_service.py @@ -1,8 +1,6 @@ -# [DEF:TestService:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status -# @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results. +# #region TestService [C:3] [TYPE Module] [SEMANTICS tests, llm-analysis, fallback, provider-error, unknown-status] +# @BRIEF Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results. +# @RELATION BELONGS_TO -> [SrcRoot] import pytest @@ -10,11 +8,11 @@ from src.plugins.llm_analysis.models import LLMProviderType from src.plugins.llm_analysis.service import LLMClient -# [DEF:test_test_runtime_connection_uses_json_completion_transport:Function] -# @RELATION: BINDS_TO -> TestService -# @PURPOSE: Provider self-test must exercise the same chat completion transport as runtime analysis. -# @PRE: get_json_completion is available on initialized client. -# @POST: Self-test forwards a lightweight user message into get_json_completion and returns its payload. +# #region test_test_runtime_connection_uses_json_completion_transport [TYPE Function] +# @BRIEF Provider self-test must exercise the same chat completion transport as runtime analysis. +# @PRE get_json_completion is available on initialized client. +# @POST Self-test forwards a lightweight user message into get_json_completion and returns its payload. +# @RELATION BINDS_TO -> [TestService] @pytest.mark.anyio async def test_test_runtime_connection_uses_json_completion_transport(monkeypatch): client = LLMClient( @@ -36,14 +34,14 @@ async def test_test_runtime_connection_uses_json_completion_transport(monkeypatc assert result == {"ok": True} assert recorded["messages"][0]["role"] == "user" assert "Return exactly this JSON object" in recorded["messages"][0]["content"] -# [/DEF:test_test_runtime_connection_uses_json_completion_transport:Function] +# #endregion test_test_runtime_connection_uses_json_completion_transport -# [DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function] -# @RELATION: BINDS_TO -> TestService -# @PURPOSE: Infrastructure/provider failures must produce UNKNOWN analysis status rather than FAIL. -# @PRE: LLMClient.get_json_completion raises provider/auth exception. -# @POST: Returned payload uses status=UNKNOWN and issue severity UNKNOWN. +# #region test_analyze_dashboard_provider_error_maps_to_unknown [TYPE Function] +# @BRIEF Infrastructure/provider failures must produce UNKNOWN analysis status rather than FAIL. +# @PRE LLMClient.get_json_completion raises provider/auth exception. +# @POST Returned payload uses status=UNKNOWN and issue severity UNKNOWN. +# @RELATION BINDS_TO -> [TestService] @pytest.mark.anyio async def test_analyze_dashboard_provider_error_maps_to_unknown(monkeypatch, tmp_path): screenshot_path = tmp_path / "shot.jpg" @@ -66,5 +64,5 @@ async def test_analyze_dashboard_provider_error_maps_to_unknown(monkeypatch, tmp assert result["status"] == "UNKNOWN" assert "Failed to get response from LLM" in result["summary"] assert result["issues"][0]["severity"] == "UNKNOWN" -# [/DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function] -# [/DEF:TestService:Module] +# #endregion test_analyze_dashboard_provider_error_maps_to_unknown +# #endregion TestService diff --git a/backend/src/plugins/llm_analysis/models.py b/backend/src/plugins/llm_analysis/models.py index 31fea9e1..9b638e24 100644 --- a/backend/src/plugins/llm_analysis/models.py +++ b/backend/src/plugins/llm_analysis/models.py @@ -1,43 +1,41 @@ -# [DEF:LLMAnalysisModels:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: pydantic, models, llm -# @PURPOSE: Define Pydantic models for LLM Analysis plugin. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> pydantic -# @RELATION: DEPENDS_on -> pydantic -# @RELATION: DEPENDs_on -> pydantic +# #region LLMAnalysisModels [C:3] [TYPE Module] [SEMANTICS pydantic, models, llm] +# @BRIEF Define Pydantic models for LLM Analysis plugin. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [pydantic] +# @RELATION DEPENDS_on -> [pydantic] +# @RELATION DEPENDs_on -> [pydantic] from typing import List, Optional from pydantic import BaseModel, Field from datetime import datetime from enum import Enum -# [DEF:LLMProviderType:Class] -# @PURPOSE: Enum for supported LLM providers. +# #region LLMProviderType [TYPE Class] +# @BRIEF Enum for supported LLM providers. class LLMProviderType(str, Enum): OPENAI = "openai" OPENROUTER = "openrouter" KILO = "kilo" -# [/DEF:LLMProviderType:Class] +# #endregion LLMProviderType -# [DEF:FetchModelsRequest:Class] -# @PURPOSE: Request model to fetch available models from a provider. +# #region FetchModelsRequest [TYPE Class] +# @BRIEF Request model to fetch available models from a provider. class FetchModelsRequest(BaseModel): base_url: str = Field(description="Provider base URL (e.g. https://api.openai.com/v1)") api_key: Optional[str] = Field(None, description="Optional API key for authenticated providers") provider_type: LLMProviderType = Field(LLMProviderType.OPENAI, description="Provider type for auth headers") provider_id: Optional[str] = Field(None, description="Existing provider ID — resolves api_key from DB if not provided directly") -# [/DEF:FetchModelsRequest:Class] +# #endregion FetchModelsRequest -# [DEF:FetchModelsResponse:Class] -# @PURPOSE: Response with available model IDs from a provider. +# #region FetchModelsResponse [TYPE Class] +# @BRIEF Response with available model IDs from a provider. class FetchModelsResponse(BaseModel): models: List[str] = Field(description="List of available model IDs") total: int = Field(description="Total number of models found") -# [/DEF:FetchModelsResponse:Class] +# #endregion FetchModelsResponse -# [DEF:LLMProviderConfig:Class] -# @PURPOSE: Configuration for an LLM provider. +# #region LLMProviderConfig [TYPE Class] +# @BRIEF Configuration for an LLM provider. class LLMProviderConfig(BaseModel): id: Optional[str] = None provider_type: LLMProviderType @@ -46,27 +44,27 @@ class LLMProviderConfig(BaseModel): api_key: Optional[str] = None default_model: str is_active: bool = True -# [/DEF:LLMProviderConfig:Class] +# #endregion LLMProviderConfig -# [DEF:ValidationStatus:Class] -# @PURPOSE: Enum for dashboard validation status. +# #region ValidationStatus [TYPE Class] +# @BRIEF Enum for dashboard validation status. class ValidationStatus(str, Enum): PASS = "PASS" WARN = "WARN" FAIL = "FAIL" UNKNOWN = "UNKNOWN" -# [/DEF:ValidationStatus:Class] +# #endregion ValidationStatus -# [DEF:DetectedIssue:Class] -# @PURPOSE: Model for a single issue detected during validation. +# #region DetectedIssue [TYPE Class] +# @BRIEF Model for a single issue detected during validation. class DetectedIssue(BaseModel): severity: ValidationStatus message: str location: Optional[str] = None -# [/DEF:DetectedIssue:Class] +# #endregion DetectedIssue -# [DEF:ValidationResult:Class] -# @PURPOSE: Model for dashboard validation result. +# #region ValidationResult [TYPE Class] +# @BRIEF Model for dashboard validation result. class ValidationResult(BaseModel): id: Optional[str] = None dashboard_id: str @@ -76,6 +74,6 @@ class ValidationResult(BaseModel): issues: List[DetectedIssue] summary: str raw_response: Optional[str] = None -# [/DEF:ValidationResult:Class] +# #endregion ValidationResult -# [/DEF:LLMAnalysisModels:Module] +# #endregion LLMAnalysisModels diff --git a/backend/src/plugins/llm_analysis/plugin.py b/backend/src/plugins/llm_analysis/plugin.py index b02332b3..af502b8a 100644 --- a/backend/src/plugins/llm_analysis/plugin.py +++ b/backend/src/plugins/llm_analysis/plugin.py @@ -1,14 +1,12 @@ -# [DEF:backend/src/plugins/llm_analysis/plugin.py:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: plugin, llm, analysis, documentation -# @PURPOSE: Implements DashboardValidationPlugin and DocumentationPlugin. -# @LAYER: Domain -# @RELATION: INHERITS -> [PluginBase] -# @RELATION: CALLS -> [ScreenshotService] -# @RELATION: CALLS -> [LLMClient] -# @RELATION: CALLS -> [LLMProviderService] -# @RELATION: USES -> TaskContext -# @INVARIANT: All LLM interactions must be executed as asynchronous tasks. +# #region backend/src/plugins/llm_analysis/plugin.py [C:3] [TYPE Module] [SEMANTICS plugin, llm, analysis, documentation] +# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin. +# @LAYER Domain +# @INVARIANT All LLM interactions must be executed as asynchronous tasks. +# @RELATION INHERITS -> [PluginBase] +# @RELATION CALLS -> [ScreenshotService] +# @RELATION CALLS -> [LLMClient] +# @RELATION CALLS -> [LLMProviderService] +# @RELATION USES -> [TaskContext] from typing import Dict, Any, Optional import os @@ -31,10 +29,10 @@ from ...services.llm_prompt_templates import ( render_prompt, ) -# [DEF:_is_masked_or_invalid_api_key:Function] -# @PURPOSE: Guards against placeholder or malformed API keys in runtime. -# @PRE: value may be None. -# @POST: Returns True when value cannot be used for authenticated provider calls. +# #region _is_masked_or_invalid_api_key [TYPE Function] +# @BRIEF Guards against placeholder or malformed API keys in runtime. +# @PRE value may be None. +# @POST Returns True when value cannot be used for authenticated provider calls. def _is_masked_or_invalid_api_key(value: Optional[str]) -> bool: key = (value or "").strip() if not key: @@ -43,12 +41,12 @@ def _is_masked_or_invalid_api_key(value: Optional[str]) -> bool: return True # Most provider tokens are significantly longer; short values are almost always placeholders. return len(key) < 16 -# [/DEF:_is_masked_or_invalid_api_key:Function] +# #endregion _is_masked_or_invalid_api_key -# [DEF:_json_safe_value:Function] -# @PURPOSE: Recursively normalize payload values for JSON serialization. -# @PRE: value may be nested dict/list with datetime values. -# @POST: datetime values are converted to ISO strings. +# #region _json_safe_value [TYPE Function] +# @BRIEF Recursively normalize payload values for JSON serialization. +# @PRE value may be nested dict/list with datetime values. +# @POST datetime values are converted to ISO strings. def _json_safe_value(value: Any): if isinstance(value, datetime): return value.isoformat() @@ -57,11 +55,11 @@ def _json_safe_value(value: Any): if isinstance(value, list): return [_json_safe_value(v) for v in value] return value -# [/DEF:_json_safe_value:Function] +# #endregion _json_safe_value -# [DEF:DashboardValidationPlugin:Class] -# @PURPOSE: Plugin for automated dashboard health analysis using LLMs. -# @RELATION: IMPLEMENTS -> backend.src.core.plugin_base.PluginBase +# #region DashboardValidationPlugin [TYPE Class] +# @BRIEF Plugin for automated dashboard health analysis using LLMs. +# @RELATION IMPLEMENTS -> [backend.src.core.plugin_base.PluginBase] class DashboardValidationPlugin(PluginBase): @property def id(self) -> str: @@ -90,8 +88,8 @@ class DashboardValidationPlugin(PluginBase): "required": ["dashboard_id", "environment_id", "provider_id"] } - # [DEF:DashboardValidationPlugin.execute:Function] - # @PURPOSE: Executes the dashboard validation task with TaskContext support. + # #region DashboardValidationPlugin.execute [TYPE Function] + # @BRIEF Executes the dashboard validation task with TaskContext support. # @PARAM: params (Dict[str, Any]) - Validation parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: params contains dashboard_id, environment_id, and provider_id. @@ -319,12 +317,12 @@ class DashboardValidationPlugin(PluginBase): finally: db.close() - # [/DEF:DashboardValidationPlugin.execute:Function] -# [/DEF:DashboardValidationPlugin:Class] + # #endregion DashboardValidationPlugin.execute +# #endregion DashboardValidationPlugin -# [DEF:DocumentationPlugin:Class] -# @PURPOSE: Plugin for automated dataset documentation using LLMs. -# @RELATION: IMPLEMENTS -> backend.src.core.plugin_base.PluginBase +# #region DocumentationPlugin [TYPE Class] +# @BRIEF Plugin for automated dataset documentation using LLMs. +# @RELATION IMPLEMENTS -> [backend.src.core.plugin_base.PluginBase] class DocumentationPlugin(PluginBase): @property def id(self) -> str: @@ -353,8 +351,8 @@ class DocumentationPlugin(PluginBase): "required": ["dataset_id", "environment_id", "provider_id"] } - # [DEF:DocumentationPlugin.execute:Function] - # @PURPOSE: Executes the dataset documentation task with TaskContext support. + # #region DocumentationPlugin.execute [TYPE Function] + # @BRIEF Executes the dataset documentation task with TaskContext support. # @PARAM: params (Dict[str, Any]) - Documentation parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: params contains dataset_id, environment_id, and provider_id. @@ -475,7 +473,7 @@ class DocumentationPlugin(PluginBase): finally: db.close() - # [/DEF:DocumentationPlugin.execute:Function] -# [/DEF:DocumentationPlugin:Class] + # #endregion DocumentationPlugin.execute +# #endregion DocumentationPlugin -# [/DEF:backend/src/plugins/llm_analysis/plugin.py:Module] +# #endregion backend/src/plugins/llm_analysis/plugin.py diff --git a/backend/src/plugins/llm_analysis/scheduler.py b/backend/src/plugins/llm_analysis/scheduler.py index 992b6538..382cb271 100644 --- a/backend/src/plugins/llm_analysis/scheduler.py +++ b/backend/src/plugins/llm_analysis/scheduler.py @@ -1,16 +1,14 @@ -# [DEF:backend/src/plugins/llm_analysis/scheduler.py:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: scheduler, task, automation -# @PURPOSE: Provides helper functions to schedule LLM-based validation tasks. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [SchedulerService] +# #region backend/src/plugins/llm_analysis/scheduler.py [C:3] [TYPE Module] [SEMANTICS scheduler, task, automation] +# @BRIEF Provides helper functions to schedule LLM-based validation tasks. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [SchedulerService] from typing import Dict, Any from ...dependencies import get_task_manager, get_scheduler_service from ...core.logger import belief_scope, logger -# [DEF:schedule_dashboard_validation:Function] -# @PURPOSE: Schedules a recurring dashboard validation task. +# #region schedule_dashboard_validation [TYPE Function] +# @BRIEF Schedules a recurring dashboard validation task. # @PARAM: dashboard_id (str) - ID of the dashboard to validate. # @PARAM: cron_expression (str) - Standard cron expression for scheduling. # @PARAM: params (Dict[str, Any]) - Task parameters (environment_id, provider_id). @@ -39,10 +37,10 @@ def schedule_dashboard_validation(dashboard_id: str, cron_expression: str, param **_parse_cron(cron_expression) ) logger.info(f"Scheduled validation for dashboard {dashboard_id} with cron {cron_expression}") -# [/DEF:schedule_dashboard_validation:Function] +# #endregion schedule_dashboard_validation -# [DEF:_parse_cron:Function] -# @PURPOSE: Basic cron parser placeholder. +# #region _parse_cron [TYPE Function] +# @BRIEF Basic cron parser placeholder. # @PARAM: cron (str) - Cron expression. # @RETURN: Dict[str, str] - Parsed cron parts. def _parse_cron(cron: str) -> Dict[str, str]: @@ -57,6 +55,6 @@ def _parse_cron(cron: str) -> Dict[str, str]: "month": parts[3], "day_of_week": parts[4] } -# [/DEF:_parse_cron:Function] +# #endregion _parse_cron -# [/DEF:backend/src/plugins/llm_analysis/scheduler.py:Module] +# #endregion backend/src/plugins/llm_analysis/scheduler.py diff --git a/backend/src/plugins/llm_analysis/service.py b/backend/src/plugins/llm_analysis/service.py index 0ecbe146..738a9178 100644 --- a/backend/src/plugins/llm_analysis/service.py +++ b/backend/src/plugins/llm_analysis/service.py @@ -1,12 +1,10 @@ -# [DEF:backend/src/plugins/llm_analysis/service.py:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: service, llm, screenshot, playwright, openai -# @PURPOSE: Services for LLM interaction and dashboard screenshots. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> playwright -# @RELATION: DEPENDS_ON -> openai -# @RELATION: DEPENDS_ON -> tenacity -# @INVARIANT: Screenshots must be 1920px width and capture full page height. +# #region backend/src/plugins/llm_analysis/service.py [C:3] [TYPE Module] [SEMANTICS service, llm, screenshot, playwright, openai] +# @BRIEF Services for LLM interaction and dashboard screenshots. +# @LAYER Domain +# @INVARIANT Screenshots must be 1920px width and capture full page height. +# @RELATION DEPENDS_ON -> [playwright] +# @RELATION DEPENDS_ON -> [openai] +# @RELATION DEPENDS_ON -> [tenacity] import asyncio import base64 @@ -25,20 +23,20 @@ from ...core.logger import belief_scope, logger from ...core.config_models import Environment from ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt -# [DEF:ScreenshotService:Class] -# @PURPOSE: Handles capturing screenshots of Superset dashboards. +# #region ScreenshotService [TYPE Class] +# @BRIEF Handles capturing screenshots of Superset dashboards. class ScreenshotService: - # [DEF:ScreenshotService.__init__:Function] - # @PURPOSE: Initializes the ScreenshotService with environment configuration. - # @PRE: env is a valid Environment object. + # #region ScreenshotService.__init__ [TYPE Function] + # @BRIEF Initializes the ScreenshotService with environment configuration. + # @PRE env is a valid Environment object. def __init__(self, env: Environment): self.env = env - # [/DEF:ScreenshotService.__init__:Function] + # #endregion ScreenshotService.__init__ - # [DEF:ScreenshotService._find_first_visible_locator:Function] - # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies. - # @PRE: candidates is a non-empty list of locator-like objects. - # @POST: Returns a locator ready for interaction or None when nothing matches. + # #region ScreenshotService._find_first_visible_locator [TYPE Function] + # @BRIEF Resolve the first visible locator from multiple Playwright locator strategies. + # @PRE candidates is a non-empty list of locator-like objects. + # @POST Returns a locator ready for interaction or None when nothing matches. async def _find_first_visible_locator(self, candidates) -> Any: for locator in candidates: try: @@ -50,12 +48,12 @@ class ScreenshotService: except Exception: continue return None - # [/DEF:ScreenshotService._find_first_visible_locator:Function] + # #endregion ScreenshotService._find_first_visible_locator - # [DEF:ScreenshotService._iter_login_roots:Function] - # @PURPOSE: Enumerate page and child frames where login controls may be rendered. - # @PRE: page is a Playwright page-like object. - # @POST: Returns ordered roots starting with main page followed by frames. + # #region ScreenshotService._iter_login_roots [TYPE Function] + # @BRIEF Enumerate page and child frames where login controls may be rendered. + # @PRE page is a Playwright page-like object. + # @POST Returns ordered roots starting with main page followed by frames. def _iter_login_roots(self, page) -> List[Any]: roots = [page] page_frames = getattr(page, "frames", []) @@ -66,12 +64,12 @@ class ScreenshotService: except Exception: pass return roots - # [/DEF:ScreenshotService._iter_login_roots:Function] + # #endregion ScreenshotService._iter_login_roots - # [DEF:ScreenshotService._extract_hidden_login_fields:Function] - # @PURPOSE: Collect hidden form fields required for direct login POST fallback. - # @PRE: Login page is loaded. - # @POST: Returns hidden input name/value mapping aggregated from page and child frames. + # #region ScreenshotService._extract_hidden_login_fields [TYPE Function] + # @BRIEF Collect hidden form fields required for direct login POST fallback. + # @PRE Login page is loaded. + # @POST Returns hidden input name/value mapping aggregated from page and child frames. async def _extract_hidden_login_fields(self, page) -> Dict[str, str]: hidden_fields: Dict[str, str] = {} for root in self._iter_login_roots(page): @@ -87,21 +85,21 @@ class ScreenshotService: except Exception: continue return hidden_fields - # [/DEF:ScreenshotService._extract_hidden_login_fields:Function] + # #endregion ScreenshotService._extract_hidden_login_fields - # [DEF:ScreenshotService._extract_csrf_token:Function] - # @PURPOSE: Resolve CSRF token value from main page or embedded login frame. - # @PRE: Login page is loaded. - # @POST: Returns first non-empty csrf token or empty string. + # #region ScreenshotService._extract_csrf_token [TYPE Function] + # @BRIEF Resolve CSRF token value from main page or embedded login frame. + # @PRE Login page is loaded. + # @POST Returns first non-empty csrf token or empty string. async def _extract_csrf_token(self, page) -> str: hidden_fields = await self._extract_hidden_login_fields(page) return str(hidden_fields.get("csrf_token") or "").strip() - # [/DEF:ScreenshotService._extract_csrf_token:Function] + # #endregion ScreenshotService._extract_csrf_token - # [DEF:ScreenshotService._response_looks_like_login_page:Function] - # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page. - # @PRE: response_text is normalized HTML or text from login POST response. - # @POST: Returns True when login-page markers dominate the response body. + # #region ScreenshotService._response_looks_like_login_page [TYPE Function] + # @BRIEF Detect when fallback login POST returned the login form again instead of an authenticated page. + # @PRE response_text is normalized HTML or text from login POST response. + # @POST Returns True when login-page markers dominate the response body. def _response_looks_like_login_page(self, response_text: str) -> bool: normalized = str(response_text or "").strip().lower() if not normalized: @@ -115,23 +113,23 @@ class ScreenshotService: 'name="csrf_token"', ] return sum(marker in normalized for marker in markers) >= 3 - # [/DEF:ScreenshotService._response_looks_like_login_page:Function] + # #endregion ScreenshotService._response_looks_like_login_page - # [DEF:ScreenshotService._redirect_looks_authenticated:Function] - # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target. - # @PRE: redirect_location may be empty or relative. - # @POST: Returns True when redirect target does not point back to login flow. + # #region ScreenshotService._redirect_looks_authenticated [TYPE Function] + # @BRIEF Treat non-login redirects after form POST as successful authentication without waiting for redirect target. + # @PRE redirect_location may be empty or relative. + # @POST Returns True when redirect target does not point back to login flow. def _redirect_looks_authenticated(self, redirect_location: str) -> bool: normalized = str(redirect_location or "").strip().lower() if not normalized: return True return "/login" not in normalized - # [/DEF:ScreenshotService._redirect_looks_authenticated:Function] + # #endregion ScreenshotService._redirect_looks_authenticated - # [DEF:ScreenshotService._submit_login_via_form_post:Function] - # @PURPOSE: Fallback login path that submits credentials directly with csrf token. - # @PRE: login_url is same-origin and csrf token can be read from DOM. - # @POST: Browser context receives authenticated cookies when login succeeds. + # #region ScreenshotService._submit_login_via_form_post [TYPE Function] + # @BRIEF Fallback login path that submits credentials directly with csrf token. + # @PRE login_url is same-origin and csrf token can be read from DOM. + # @POST Browser context receives authenticated cookies when login succeeds. async def _submit_login_via_form_post(self, page, login_url: str) -> bool: hidden_fields = await self._extract_hidden_login_fields(page) csrf_token = str(hidden_fields.get("csrf_token") or "").strip() @@ -188,12 +186,12 @@ class ScreenshotService: f"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}" ) return not looks_like_login_page - # [/DEF:ScreenshotService._submit_login_via_form_post:Function] + # #endregion ScreenshotService._submit_login_via_form_post - # [DEF:ScreenshotService._find_login_field_locator:Function] - # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks. - # @PRE: field_name is `username` or `password`. - # @POST: Returns a locator for the corresponding input or None. + # #region ScreenshotService._find_login_field_locator [TYPE Function] + # @BRIEF Resolve login form input using semantic label text plus generic visible-input fallbacks. + # @PRE field_name is `username` or `password`. + # @POST Returns a locator for the corresponding input or None. async def _find_login_field_locator(self, page, field_name: str) -> Any: normalized = str(field_name or "").strip().lower() for root in self._iter_login_roots(page): @@ -228,12 +226,12 @@ class ScreenshotService: return locator return None - # [/DEF:ScreenshotService._find_login_field_locator:Function] + # #endregion ScreenshotService._find_login_field_locator - # [DEF:ScreenshotService._find_submit_locator:Function] - # @PURPOSE: Resolve login submit button from main page or embedded auth frame. - # @PRE: page is ready for login interaction. - # @POST: Returns visible submit locator or None. + # #region ScreenshotService._find_submit_locator [TYPE Function] + # @BRIEF Resolve login submit button from main page or embedded auth frame. + # @PRE page is ready for login interaction. + # @POST Returns visible submit locator or None. async def _find_submit_locator(self, page) -> Any: selectors = [ lambda root: root.get_by_role("button", name="Sign in", exact=False), @@ -248,12 +246,12 @@ class ScreenshotService: if locator: return locator return None - # [/DEF:ScreenshotService._find_submit_locator:Function] + # #endregion ScreenshotService._find_submit_locator - # [DEF:ScreenshotService._goto_resilient:Function] - # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests. - # @PRE: page is a valid Playwright page and url is non-empty. - # @POST: Returns last navigation response or raises when both primary and fallback waits fail. + # #region ScreenshotService._goto_resilient [TYPE Function] + # @BRIEF Navigate without relying on networkidle for pages with long-polling or persistent requests. + # @PRE page is a valid Playwright page and url is non-empty. + # @POST Returns last navigation response or raises when both primary and fallback waits fail. async def _goto_resilient( self, page, @@ -269,13 +267,13 @@ class ScreenshotService: f"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}" ) return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout) - # [/DEF:ScreenshotService._goto_resilient:Function] + # #endregion ScreenshotService._goto_resilient - # [DEF:ScreenshotService.capture_dashboard:Function] - # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP. - # @PRE: dashboard_id is a valid string, output_path is a writable path. - # @POST: Returns True if screenshot is saved successfully. - # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file. + # #region ScreenshotService.capture_dashboard [TYPE Function] + # @BRIEF Captures a full-page screenshot of a dashboard using Playwright and CDP. + # @PRE dashboard_id is a valid string, output_path is a writable path. + # @POST Returns True if screenshot is saved successfully. + # @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, and writes a PNG file. # @UX_STATE: [Navigating] -> Loading dashboard UI # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions @@ -671,15 +669,15 @@ class ScreenshotService: await browser.close() return True - # [/DEF:ScreenshotService.capture_dashboard:Function] -# [/DEF:ScreenshotService:Class] + # #endregion ScreenshotService.capture_dashboard +# #endregion ScreenshotService -# [DEF:LLMClient:Class] -# @PURPOSE: Wrapper for LLM provider APIs. +# #region LLMClient [TYPE Class] +# @BRIEF Wrapper for LLM provider APIs. class LLMClient: - # [DEF:LLMClient.__init__:Function] - # @PURPOSE: Initializes the LLMClient with provider settings. - # @PRE: api_key, base_url, and default_model are non-empty strings. + # #region LLMClient.__init__ [TYPE Function] + # @BRIEF Initializes the LLMClient with provider settings. + # @PRE api_key, base_url, and default_model are non-empty strings. def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str): self.provider_type = provider_type normalized_key = (api_key or "").strip() @@ -717,12 +715,12 @@ class LLMClient: default_headers=default_headers, http_client=http_client, ) - # [/DEF:LLMClient.__init__:Function] + # #endregion LLMClient.__init__ - # [DEF:LLMClient._supports_json_response_format:Function] - # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object. - # @PRE: Client initialized with base_url and default_model. - # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors. + # #region LLMClient._supports_json_response_format [TYPE Function] + # @BRIEF Detect whether provider/model is likely compatible with response_format=json_object. + # @PRE Client initialized with base_url and default_model. + # @POST Returns False for known-incompatible combinations to avoid avoidable 400 errors. def _supports_json_response_format(self) -> bool: base = (self.base_url or "").lower() model = (self.default_model or "").lower() @@ -737,13 +735,13 @@ class LLMClient: if any(token in model for token in incompatible_tokens): return False return True - # [/DEF:LLMClient._supports_json_response_format:Function] + # #endregion LLMClient._supports_json_response_format - # [DEF:LLMClient.get_json_completion:Function] - # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing. - # @PRE: messages is a list of valid message dictionaries. - # @POST: Returns a parsed JSON dictionary. - # @SIDE_EFFECT: Calls external LLM API. + # #region LLMClient.get_json_completion [TYPE Function] + # @BRIEF Helper to handle LLM calls with JSON mode and fallback parsing. + # @PRE messages is a list of valid message dictionaries. + # @POST Returns a parsed JSON dictionary. + # @SIDE_EFFECT Calls external LLM API. def _should_retry(exception: Exception) -> bool: """Custom retry predicate that excludes authentication errors.""" # Don't retry on authentication errors @@ -859,13 +857,13 @@ class LLMClient: return json.loads(json_str) else: raise - # [/DEF:LLMClient.get_json_completion:Function] + # #endregion LLMClient.get_json_completion - # [DEF:LLMClient.test_runtime_connection:Function] - # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis. - # @PRE: Client is initialized with provider credentials and default_model. - # @POST: Returns lightweight JSON payload when runtime auth/model path is valid. - # @SIDE_EFFECT: Calls external LLM API. + # #region LLMClient.test_runtime_connection [TYPE Function] + # @BRIEF Validate provider credentials using the same chat completions transport as runtime analysis. + # @PRE Client is initialized with provider credentials and default_model. + # @POST Returns lightweight JSON payload when runtime auth/model path is valid. + # @SIDE_EFFECT Calls external LLM API. async def test_runtime_connection(self) -> Dict[str, Any]: with belief_scope("test_runtime_connection"): messages = [ @@ -875,13 +873,13 @@ class LLMClient: } ] return await self.get_json_completion(messages) - # [/DEF:LLMClient.test_runtime_connection:Function] + # #endregion LLMClient.test_runtime_connection - # [DEF:LLMClient.analyze_dashboard:Function] - # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis. - # @PRE: screenshot_path exists, logs is a list of strings. - # @POST: Returns a structured analysis dictionary (status, summary, issues). - # @SIDE_EFFECT: Reads screenshot file and calls external LLM API. + # #region LLMClient.analyze_dashboard [TYPE Function] + # @BRIEF Sends dashboard data (screenshot + logs) to LLM for health analysis. + # @PRE screenshot_path exists, logs is a list of strings. + # @POST Returns a structured analysis dictionary (status, summary, issues). + # @SIDE_EFFECT Reads screenshot file and calls external LLM API. async def analyze_dashboard( self, screenshot_path: str, @@ -947,7 +945,7 @@ class LLMClient: "summary": f"Failed to get response from LLM: {str(e)}", "issues": [{"severity": "UNKNOWN", "message": "LLM provider returned empty or invalid response"}] } - # [/DEF:LLMClient.analyze_dashboard:Function] -# [/DEF:LLMClient:Class] + # #endregion LLMClient.analyze_dashboard +# #endregion LLMClient -# [/DEF:backend/src/plugins/llm_analysis/service.py:Module] +# #endregion backend/src/plugins/llm_analysis/service.py diff --git a/backend/src/plugins/mapper.py b/backend/src/plugins/mapper.py index ea103a66..7f537a53 100644 --- a/backend/src/plugins/mapper.py +++ b/backend/src/plugins/mapper.py @@ -1,9 +1,8 @@ -# [DEF:MapperPluginModule:Module] -# @SEMANTICS: plugin, mapper, datasets, postgresql, excel -# @PURPOSE: Implements a plugin for mapping dataset columns using external database connections or Excel files. -# @LAYER: Plugins -# @RELATION: Inherits from PluginBase. Uses DatasetMapper from superset_tool. -# @RELATION: USES -> TaskContext +# #region MapperPluginModule [TYPE Module] [SEMANTICS plugin, mapper, datasets, postgresql, excel] +# @BRIEF Implements a plugin for mapping dataset columns using external database connections or Excel files. +# @LAYER Plugins +# @RELATION Inherits from PluginBase. Uses DatasetMapper from superset_tool. +# @RELATION USES -> [TaskContext] # [SECTION: IMPORTS] from typing import Dict, Any, Optional @@ -16,71 +15,71 @@ from ..core.utils.dataset_mapper import DatasetMapper from ..core.task_manager.context import TaskContext # [/SECTION] -# [DEF:MapperPlugin:Class] -# @PURPOSE: Plugin for mapping dataset columns verbose names. +# #region MapperPlugin [TYPE Class] +# @BRIEF Plugin for mapping dataset columns verbose names. class MapperPlugin(PluginBase): """ Plugin for mapping dataset columns verbose names. """ @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the mapper plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string ID. - # @RETURN: str - "dataset-mapper" + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the mapper plugin. + # @PRE Plugin instance exists. + # @POST Returns string ID. + # @RETURN str - "dataset-mapper" def id(self) -> str: with belief_scope("id"): return "dataset-mapper" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the mapper plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string name. - # @RETURN: str - Plugin name. + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the mapper plugin. + # @PRE Plugin instance exists. + # @POST Returns string name. + # @RETURN str - Plugin name. def name(self) -> str: with belief_scope("name"): return "Dataset Mapper" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns a description of the mapper plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string description. - # @RETURN: str - Plugin description. + # #region description [TYPE Function] + # @BRIEF Returns a description of the mapper plugin. + # @PRE Plugin instance exists. + # @POST Returns string description. + # @RETURN str - Plugin description. def description(self) -> str: with belief_scope("description"): return "Map dataset column verbose names using PostgreSQL comments or Excel files." - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the version of the mapper plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string version. - # @RETURN: str - "1.0.0" + # #region version [TYPE Function] + # @BRIEF Returns the version of the mapper plugin. + # @PRE Plugin instance exists. + # @POST Returns string version. + # @RETURN str - "1.0.0" def version(self) -> str: with belief_scope("version"): return "1.0.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the mapper plugin. - # @RETURN: str - "/tools/mapper" + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the mapper plugin. + # @RETURN str - "/tools/mapper" def ui_route(self) -> str: with belief_scope("ui_route"): return "/tools/mapper" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for the mapper plugin parameters. - # @PRE: Plugin instance exists. - # @POST: Returns dictionary schema. - # @RETURN: Dict[str, Any] - JSON schema. + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for the mapper plugin parameters. + # @PRE Plugin instance exists. + # @POST Returns dictionary schema. + # @RETURN Dict[str, Any] - JSON schema. def get_schema(self) -> Dict[str, Any]: with belief_scope("get_schema"): return { @@ -126,10 +125,10 @@ class MapperPlugin(PluginBase): }, "required": ["env", "dataset_id", "source"] } - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:execute:Function] - # @PURPOSE: Executes the dataset mapping logic with TaskContext support. + # #region execute [TYPE Function] + # @BRIEF Executes the dataset mapping logic with TaskContext support. # @PARAM: params (Dict[str, Any]) - Mapping parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary. @@ -208,7 +207,7 @@ class MapperPlugin(PluginBase): except Exception as e: log.error(f"Mapping failed: {e}") raise - # [/DEF:execute:Function] + # #endregion execute -# [/DEF:MapperPlugin:Class] -# [/DEF:MapperPluginModule:Module] \ No newline at end of file +# #endregion MapperPlugin +# #endregion MapperPluginModule diff --git a/backend/src/plugins/migration.py b/backend/src/plugins/migration.py index 6bba26f0..932f472b 100755 --- a/backend/src/plugins/migration.py +++ b/backend/src/plugins/migration.py @@ -1,18 +1,16 @@ -# [DEF:MigrationPlugin:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: migration, superset, automation, dashboard, plugin, transformation -# @PURPOSE: Orchestrates export, DB-mapping transformation, and import of Superset dashboards across environments. -# @LAYER: App -# @RELATION: IMPLEMENTS -> PluginBase -# @RELATION: DEPENDS_ON -> SupersetClient -# @RELATION: DEPENDS_ON -> MigrationEngine -# @RELATION: DEPENDS_ON -> IdMappingService -# @RELATION: USES -> TaskContext -# @PRE: Plugin loader can resolve infrastructure dependencies and execution requests provide validated migration context. -# @POST: Plugin metadata remains stable and migration execution preserves mapped-environment import guarantees. -# @SIDE_EFFECT: Reads config, opens database sessions, creates temporary artifacts, and triggers Superset export/import workflows. -# @DATA_CONTRACT: Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set] -# @INVARIANT: Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution. +# #region MigrationPlugin [C:5] [TYPE Module] [SEMANTICS migration, superset, automation, dashboard, plugin, transformation] +# @BRIEF Orchestrates export, DB-mapping transformation, and import of Superset dashboards across environments. +# @LAYER App +# @PRE Plugin loader can resolve infrastructure dependencies and execution requests provide validated migration context. +# @POST Plugin metadata remains stable and migration execution preserves mapped-environment import guarantees. +# @SIDE_EFFECT Reads config, opens database sessions, creates temporary artifacts, and triggers Superset export/import workflows. +# @DATA_CONTRACT Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set] +# @INVARIANT Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution. +# @RELATION IMPLEMENTS -> [PluginBase] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [MigrationEngine] +# @RELATION DEPENDS_ON -> [IdMappingService] +# @RELATION USES -> [TaskContext] from typing import Dict, Any, Optional import re @@ -31,10 +29,10 @@ from ..models.mapping import DatabaseMapping, Environment from ..core.mapping_service import IdMappingService from ..core.task_manager.context import TaskContext -# [DEF:MigrationPlugin:Class] -# @PURPOSE: Implementation of the migration plugin workflow and transformation orchestration. -# @PRE: SupersetClient authenticated, database session active -# @POST: Returns MigrationResult with success/failure status and artifact list +# #region MigrationPlugin [TYPE Class] +# @BRIEF Implementation of the migration plugin workflow and transformation orchestration. +# @PRE SupersetClient authenticated, database session active +# @POST Returns MigrationResult with success/failure status and artifact list # @TEST_FIXTURE: superset_export_zip -> file:backend/tests/fixtures/migration/dashboard_export.zip # @TEST_FIXTURE: db_mapping_payload -> INLINE_JSON: {"db_mappings": {"source_uuid_1": "target_uuid_2"}} # @TEST_FIXTURE: password_inject_payload -> INLINE_JSON: {"passwords": {"PostgreSQL": "secret123"}} @@ -47,65 +45,65 @@ class MigrationPlugin(PluginBase): """ @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the migration plugin. - # @PRE: None. - # @POST: Returns stable string "superset-migration". - # @RETURN: str + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the migration plugin. + # @PRE None. + # @POST Returns stable string "superset-migration". + # @RETURN str def id(self) -> str: with belief_scope("MigrationPlugin.id"): return "superset-migration" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the plugin. - # @PRE: None. - # @POST: Returns "Superset Dashboard Migration". - # @RETURN: str + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the plugin. + # @PRE None. + # @POST Returns "Superset Dashboard Migration". + # @RETURN str def name(self) -> str: with belief_scope("MigrationPlugin.name"): return "Superset Dashboard Migration" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns the semantic description of the plugin. - # @PRE: None. - # @POST: Returns description string. - # @RETURN: str + # #region description [TYPE Function] + # @BRIEF Returns the semantic description of the plugin. + # @PRE None. + # @POST Returns description string. + # @RETURN str def description(self) -> str: with belief_scope("MigrationPlugin.description"): return "Migrates dashboards between Superset environments." - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the semantic version of the migration plugin. - # @PRE: None. - # @POST: Returns "1.0.0". - # @RETURN: str + # #region version [TYPE Function] + # @BRIEF Returns the semantic version of the migration plugin. + # @PRE None. + # @POST Returns "1.0.0". + # @RETURN str def version(self) -> str: with belief_scope("MigrationPlugin.version"): return "1.0.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend routing anchor for the plugin. - # @PRE: None. - # @POST: Returns "/migration". - # @RETURN: str + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend routing anchor for the plugin. + # @PRE None. + # @POST Returns "/migration". + # @RETURN str def ui_route(self) -> str: with belief_scope("MigrationPlugin.ui_route"): return "/migration" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Generates the JSON Schema for the plugin execution form dynamically. - # @PRE: ConfigManager is accessible and environments are defined. - # @POST: Returns a JSON Schema dict matching current system environments. - # @RETURN: Dict[str, Any] + # #region get_schema [TYPE Function] + # @BRIEF Generates the JSON Schema for the plugin execution form dynamically. + # @PRE ConfigManager is accessible and environments are defined. + # @POST Returns a JSON Schema dict matching current system environments. + # @RETURN Dict[str, Any] def get_schema(self) -> Dict[str, Any]: with belief_scope("MigrationPlugin.get_schema"): log.reason("Generating migration UI schema") @@ -153,10 +151,10 @@ class MigrationPlugin(PluginBase): } log.reflect("Schema generated successfully", payload={"environments_count": len(envs)}) return schema - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:execute:Function] - # @PURPOSE: Orchestrates the dashboard migration pipeline including extraction, AST mutation, and ingestion. + # #region execute [TYPE Function] + # @BRIEF Orchestrates the dashboard migration pipeline including extraction, AST mutation, and ingestion. # @PARAM: params (Dict[str, Any]) - Extracted parameters from UI/API execution request. # @PARAM: context (Optional[TaskContext]) - Dependency injected TaskContext for IO tracing. # @PRE: Source and target environments must resolve. Matching dashboards must exist. @@ -388,6 +386,6 @@ class MigrationPlugin(PluginBase): except Exception as e: log.explore("Fatal plugin failure", error=f"Plugin execution failed: {e}", exc_info=True) raise e - # [/DEF:execute:Function] -# [/DEF:MigrationPlugin:Class] -# [/DEF:MigrationPlugin:Module] \ No newline at end of file + # #endregion execute +# #endregion MigrationPlugin +# #endregion MigrationPlugin diff --git a/backend/src/plugins/search.py b/backend/src/plugins/search.py index 5f4c25aa..c2da73d6 100644 --- a/backend/src/plugins/search.py +++ b/backend/src/plugins/search.py @@ -1,9 +1,8 @@ -# [DEF:SearchPluginModule:Module] -# @SEMANTICS: plugin, search, datasets, regex, superset -# @PURPOSE: Implements a plugin for searching text patterns across all datasets in a specific Superset environment. -# @LAYER: Plugins -# @RELATION: Inherits from PluginBase. Uses SupersetClient from core. -# @RELATION: USES -> TaskContext +# #region SearchPluginModule [TYPE Module] [SEMANTICS plugin, search, datasets, regex, superset] +# @BRIEF Implements a plugin for searching text patterns across all datasets in a specific Superset environment. +# @LAYER Plugins +# @RELATION Inherits from PluginBase. Uses SupersetClient from core. +# @RELATION USES -> [TaskContext] # [SECTION: IMPORTS] import re @@ -14,71 +13,71 @@ from ..core.logger import logger, belief_scope from ..core.task_manager.context import TaskContext # [/SECTION] -# [DEF:SearchPlugin:Class] -# @PURPOSE: Plugin for searching text patterns in Superset datasets. +# #region SearchPlugin [TYPE Class] +# @BRIEF Plugin for searching text patterns in Superset datasets. class SearchPlugin(PluginBase): """ Plugin for searching text patterns in Superset datasets. """ @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the search plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string ID. - # @RETURN: str - "search-datasets" + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the search plugin. + # @PRE Plugin instance exists. + # @POST Returns string ID. + # @RETURN str - "search-datasets" def id(self) -> str: with belief_scope("id"): return "search-datasets" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the search plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string name. - # @RETURN: str - Plugin name. + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the search plugin. + # @PRE Plugin instance exists. + # @POST Returns string name. + # @RETURN str - Plugin name. def name(self) -> str: with belief_scope("name"): return "Search Datasets" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns a description of the search plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string description. - # @RETURN: str - Plugin description. + # #region description [TYPE Function] + # @BRIEF Returns a description of the search plugin. + # @PRE Plugin instance exists. + # @POST Returns string description. + # @RETURN str - Plugin description. def description(self) -> str: with belief_scope("description"): return "Search for text patterns across all datasets in a specific environment." - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the version of the search plugin. - # @PRE: Plugin instance exists. - # @POST: Returns string version. - # @RETURN: str - "1.0.0" + # #region version [TYPE Function] + # @BRIEF Returns the version of the search plugin. + # @PRE Plugin instance exists. + # @POST Returns string version. + # @RETURN str - "1.0.0" def version(self) -> str: with belief_scope("version"): return "1.0.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the search plugin. - # @RETURN: str - "/tools/search" + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the search plugin. + # @RETURN str - "/tools/search" def ui_route(self) -> str: with belief_scope("ui_route"): return "/tools/search" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for the search plugin parameters. - # @PRE: Plugin instance exists. - # @POST: Returns dictionary schema. - # @RETURN: Dict[str, Any] - JSON schema. + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for the search plugin parameters. + # @PRE Plugin instance exists. + # @POST Returns dictionary schema. + # @RETURN Dict[str, Any] - JSON schema. def get_schema(self) -> Dict[str, Any]: with belief_scope("get_schema"): return { @@ -97,10 +96,10 @@ class SearchPlugin(PluginBase): }, "required": ["env", "query"] } - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:execute:Function] - # @PURPOSE: Executes the dataset search logic with TaskContext support. + # #region execute [TYPE Function] + # @BRIEF Executes the dataset search logic with TaskContext support. # @PARAM: params (Dict[str, Any]) - Search parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: Params contain valid 'env' and 'query'. @@ -176,10 +175,10 @@ class SearchPlugin(PluginBase): except Exception as e: log.error(f"Error during search: {e}") raise - # [/DEF:execute:Function] + # #endregion execute - # [DEF:_get_context:Function] - # @PURPOSE: Extracts a small context around the match for display. + # #region _get_context [TYPE Function] + # @BRIEF Extracts a small context around the match for display. # @PARAM: text (str) - The full text to extract context from. # @PARAM: match_text (str) - The matched text pattern. # @PARAM: context_lines (int) - Number of lines of context to include. @@ -214,7 +213,7 @@ class SearchPlugin(PluginBase): return "\n".join(context) return text[:100] + "..." if len(text) > 100 else text - # [/DEF:_get_context:Function] + # #endregion _get_context -# [/DEF:SearchPlugin:Class] -# [/DEF:SearchPluginModule:Module] \ No newline at end of file +# #endregion SearchPlugin +# #endregion SearchPluginModule diff --git a/backend/src/plugins/storage/plugin.py b/backend/src/plugins/storage/plugin.py index cce0e4fa..3032b359 100644 --- a/backend/src/plugins/storage/plugin.py +++ b/backend/src/plugins/storage/plugin.py @@ -1,13 +1,12 @@ -# [DEF:StoragePlugin:Module] +# #region StoragePlugin [TYPE Module] [SEMANTICS storage, files, filesystem, plugin] +# @BRIEF Provides core filesystem operations for managing backups and repositories. +# @LAYER App +# @INVARIANT All file operations must be restricted to the configured storage root. +# @RELATION IMPLEMENTS -> [PluginBase] +# @RELATION DEPENDS_ON -> [backend.src.models.storage] +# @RELATION USES -> [TaskContext] # -# @SEMANTICS: storage, files, filesystem, plugin -# @PURPOSE: Provides core filesystem operations for managing backups and repositories. -# @LAYER: App -# @RELATION: IMPLEMENTS -> PluginBase -# @RELATION: DEPENDS_ON -> backend.src.models.storage -# @RELATION: USES -> TaskContext # -# @INVARIANT: All file operations must be restricted to the configured storage root. # [SECTION: IMPORTS] import os @@ -27,80 +26,80 @@ from ...dependencies import get_config_manager from ...core.task_manager.context import TaskContext # [/SECTION] -# [DEF:StoragePlugin:Class] -# @PURPOSE: Implementation of the storage management plugin. +# #region StoragePlugin [TYPE Class] +# @BRIEF Implementation of the storage management plugin. class StoragePlugin(PluginBase): """ Plugin for managing local file storage for backups and repositories. """ - # [DEF:__init__:Function] - # @PURPOSE: Initializes the StoragePlugin and ensures required directories exist. - # @PRE: Configuration manager must be accessible. - # @POST: Storage root and category directories are created on disk. + # #region __init__ [TYPE Function] + # @BRIEF Initializes the StoragePlugin and ensures required directories exist. + # @PRE Configuration manager must be accessible. + # @POST Storage root and category directories are created on disk. def __init__(self): with belief_scope("StoragePlugin:init"): self.ensure_directories() - # [/DEF:__init__:Function] + # #endregion __init__ @property - # [DEF:id:Function] - # @PURPOSE: Returns the unique identifier for the storage plugin. - # @PRE: None. - # @POST: Returns the plugin ID string. - # @RETURN: str - "storage-manager" + # #region id [TYPE Function] + # @BRIEF Returns the unique identifier for the storage plugin. + # @PRE None. + # @POST Returns the plugin ID string. + # @RETURN str - "storage-manager" def id(self) -> str: with belief_scope("StoragePlugin:id"): return "storage-manager" - # [/DEF:id:Function] + # #endregion id @property - # [DEF:name:Function] - # @PURPOSE: Returns the human-readable name of the storage plugin. - # @PRE: None. - # @POST: Returns the plugin name string. - # @RETURN: str - "Storage Manager" + # #region name [TYPE Function] + # @BRIEF Returns the human-readable name of the storage plugin. + # @PRE None. + # @POST Returns the plugin name string. + # @RETURN str - "Storage Manager" def name(self) -> str: with belief_scope("StoragePlugin:name"): return "Storage Manager" - # [/DEF:name:Function] + # #endregion name @property - # [DEF:description:Function] - # @PURPOSE: Returns a description of the storage plugin. - # @PRE: None. - # @POST: Returns the plugin description string. - # @RETURN: str - Plugin description. + # #region description [TYPE Function] + # @BRIEF Returns a description of the storage plugin. + # @PRE None. + # @POST Returns the plugin description string. + # @RETURN str - Plugin description. def description(self) -> str: with belief_scope("StoragePlugin:description"): return "Manages local file storage for backups and repositories." - # [/DEF:description:Function] + # #endregion description @property - # [DEF:version:Function] - # @PURPOSE: Returns the version of the storage plugin. - # @PRE: None. - # @POST: Returns the version string. - # @RETURN: str - "1.0.0" + # #region version [TYPE Function] + # @BRIEF Returns the version of the storage plugin. + # @PRE None. + # @POST Returns the version string. + # @RETURN str - "1.0.0" def version(self) -> str: with belief_scope("StoragePlugin:version"): return "1.0.0" - # [/DEF:version:Function] + # #endregion version @property - # [DEF:ui_route:Function] - # @PURPOSE: Returns the frontend route for the storage plugin. - # @RETURN: str - "/tools/storage" + # #region ui_route [TYPE Function] + # @BRIEF Returns the frontend route for the storage plugin. + # @RETURN str - "/tools/storage" def ui_route(self) -> str: with belief_scope("StoragePlugin:ui_route"): return "/tools/storage" - # [/DEF:ui_route:Function] + # #endregion ui_route - # [DEF:get_schema:Function] - # @PURPOSE: Returns the JSON schema for storage plugin parameters. - # @PRE: None. - # @POST: Returns a dictionary representing the JSON schema. - # @RETURN: Dict[str, Any] - JSON schema. + # #region get_schema [TYPE Function] + # @BRIEF Returns the JSON schema for storage plugin parameters. + # @PRE None. + # @POST Returns a dictionary representing the JSON schema. + # @RETURN Dict[str, Any] - JSON schema. def get_schema(self) -> Dict[str, Any]: with belief_scope("StoragePlugin:get_schema"): return { @@ -114,10 +113,10 @@ class StoragePlugin(PluginBase): }, "required": ["category"] } - # [/DEF:get_schema:Function] + # #endregion get_schema - # [DEF:execute:Function] - # @PURPOSE: Executes storage-related tasks with TaskContext support. + # #region execute [TYPE Function] + # @BRIEF Executes storage-related tasks with TaskContext support. # @PARAM: params (Dict[str, Any]) - Storage parameters. # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution. # @PRE: params must match the plugin schema. @@ -125,12 +124,12 @@ class StoragePlugin(PluginBase): async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None): with belief_scope("StoragePlugin:execute"): log.reason(f"Executing with params: {params}") - # [/DEF:execute:Function] + # #endregion execute - # [DEF:get_storage_root:Function] - # @PURPOSE: Resolves the absolute path to the storage root. - # @PRE: Settings must define a storage root path. - # @POST: Returns a Path object representing the storage root. + # #region get_storage_root [TYPE Function] + # @BRIEF Resolves the absolute path to the storage root. + # @PRE Settings must define a storage root path. + # @POST Returns a Path object representing the storage root. def get_storage_root(self) -> Path: with belief_scope("StoragePlugin:get_storage_root"): config_manager = get_config_manager() @@ -147,10 +146,10 @@ class StoragePlugin(PluginBase): project_root = Path(__file__).parents[3] root = (project_root / root).resolve() return root - # [/DEF:get_storage_root:Function] + # #endregion get_storage_root - # [DEF:resolve_path:Function] - # @PURPOSE: Resolves a dynamic path pattern using provided variables. + # #region resolve_path [TYPE Function] + # @BRIEF Resolves a dynamic path pattern using provided variables. # @PARAM: pattern (str) - The path pattern to resolve. # @PARAM: variables (Dict[str, str]) - Variables to substitute in the pattern. # @PRE: pattern must be a valid format string. @@ -171,13 +170,13 @@ class StoragePlugin(PluginBase): log.explore("Missing variable for path resolution", error=str(e)) # Fallback to literal pattern if formatting fails partially (or handle as needed) return pattern.replace("{", "").replace("}", "") - # [/DEF:resolve_path:Function] + # #endregion resolve_path - # [DEF:ensure_directories:Function] - # @PURPOSE: Creates the storage root and category subdirectories if they don't exist. - # @PRE: Storage root must be resolvable. - # @POST: Directories are created on the filesystem. - # @SIDE_EFFECT: Creates directories on the filesystem. + # #region ensure_directories [TYPE Function] + # @BRIEF Creates the storage root and category subdirectories if they don't exist. + # @PRE Storage root must be resolvable. + # @POST Directories are created on the filesystem. + # @SIDE_EFFECT Creates directories on the filesystem. def ensure_directories(self): with belief_scope("StoragePlugin:ensure_directories"): root = self.get_storage_root() @@ -186,12 +185,12 @@ class StoragePlugin(PluginBase): path = root / category.value path.mkdir(parents=True, exist_ok=True) log.reason(f"Ensured directory: {path}") - # [/DEF:ensure_directories:Function] + # #endregion ensure_directories - # [DEF:validate_path:Function] - # @PURPOSE: Prevents path traversal attacks by ensuring the path is within the storage root. - # @PRE: path must be a Path object. - # @POST: Returns the resolved absolute path if valid, otherwise raises ValueError. + # #region validate_path [TYPE Function] + # @BRIEF Prevents path traversal attacks by ensuring the path is within the storage root. + # @PRE path must be a Path object. + # @POST Returns the resolved absolute path if valid, otherwise raises ValueError. def validate_path(self, path: Path) -> Path: with belief_scope("StoragePlugin:validate_path"): root = self.get_storage_root().resolve() @@ -202,10 +201,10 @@ class StoragePlugin(PluginBase): log.explore(f"Path traversal detected: {resolved} is not under {root}", error="Path outside storage root") raise ValueError("Access denied: Path is outside of storage root.") return resolved - # [/DEF:validate_path:Function] + # #endregion validate_path - # [DEF:list_files:Function] - # @PURPOSE: Lists all files and directories in a specific category and subpath. + # #region list_files [TYPE Function] + # @BRIEF Lists all files and directories in a specific category and subpath. # @PARAM: category (Optional[FileCategory]) - The category to list. # @PARAM: subpath (Optional[str]) - Nested path within the category. # @PARAM: recursive (bool) - Whether to scan nested subdirectories recursively. @@ -300,10 +299,10 @@ class StoragePlugin(PluginBase): # Sort: directories first, then by name return sorted(files, key=lambda x: (x.mime_type != "directory", x.name)) - # [/DEF:list_files:Function] + # #endregion list_files - # [DEF:save_file:Function] - # @PURPOSE: Saves an uploaded file to the specified category and optional subpath. + # #region save_file [TYPE Function] + # @BRIEF Saves an uploaded file to the specified category and optional subpath. # @PARAM: file (UploadFile) - The uploaded file. # @PARAM: category (FileCategory) - The target category. # @PARAM: subpath (Optional[str]) - The target subpath. @@ -334,10 +333,10 @@ class StoragePlugin(PluginBase): category=category, mime_type=file.content_type ) - # [/DEF:save_file:Function] + # #endregion save_file - # [DEF:delete_file:Function] - # @PURPOSE: Deletes a file or directory from the specified category and path. + # #region delete_file [TYPE Function] + # @BRIEF Deletes a file or directory from the specified category and path. # @PARAM: category (FileCategory) - The category. # @PARAM: path (str) - The relative path of the file or directory. # @PRE: path must belong to the specified category and exist on disk. @@ -360,10 +359,10 @@ class StoragePlugin(PluginBase): log.reflect(f"Deleted: {full_path}") else: raise FileNotFoundError(f"Item {path} not found") - # [/DEF:delete_file:Function] + # #endregion delete_file - # [DEF:get_file_path:Function] - # @PURPOSE: Returns the absolute path of a file for download. + # #region get_file_path [TYPE Function] + # @BRIEF Returns the absolute path of a file for download. # @PARAM: category (FileCategory) - The category. # @PARAM: path (str) - The relative path of the file. # @PRE: path must belong to the specified category and be a file. @@ -381,7 +380,7 @@ class StoragePlugin(PluginBase): raise FileNotFoundError(f"File {path} not found") return file_path - # [/DEF:get_file_path:Function] + # #endregion get_file_path -# [/DEF:StoragePlugin:Class] -# [/DEF:StoragePlugin:Module] +# #endregion StoragePlugin +# #endregion StoragePlugin diff --git a/backend/src/plugins/translate/__tests__/__init__.py b/backend/src/plugins/translate/__tests__/__init__.py index b0a441a2..e82687da 100644 --- a/backend/src/plugins/translate/__tests__/__init__.py +++ b/backend/src/plugins/translate/__tests__/__init__.py @@ -1,3 +1,3 @@ -# [DEF:TranslatePluginTestsPackage:Package] -# @PURPOSE: Tests for the translate plugin package. -# [/DEF:TranslatePluginTestsPackage:Package] +# #region TranslatePluginTestsPackage [TYPE Package] +# @BRIEF Tests for the translate plugin package. +# #endregion TranslatePluginTestsPackage diff --git a/backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py b/backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py index 81facee8..4e158885 100644 --- a/backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py +++ b/backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py @@ -68,8 +68,8 @@ def _make_mock_record( return rec -# [DEF:TestClickHouseTimestampNormalization:Class] -# @PURPOSE: Verify Unix timestamp detection and conversion for ClickHouse Date columns. +# #region TestClickHouseTimestampNormalization [TYPE Class] +# @BRIEF Verify Unix timestamp detection and conversion for ClickHouse Date columns. class TestClickHouseTimestampNormalization: """Verify _normalize_timestamp_value handles real-world ClickHouse Date scenarios.""" @@ -119,11 +119,11 @@ class TestClickHouseTimestampNormalization: def test_postgresql_encode_not_affected(self) -> None: result = _encode_sql_value("1726358400000.0", dialect="postgresql") assert result == "'1726358400000.0'" -# [/DEF:TestClickHouseTimestampNormalization:Class] +# #endregion TestClickHouseTimestampNormalization -# [DEF:TestClickHouseInsertSqlGeneration:Class] -# @PURPOSE: Verify SQLGenerator produces valid ClickHouse INSERT SQL with normalized dates. +# #region TestClickHouseInsertSqlGeneration [TYPE Class] +# @BRIEF Verify SQLGenerator produces valid ClickHouse INSERT SQL with normalized dates. class TestClickHouseInsertSqlGeneration: """Verify SQLGenerator.generate for ClickHouse with timestamp key columns.""" @@ -227,11 +227,11 @@ class TestClickHouseInsertSqlGeneration: assert "'2024-09-15'" in sql assert "1726358400" not in sql or "'2024-09-15'" in sql -# [/DEF:TestClickHouseInsertSqlGeneration:Class] +# #endregion TestClickHouseInsertSqlGeneration -# [DEF:TestClickHouseJoinVerification:Class] -# @PURPOSE: Verify the JOIN query SQL is syntactically correct for ClickHouse. +# #region TestClickHouseJoinVerification [TYPE Class] +# @BRIEF Verify the JOIN query SQL is syntactically correct for ClickHouse. class TestClickHouseJoinVerification: """Verify the verification JOIN query structure for ClickHouse.""" @@ -295,11 +295,11 @@ class TestClickHouseJoinVerification: assert "`report_date`" in insert_sql assert "`document_number`" in insert_sql assert "`comment_text_en`" in insert_sql -# [/DEF:TestClickHouseJoinVerification:Class] +# #endregion TestClickHouseJoinVerification -# [DEF:TestOrchestratorInsertFlow:Class] -# @PURPOSE: Verify the orchestrator's _generate_and_insert_sql produces correct SQL for ClickHouse. +# #region TestOrchestratorInsertFlow [TYPE Class] +# @BRIEF Verify the orchestrator's _generate_and_insert_sql produces correct SQL for ClickHouse. class TestOrchestratorInsertFlow: """Verify TranslationOrchestrator._generate_and_insert_sql with mocked DB.""" @@ -383,11 +383,11 @@ class TestOrchestratorInsertFlow: # Verify result assert result["status"] == "success" -# [/DEF:TestOrchestratorInsertFlow:Class] +# #endregion TestOrchestratorInsertFlow -# [DEF:TestClickHouseEndToEnd:Class] -# @PURPOSE: End-to-end test: generate SQL, verify structure, simulate execution. +# #region TestClickHouseEndToEnd [TYPE Class] +# @BRIEF End-to-end test: generate SQL, verify structure, simulate execution. class TestClickHouseEndToEnd: """End-to-end verification of ClickHouse INSERT flow.""" @@ -509,4 +509,4 @@ LIMIT 10 assert "f_a.document_number = f_c.document_number" in join_sql assert "dm_view.financial_arrears" in join_sql assert "dm_view.financial_comments_translated" in join_sql -# [/DEF:TestClickHouseEndToEnd:Class] +# #endregion TestClickHouseEndToEnd diff --git a/backend/src/plugins/translate/__tests__/test_dictionary.py b/backend/src/plugins/translate/__tests__/test_dictionary.py index 90328c46..c7934462 100644 --- a/backend/src/plugins/translate/__tests__/test_dictionary.py +++ b/backend/src/plugins/translate/__tests__/test_dictionary.py @@ -1,8 +1,6 @@ -# [DEF:DictionaryTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, dictionary, crud, import, filter -# @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering. -# @RELATION: BINDS_TO -> [DictionaryManager:Class] +# #region DictionaryTests [C:3] [TYPE Module] [SEMANTICS tests, dictionary, crud, import, filter] +# @BRIEF Validate DictionaryManager CRUD, import, deletion guards, and batch filtering. +# @RELATION BINDS_TO -> [DictionaryManager:Class] # # @TEST_CONTRACT: [DictionaryManager] -> { # invariants: [ @@ -37,16 +35,15 @@ from src.plugins.translate.dictionary import DictionaryManager from src.plugins.translate._utils import _normalize_term, _detect_delimiter -# [DEF:_FakeJob:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Helper to create inline TranslationJob records. +# #region _FakeJob [C:1] [TYPE Class] +# @BRIEF Helper to create inline TranslationJob records. class _FakeJob: pass -# [/DEF:_FakeJob:Class] +# #endregion _FakeJob -# [DEF:db_session:Fixture] -# @PURPOSE: Provide an in-memory SQLite session for each test, with tables created and torn down. +# #region db_session [TYPE Fixture] +# @BRIEF Provide an in-memory SQLite session for each test, with tables created and torn down. @pytest.fixture def db_session(): engine = create_engine("sqlite:///:memory:", echo=False) @@ -65,11 +62,11 @@ def db_session(): finally: session.close() Base.metadata.drop_all(bind=engine) -# [/DEF:db_session:Fixture] +# #endregion db_session -# [DEF:test_create_dictionary:Function] -# @PURPOSE: Verify dictionary creation and read-back. +# #region test_create_dictionary [TYPE Function] +# @BRIEF Verify dictionary creation and read-back. def test_create_dictionary(db_session: Session): d = DictionaryManager.create_dictionary( db_session, @@ -90,11 +87,11 @@ def test_create_dictionary(db_session: Session): fetched = DictionaryManager.get_dictionary(db_session, d.id) assert fetched.id == d.id assert fetched.name == "Finance Terms" -# [/DEF:test_create_dictionary:Function] +# #endregion test_create_dictionary -# [DEF:test_update_dictionary:Function] -# @PURPOSE: Verify dictionary metadata update. +# #region test_update_dictionary [TYPE Function] +# @BRIEF Verify dictionary metadata update. def test_update_dictionary(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Old Name", @@ -109,11 +106,11 @@ def test_update_dictionary(db_session: Session): assert updated.name == "New Name" assert updated.description == "Updated desc" assert updated.is_active is False -# [/DEF:test_update_dictionary:Function] +# #endregion test_update_dictionary -# [DEF:test_delete_dictionary:Function] -# @PURPOSE: Verify dictionary deletion. +# #region test_delete_dictionary [TYPE Function] +# @BRIEF Verify dictionary deletion. def test_delete_dictionary(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="To Delete", @@ -126,11 +123,11 @@ def test_delete_dictionary(db_session: Session): DictionaryManager.delete_dictionary(db_session, d.id) with pytest.raises(ValueError, match="Dictionary not found"): DictionaryManager.get_dictionary(db_session, d.id) -# [/DEF:test_delete_dictionary:Function] +# #endregion test_delete_dictionary -# [DEF:test_list_dictionaries:Function] -# @PURPOSE: Verify paginated dictionary listing. +# #region test_list_dictionaries [TYPE Function] +# @BRIEF Verify paginated dictionary listing. def test_list_dictionaries(db_session: Session): for i in range(5): DictionaryManager.create_dictionary( @@ -141,11 +138,11 @@ def test_list_dictionaries(db_session: Session): dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2) assert total == 5 assert len(dicts) == 2 -# [/DEF:test_list_dictionaries:Function] +# #endregion test_list_dictionaries -# [DEF:test_add_entry_duplicate:Function] -# @PURPOSE: Verify duplicate entry raises ValueError and unique constraint is enforced. +# #region test_add_entry_duplicate [TYPE Function] +# @BRIEF Verify duplicate entry raises ValueError and unique constraint is enforced. def test_add_entry_duplicate(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -160,11 +157,11 @@ def test_add_entry_duplicate(db_session: Session): # Different case, same normalized should also raise with pytest.raises(ValueError, match="already exists"): DictionaryManager.add_entry(db_session, d.id, "HELLO", "Ciao") -# [/DEF:test_add_entry_duplicate:Function] +# #endregion test_add_entry_duplicate -# [DEF:test_add_entry_duplicate_per_dictionary:Function] -# @PURPOSE: Verify duplicate is per-dictionary (same term in different dictionaries is OK). +# #region test_add_entry_duplicate_per_dictionary [TYPE Function] +# @BRIEF Verify duplicate is per-dictionary (same term in different dictionaries is OK). def test_add_entry_duplicate_per_dictionary(db_session: Session): d1 = DictionaryManager.create_dictionary( db_session, name="Dict1", @@ -178,11 +175,11 @@ def test_add_entry_duplicate_per_dictionary(db_session: Session): # Same term in different dictionary should work entry = DictionaryManager.add_entry(db_session, d2.id, "hello", "bonjour") assert entry.id is not None -# [/DEF:test_add_entry_duplicate_per_dictionary:Function] +# #endregion test_add_entry_duplicate_per_dictionary -# [DEF:test_edit_entry:Function] -# @PURPOSE: Verify entry edit updates fields and enforces uniqueness. +# #region test_edit_entry [TYPE Function] +# @BRIEF Verify entry edit updates fields and enforces uniqueness. def test_edit_entry(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -197,11 +194,11 @@ def test_edit_entry(db_session: Session): DictionaryManager.add_entry(db_session, d.id, "world", "mundo") with pytest.raises(ValueError, match="already exists"): DictionaryManager.edit_entry(db_session, entry.id, source_term="WORLD") -# [/DEF:test_edit_entry:Function] +# #endregion test_edit_entry -# [DEF:test_delete_entry:Function] -# @PURPOSE: Verify entry deletion. +# #region test_delete_entry [TYPE Function] +# @BRIEF Verify entry deletion. def test_delete_entry(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -212,11 +209,11 @@ def test_delete_entry(db_session: Session): entries, total = DictionaryManager.list_entries(db_session, d.id) assert total == 0 -# [/DEF:test_delete_entry:Function] +# #endregion test_delete_entry -# [DEF:test_import_csv_overwrite:Function] -# @PURPOSE: Verify CSV import with overwrite conflict mode. +# #region test_import_csv_overwrite [TYPE Function] +# @BRIEF Verify CSV import with overwrite conflict mode. def test_import_csv_overwrite(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -239,11 +236,11 @@ def test_import_csv_overwrite(db_session: Session): entries, _ = DictionaryManager.list_entries(db_session, d.id) hello_entry = [e for e in entries if e.source_term_normalized == "hello"][0] assert hello_entry.target_term == "HELLO" -# [/DEF:test_import_csv_overwrite:Function] +# #endregion test_import_csv_overwrite -# [DEF:test_import_csv_keep_existing:Function] -# @PURPOSE: Verify CSV import with keep_existing conflict mode. +# #region test_import_csv_keep_existing [TYPE Function] +# @BRIEF Verify CSV import with keep_existing conflict mode. def test_import_csv_keep_existing(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -264,11 +261,11 @@ def test_import_csv_keep_existing(db_session: Session): entries, _ = DictionaryManager.list_entries(db_session, d.id) hello_entry = [e for e in entries if e.source_term_normalized == "hello"][0] assert hello_entry.target_term == "hola" -# [/DEF:test_import_csv_keep_existing:Function] +# #endregion test_import_csv_keep_existing -# [DEF:test_import_csv_cancel_on_conflict:Function] -# @PURPOSE: Verify CSV import with cancel conflict mode raises errors. +# #region test_import_csv_cancel_on_conflict [TYPE Function] +# @BRIEF Verify CSV import with cancel conflict mode raises errors. def test_import_csv_cancel_on_conflict(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -286,11 +283,11 @@ def test_import_csv_cancel_on_conflict(db_session: Session): assert result["updated"] == 0 assert len(result["errors"]) == 1 # hello conflict error assert "Conflict" in result["errors"][0]["error"] -# [/DEF:test_import_csv_cancel_on_conflict:Function] +# #endregion test_import_csv_cancel_on_conflict -# [DEF:test_import_tsv:Function] -# @PURPOSE: Verify TSV import. +# #region test_import_tsv [TYPE Function] +# @BRIEF Verify TSV import. def test_import_tsv(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -303,11 +300,11 @@ def test_import_tsv(db_session: Session): ) assert result["created"] == 2 assert result["total"] == 2 -# [/DEF:test_import_tsv:Function] +# #endregion test_import_tsv -# [DEF:test_import_invalid_format:Function] -# @PURPOSE: Verify import raises ValueError for missing required columns. +# #region test_import_invalid_format [TYPE Function] +# @BRIEF Verify import raises ValueError for missing required columns. def test_import_invalid_format(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -319,11 +316,11 @@ def test_import_invalid_format(db_session: Session): db_session, d.id, bad_content, delimiter=",", on_conflict="overwrite", ) -# [/DEF:test_import_invalid_format:Function] +# #endregion test_import_invalid_format -# [DEF:test_import_empty_rows:Function] -# @PURPOSE: Verify import handles rows with missing fields gracefully. +# #region test_import_empty_rows [TYPE Function] +# @BRIEF Verify import handles rows with missing fields gracefully. def test_import_empty_rows(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -336,11 +333,11 @@ def test_import_empty_rows(db_session: Session): ) assert result["created"] == 1 # hello assert len(result["errors"]) == 2 # empty source or target -# [/DEF:test_import_empty_rows:Function] +# #endregion test_import_empty_rows -# [DEF:test_import_preview:Function] -# @PURPOSE: Verify import preview returns conflicts without mutating. +# #region test_import_preview [TYPE Function] +# @BRIEF Verify import preview returns conflicts without mutating. def test_import_preview(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -364,11 +361,11 @@ def test_import_preview(db_session: Session): # Verify no mutation happened entries, total = DictionaryManager.list_entries(db_session, d.id) assert total == 1 # still only the original entry -# [/DEF:test_import_preview:Function] +# #endregion test_import_preview -# [DEF:test_delete_dictionary_blocked_by_active_job:Function] -# @PURPOSE: Verify deletion is blocked when dictionary is attached to active/scheduled jobs. +# #region test_delete_dictionary_blocked_by_active_job [TYPE Function] +# @BRIEF Verify deletion is blocked when dictionary is attached to active/scheduled jobs. def test_delete_dictionary_blocked_by_active_job(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -395,11 +392,11 @@ def test_delete_dictionary_blocked_by_active_job(db_session: Session): with pytest.raises(ValueError, match="active/scheduled"): DictionaryManager.delete_dictionary(db_session, d.id) -# [/DEF:test_delete_dictionary_blocked_by_active_job:Function] +# #endregion test_delete_dictionary_blocked_by_active_job -# [DEF:test_delete_dictionary_allowed_with_completed_job:Function] -# @PURPOSE: Verify deletion is allowed when only completed/failed jobs reference the dictionary. +# #region test_delete_dictionary_allowed_with_completed_job [TYPE Function] +# @BRIEF Verify deletion is allowed when only completed/failed jobs reference the dictionary. def test_delete_dictionary_allowed_with_completed_job(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", @@ -427,11 +424,11 @@ def test_delete_dictionary_allowed_with_completed_job(db_session: Session): DictionaryManager.delete_dictionary(db_session, d.id) with pytest.raises(ValueError, match="Dictionary not found"): DictionaryManager.get_dictionary(db_session, d.id) -# [/DEF:test_delete_dictionary_allowed_with_completed_job:Function] +# #endregion test_delete_dictionary_allowed_with_completed_job -# [DEF:test_filter_for_batch_no_dictionaries:Function] -# @PURPOSE: Verify filter_for_batch returns empty when job has no dictionaries. +# #region test_filter_for_batch_no_dictionaries [TYPE Function] +# @BRIEF Verify filter_for_batch returns empty when job has no dictionaries. def test_filter_for_batch_no_dictionaries(db_session: Session): job = TranslationJob( name="No Dict Job", @@ -443,11 +440,11 @@ def test_filter_for_batch_no_dictionaries(db_session: Session): result = DictionaryManager.filter_for_batch(db_session, ["hello world"], job.id) assert result == [] -# [/DEF:test_filter_for_batch_no_dictionaries:Function] +# #endregion test_filter_for_batch_no_dictionaries -# [DEF:test_filter_for_batch_matches:Function] -# @PURPOSE: Verify filter_for_batch returns correct matched entries with word-boundary awareness. +# #region test_filter_for_batch_matches [TYPE Function] +# @BRIEF Verify filter_for_batch returns correct matched entries with word-boundary awareness. def test_filter_for_batch_matches(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test Dict", @@ -489,11 +486,11 @@ def test_filter_for_batch_matches(db_session: Session): foo_matches = [m for m in result if m["source_term"] == "foo"] assert len(foo_matches) == 1 assert foo_matches[0]["text_index"] == 2 -# [/DEF:test_filter_for_batch_matches:Function] +# #endregion test_filter_for_batch_matches -# [DEF:test_filter_for_batch_case_insensitive:Function] -# @PURPOSE: Verify filter_for_batch matching is case-insensitive. +# #region test_filter_for_batch_case_insensitive [TYPE Function] +# @BRIEF Verify filter_for_batch matching is case-insensitive. def test_filter_for_batch_case_insensitive(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test Dict", @@ -520,11 +517,11 @@ def test_filter_for_batch_case_insensitive(db_session: Session): assert len(result) == 1 assert result[0]["source_term"] == "Hello World" assert result[0]["target_term"] == "Hola Mundo" -# [/DEF:test_filter_for_batch_case_insensitive:Function] +# #endregion test_filter_for_batch_case_insensitive -# [DEF:test_filter_for_batch_word_boundary:Function] -# @PURPOSE: Verify filter_for_batch respects word boundaries (no substring matching within words). +# #region test_filter_for_batch_word_boundary [TYPE Function] +# @BRIEF Verify filter_for_batch respects word boundaries (no substring matching within words). def test_filter_for_batch_word_boundary(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test Dict", @@ -557,11 +554,11 @@ def test_filter_for_batch_word_boundary(db_session: Session): job.id, ) assert len(result) == 1 -# [/DEF:test_filter_for_batch_word_boundary:Function] +# #endregion test_filter_for_batch_word_boundary -# [DEF:test_filter_for_batch_multi_dictionary_priority:Function] -# @PURPOSE: Verify filter_for_batch respects dictionary link order priority. +# #region test_filter_for_batch_multi_dictionary_priority [TYPE Function] +# @BRIEF Verify filter_for_batch respects dictionary link order priority. def test_filter_for_batch_multi_dictionary_priority(db_session: Session): d1 = DictionaryManager.create_dictionary( db_session, name="Priority1", source_dialect="a", target_dialect="b", @@ -596,11 +593,11 @@ def test_filter_for_batch_multi_dictionary_priority(db_session: Session): assert result[0]["target_term"] == "hola" assert result[1]["dictionary_id"] == d2.id assert result[1]["target_term"] == "bonjour" -# [/DEF:test_filter_for_batch_multi_dictionary_priority:Function] +# #endregion test_filter_for_batch_multi_dictionary_priority -# [DEF:test_normalize_term:Function] -# @PURPOSE: Verify _normalize_term produces lowercase NFC-normalized strings. +# #region test_normalize_term [TYPE Function] +# @BRIEF Verify _normalize_term produces lowercase NFC-normalized strings. def test_normalize_term(): assert _normalize_term("Hello") == "hello" assert _normalize_term("HELLO") == "hello" @@ -609,11 +606,11 @@ def test_normalize_term(): composed = "\u00C9" # É precomposed decomposed = "\u0045\u0301" # E + combining acute assert _normalize_term(composed) == _normalize_term(decomposed) -# [/DEF:test_normalize_term:Function] +# #endregion test_normalize_term -# [DEF:test_detect_delimiter:Function] -# @PURPOSE: Verify _detect_delimiter correctly identifies CSV vs TSV. +# #region test_detect_delimiter [TYPE Function] +# @BRIEF Verify _detect_delimiter correctly identifies CSV vs TSV. def test_detect_delimiter(): csv_content = "source_term,target_term,context_notes\nhello,hola," assert _detect_delimiter(csv_content) == "," @@ -623,11 +620,11 @@ def test_detect_delimiter(): empty_content = "" assert _detect_delimiter(empty_content) == "," # default fallback -# [/DEF:test_detect_delimiter:Function] +# #endregion test_detect_delimiter -# [DEF:test_clear_entries:Function] -# @PURPOSE: Verify clearing all entries for a dictionary. +# #region test_clear_entries [TYPE Function] +# @BRIEF Verify clearing all entries for a dictionary. def test_clear_entries(db_session: Session): d = DictionaryManager.create_dictionary( db_session, name="Test", source_dialect="a", target_dialect="b", @@ -640,5 +637,5 @@ def test_clear_entries(db_session: Session): entries, total = DictionaryManager.list_entries(db_session, d.id) assert total == 0 -# [/DEF:test_clear_entries:Function] -# [/DEF:DictionaryTests:Module] +# #endregion test_clear_entries +# #endregion DictionaryTests diff --git a/backend/src/plugins/translate/__tests__/test_orchestrator.py b/backend/src/plugins/translate/__tests__/test_orchestrator.py index 385d5f2b..04a3dde7 100644 --- a/backend/src/plugins/translate/__tests__/test_orchestrator.py +++ b/backend/src/plugins/translate/__tests__/test_orchestrator.py @@ -1,10 +1,8 @@ -# [DEF:OrchestratorTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: test, translate, orchestrator, events -# @PURPOSE: Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling. -# @LAYER: Test -# @RELATION: BINDS_TO -> [TranslationOrchestrator:Module] -# @RELATION: BINDS_TO -> [TranslationEventLog:Module] +# #region OrchestratorTests [C:3] [TYPE Module] [SEMANTICS test, translate, orchestrator, events] +# @BRIEF Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling. +# @LAYER Test +# @RELATION BINDS_TO -> [TranslationOrchestrator:Module] +# @RELATION BINDS_TO -> [TranslationEventLog:Module] # @TEST_CONTRACT: TranslationOrchestrator -> start_run, execute_run, cancel_run, retry_failed_batches # @TEST_FIXTURE: mock_db -> MagicMock SQLAlchemy session # @TEST_FIXTURE: mock_config_manager -> MagicMock ConfigManager @@ -65,8 +63,8 @@ def mock_preview_session() -> MagicMock: return session -# [DEF:TestTranslationEventLog:Class] -# @PURPOSE: Tests for TranslationEventLog terminal event invariant. +# #region TestTranslationEventLog [TYPE Class] +# @BRIEF Tests for TranslationEventLog terminal event invariant. class TestTranslationEventLog: # [DEF:test_log_event_creates_record:Function] @@ -142,11 +140,11 @@ class TestTranslationEventLog: log = TranslationEventLog(db) result = log.prune_expired(retention_days=90) assert result["pruned"] >= 0 -# [/DEF:TestTranslationEventLog:Class] +# #endregion TestTranslationEventLog -# [DEF:TestTranslationOrchestrator:Class] -# @PURPOSE: Tests for TranslationOrchestrator run lifecycle. +# #region TestTranslationOrchestrator [TYPE Class] +# @BRIEF Tests for TranslationOrchestrator run lifecycle. class TestTranslationOrchestrator: # [DEF:test_start_run_success:Function] @@ -372,5 +370,5 @@ class TestTranslationOrchestrator: assert total == 1 assert len(runs) == 1 assert runs[0]["id"] == "run-1" -# [/DEF:TestTranslationOrchestrator:Class] -# [/DEF:OrchestratorTests:Module] +# #endregion TestTranslationOrchestrator +# #endregion OrchestratorTests diff --git a/backend/src/plugins/translate/__tests__/test_preview.py b/backend/src/plugins/translate/__tests__/test_preview.py index e95e6b73..09daf6a9 100644 --- a/backend/src/plugins/translate/__tests__/test_preview.py +++ b/backend/src/plugins/translate/__tests__/test_preview.py @@ -1,9 +1,7 @@ -# [DEF:TranslationPreviewTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: test, translate, preview, session -# @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate. -# @LAYER: Test -# @RELATION: BINDS_TO -> [TranslationPreview:Module] +# #region TranslationPreviewTests [C:3] [TYPE Module] [SEMANTICS test, translate, preview, session] +# @BRIEF Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate. +# @LAYER Test +# @RELATION BINDS_TO -> [TranslationPreview:Module] from unittest.mock import MagicMock, patch, PropertyMock from datetime import datetime, timezone, timedelta @@ -19,8 +17,8 @@ from src.models.translate import ( from src.plugins.translate.preview import TranslationPreview, TokenEstimator -# [DEF:_make_mock_job:Function] -# @PURPOSE: Create a mock TranslationJob with test config. +# #region _make_mock_job [TYPE Function] +# @BRIEF Create a mock TranslationJob with test config. def _make_mock_job(**overrides): job = MagicMock(spec=TranslationJob) job.id = overrides.get("id", "job-123") @@ -37,11 +35,11 @@ def _make_mock_job(**overrides): job.upsert_strategy = overrides.get("upsert_strategy", "MERGE") job.status = overrides.get("status", "DRAFT") return job -# [/DEF:_make_mock_job:Function] +# #endregion _make_mock_job -# [DEF:_make_mock_provider:Function] -# @PURPOSE: Create a mock LLMProvider. +# #region _make_mock_provider [TYPE Function] +# @BRIEF Create a mock LLMProvider. def _make_mock_provider(**overrides): provider = MagicMock() provider.id = overrides.get("id", "provider-1") @@ -50,15 +48,15 @@ def _make_mock_provider(**overrides): provider.default_model = overrides.get("default_model", "gpt-4o-mini") provider.api_key = "encrypted-key-123" return provider -# [/DEF:_make_mock_provider:Function] +# #endregion _make_mock_provider -# [DEF:TestTranslationPreview:Class] -# @PURPOSE: Test suite for TranslationPreview service. +# #region TestTranslationPreview [TYPE Class] +# @BRIEF Test suite for TranslationPreview service. class TestTranslationPreview: - # [DEF:test_preview_valid_job:Function] - # @PURPOSE: Verify preview creates session and records successfully. + # #region test_preview_valid_job [TYPE Function] + # @BRIEF Verify preview creates session and records successfully. def test_preview_valid_job(self): """Preview with valid job should create session and return records.""" db = MagicMock() @@ -161,10 +159,10 @@ class TestTranslationPreview: # Verify session was added to DB assert db.add.called assert db.commit.called - # [/DEF:test_preview_valid_job:Function] + # #endregion test_preview_valid_job - # [DEF:test_preview_with_dictionary:Function] - # @PURPOSE: Verify glossary entries from dictionary are included in LLM prompt. + # #region test_preview_with_dictionary [TYPE Function] + # @BRIEF Verify glossary entries from dictionary are included in LLM prompt. def test_preview_with_dictionary(self): """Preview should include dictionary glossary in the prompt sent to LLM.""" db = MagicMock() @@ -220,10 +218,10 @@ class TestTranslationPreview: # Verify LLM was called mock_llm.assert_called_once() assert len(result["records"]) == 1 - # [/DEF:test_preview_with_dictionary:Function] + # #endregion test_preview_with_dictionary - # [DEF:test_preview_row_approve:Function] - # @PURPOSE: Verify approving a preview row changes its status. + # #region test_preview_row_approve [TYPE Function] + # @BRIEF Verify approving a preview row changes its status. def test_preview_row_approve(self): """Approve action should set record status to APPROVED.""" db = MagicMock() @@ -259,10 +257,10 @@ class TestTranslationPreview: assert record.status == "APPROVED" assert result["status"] == "APPROVED" assert db.commit.called - # [/DEF:test_preview_row_approve:Function] + # #endregion test_preview_row_approve - # [DEF:test_preview_row_reject:Function] - # @PURPOSE: Verify rejecting a preview row changes its status. + # #region test_preview_row_reject [TYPE Function] + # @BRIEF Verify rejecting a preview row changes its status. def test_preview_row_reject(self): """Reject action should set record status to REJECTED.""" db = MagicMock() @@ -292,10 +290,10 @@ class TestTranslationPreview: assert record.status == "REJECTED" assert result["status"] == "REJECTED" - # [/DEF:test_preview_row_reject:Function] + # #endregion test_preview_row_reject - # [DEF:test_preview_row_edit:Function] - # @PURPOSE: Verify editing a preview row updates its translation and status. + # #region test_preview_row_edit [TYPE Function] + # @BRIEF Verify editing a preview row updates its translation and status. def test_preview_row_edit(self): """Edit action should update translation and set status to APPROVED.""" db = MagicMock() @@ -328,10 +326,10 @@ class TestTranslationPreview: assert record.status == "APPROVED" assert result["status"] == "APPROVED" assert result["target_sql"] == "edited translation" - # [/DEF:test_preview_row_edit:Function] + # #endregion test_preview_row_edit - # [DEF:test_preview_row_invalid_action:Function] - # @PURPOSE: Verify invalid action raises ValueError. + # #region test_preview_row_invalid_action [TYPE Function] + # @BRIEF Verify invalid action raises ValueError. def test_preview_row_invalid_action(self): """Invalid action should raise ValueError.""" db = MagicMock() @@ -357,10 +355,10 @@ class TestTranslationPreview: row_id="record-1", action="invalid_action", ) - # [/DEF:test_preview_row_invalid_action:Function] + # #endregion test_preview_row_invalid_action - # [DEF:test_preview_accept_session:Function] - # @PURPOSE: Verify accepting a preview session gates execution. + # #region test_preview_accept_session [TYPE Function] + # @BRIEF Verify accepting a preview session gates execution. def test_preview_accept_session(self): """Accept should set session status to APPLIED and return records.""" db = MagicMock() @@ -395,10 +393,10 @@ class TestTranslationPreview: assert len(result["records"]) == 1 assert result["records"][0]["status"] == "APPROVED" assert db.commit.called - # [/DEF:test_preview_accept_session:Function] + # #endregion test_preview_accept_session - # [DEF:test_preview_no_active_session:Function] - # @PURPOSE: Verify accept raises error when no active session. + # #region test_preview_no_active_session [TYPE Function] + # @BRIEF Verify accept raises error when no active session. def test_preview_no_active_session(self): """Accept without active session should raise ValueError.""" db = MagicMock() @@ -409,10 +407,10 @@ class TestTranslationPreview: preview_service = TranslationPreview(db, config_manager, "test-user") with pytest.raises(ValueError, match="No active preview session"): preview_service.accept_preview_session(job_id="job-123") - # [/DEF:test_preview_no_active_session:Function] + # #endregion test_preview_no_active_session - # [DEF:test_cost_estimation:Function] - # @PURPOSE: Verify token and cost estimation methods. + # #region test_cost_estimation [TYPE Function] + # @BRIEF Verify token and cost estimation methods. def test_cost_estimation(self): """Token and cost estimation should return reasonable values.""" prompt = "Translate this text from English to Russian. " * 100 @@ -428,10 +426,10 @@ class TestTranslationPreview: cost_default = TokenEstimator.estimate_cost(1000) assert cost_default == 0.002 - # [/DEF:test_cost_estimation:Function] + # #endregion test_cost_estimation - # [DEF:test_preview_parse_llm_response:Function] - # @PURPOSE: Verify LLM JSON response parsing. + # #region test_preview_parse_llm_response [TYPE Function] + # @BRIEF Verify LLM JSON response parsing. def test_preview_parse_llm_response(self): """Parse LLM response should extract translations keyed by row_id.""" response = json.dumps({ @@ -442,19 +440,19 @@ class TestTranslationPreview: }) result = TranslationPreview._parse_llm_response(response, 2) assert result == {"0": "Привет", "1": "Мир"} - # [/DEF:test_preview_parse_llm_response:Function] + # #endregion test_preview_parse_llm_response - # [DEF:test_preview_parse_llm_response_with_code_block:Function] - # @PURPOSE: Verify LLM response parsing handles markdown code blocks. + # #region test_preview_parse_llm_response_with_code_block [TYPE Function] + # @BRIEF Verify LLM response parsing handles markdown code blocks. def test_preview_parse_llm_response_with_code_block(self): """Parse LLM response should handle markdown code block wrapping.""" response = "```json\n{\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Test\"}\n ]\n}\n```" result = TranslationPreview._parse_llm_response(response, 1) assert result == {"0": "Test"} - # [/DEF:test_preview_parse_llm_response_with_code_block:Function] + # #endregion test_preview_parse_llm_response_with_code_block - # [DEF:test_preview_compute_config_hash:Function] - # @PURPOSE: Verify config hash computation is deterministic. + # #region test_preview_compute_config_hash [TYPE Function] + # @BRIEF Verify config hash computation is deterministic. def test_preview_compute_config_hash(self): """Config hash should be deterministic and non-empty.""" job = _make_mock_job() @@ -462,10 +460,10 @@ class TestTranslationPreview: hash2 = TranslationPreview._compute_config_hash(job) assert hash1 == hash2 assert len(hash1) == 16 - # [/DEF:test_preview_compute_config_hash:Function] + # #endregion test_preview_compute_config_hash - # [DEF:test_preview_missing_datasource:Function] - # @PURPOSE: Verify error when job has no datasource configured. + # #region test_preview_missing_datasource [TYPE Function] + # @BRIEF Verify error when job has no datasource configured. def test_preview_missing_datasource(self): """Preview should fail if job has no datasource.""" db = MagicMock() @@ -477,10 +475,10 @@ class TestTranslationPreview: preview_service = TranslationPreview(db, config_manager, "test-user") with pytest.raises(ValueError, match="source datasource"): preview_service.preview_rows(job_id="job-123") - # [/DEF:test_preview_missing_datasource:Function] + # #endregion test_preview_missing_datasource - # [DEF:test_preview_missing_translation_column:Function] - # @PURPOSE: Verify error when job has no translation column. + # #region test_preview_missing_translation_column [TYPE Function] + # @BRIEF Verify error when job has no translation column. def test_preview_missing_translation_column(self): """Preview should fail if job has no translation column.""" db = MagicMock() @@ -492,9 +490,9 @@ class TestTranslationPreview: preview_service = TranslationPreview(db, config_manager, "test-user") with pytest.raises(ValueError, match="translation column"): preview_service.preview_rows(job_id="job-123") - # [/DEF:test_preview_missing_translation_column:Function] + # #endregion test_preview_missing_translation_column -# [/DEF:TestTranslationPreview:Class] +# #endregion TestTranslationPreview -# [/DEF:TranslationPreviewTests:Module] +# #endregion TranslationPreviewTests diff --git a/backend/src/plugins/translate/__tests__/test_sql_generator.py b/backend/src/plugins/translate/__tests__/test_sql_generator.py index 21e721a1..7ed9e58b 100644 --- a/backend/src/plugins/translate/__tests__/test_sql_generator.py +++ b/backend/src/plugins/translate/__tests__/test_sql_generator.py @@ -1,9 +1,7 @@ -# [DEF:SQLGeneratorTests:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: test, translate, sql_generator -# @PURPOSE: Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety. -# @LAYER: Test -# @RELATION: BINDS_TO -> [SQLGenerator:Module] +# #region SQLGeneratorTests [C:3] [TYPE Module] [SEMANTICS test, translate, sql_generator] +# @BRIEF Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety. +# @LAYER Test +# @RELATION BINDS_TO -> [SQLGenerator:Module] # @TEST_CONTRACT: SQLGenerator.generate -> SQL string, row_count # @TEST_FIXTURE: sample_rows -> [{"col1": "val1", "col2": 42}, {"col1": "val2", "col2": 99}] # @TEST_EDGE: empty_rows -> raises ValueError @@ -40,8 +38,8 @@ def sample_rows() -> List[Dict[str, Any]]: ] -# [DEF:TestQuoteIdentifier:Class] -# @PURPOSE: Tests for identifier quoting per dialect. +# #region TestQuoteIdentifier [TYPE Class] +# @BRIEF Tests for identifier quoting per dialect. class TestQuoteIdentifier: # [DEF:test_quote_postgresql:Function] # @PURPOSE: PostgreSQL uses double quotes. @@ -64,11 +62,11 @@ class TestQuoteIdentifier: def test_quote_schema_table(self) -> None: assert _quote_identifier("my_schema", "postgresql") == '"my_schema"' assert _quote_identifier("my_table", "postgresql") == '"my_table"' -# [/DEF:TestQuoteIdentifier:Class] +# #endregion TestQuoteIdentifier -# [DEF:TestEncodeSqlValue:Class] -# @PURPOSE: Tests for SQL value encoding. +# #region TestEncodeSqlValue [TYPE Class] +# @BRIEF Tests for SQL value encoding. class TestEncodeSqlValue: # [DEF:test_null:Function] # @PURPOSE: None encodes as NULL. @@ -131,11 +129,11 @@ class TestEncodeSqlValue: def test_postgresql_timestamp_not_normalized(self) -> None: result = _encode_sql_value("1726358400000.0", dialect="postgresql") assert result == "'1726358400000.0'" -# [/DEF:TestEncodeSqlValue:Class] +# #endregion TestEncodeSqlValue -# [DEF:TestNormalizeTimestampValue:Class] -# @PURPOSE: Tests for Unix timestamp detection and conversion. +# #region TestNormalizeTimestampValue [TYPE Class] +# @BRIEF Tests for Unix timestamp detection and conversion. class TestNormalizeTimestampValue: # [DEF:test_millis_timestamp:Function] # @PURPOSE: Millisecond Unix timestamps convert to YYYY-MM-DD. @@ -166,11 +164,11 @@ class TestNormalizeTimestampValue: # @PURPOSE: Integer seconds timestamp converts correctly. def test_int_seconds(self) -> None: assert _normalize_timestamp_value(1726358400) == "2024-09-15" -# [/DEF:TestNormalizeTimestampValue:Class] +# #endregion TestNormalizeTimestampValue -# [DEF:TestGenerateInsertSql:Class] -# @PURPOSE: Tests for plain INSERT SQL generation. +# #region TestGenerateInsertSql [TYPE Class] +# @BRIEF Tests for plain INSERT SQL generation. class TestGenerateInsertSql: # [DEF:test_basic_insert:Function] # @PURPOSE: Generates basic INSERT with VALUES. @@ -248,11 +246,11 @@ class TestGenerateInsertSql: # The single quote should be doubled, preventing injection assert "'';" in sql or "''" in sql assert "DROP TABLE" in sql # It's literal data, not a command -# [/DEF:TestGenerateInsertSql:Class] +# #endregion TestGenerateInsertSql -# [DEF:TestGenerateUpsertSql:Class] -# @PURPOSE: Tests for PostgreSQL UPSERT SQL generation. +# #region TestGenerateUpsertSql [TYPE Class] +# @BRIEF Tests for PostgreSQL UPSERT SQL generation. class TestGenerateUpsertSql: # [DEF:test_basic_upsert:Function] # @PURPOSE: Generates INSERT ... ON CONFLICT DO UPDATE. @@ -294,11 +292,11 @@ class TestGenerateUpsertSql: key_columns=[], rows=sample_rows, ) -# [/DEF:TestGenerateUpsertSql:Class] +# #endregion TestGenerateUpsertSql -# [DEF:TestSQLGenerator:Class] -# @PURPOSE: Tests for the full SQLGenerator class. +# #region TestSQLGenerator [TYPE Class] +# @BRIEF Tests for the full SQLGenerator class. class TestSQLGenerator: # [DEF:test_postgresql_insert:Function] # @PURPOSE: PostgreSQL dialect generates proper INSERT. @@ -390,5 +388,5 @@ class TestSQLGenerator: assert len(statements) > 1 # Should be split into multiple statements total_count = sum(count for _, count in statements) assert total_count == 10 -# [/DEF:TestSQLGenerator:Class] -# [/DEF:SQLGeneratorTests:Module] +# #endregion TestSQLGenerator +# #endregion SQLGeneratorTests diff --git a/backend/src/schemas/__tests__/test_settings_and_health_schemas.py b/backend/src/schemas/__tests__/test_settings_and_health_schemas.py index 5aeff6aa..212703f0 100644 --- a/backend/src/schemas/__tests__/test_settings_and_health_schemas.py +++ b/backend/src/schemas/__tests__/test_settings_and_health_schemas.py @@ -1,7 +1,6 @@ -# [DEF:TestSettingsAndHealthSchemas:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @PURPOSE: Regression tests for settings and health schema contracts updated in 026 fix batch. +# #region TestSettingsAndHealthSchemas [C:3] [TYPE Module] +# @BRIEF Regression tests for settings and health schema contracts updated in 026 fix batch. +# @RELATION BELONGS_TO -> [SrcRoot] import pytest from pydantic import ValidationError @@ -10,9 +9,9 @@ from src.schemas.health import DashboardHealthItem from src.schemas.settings import ValidationPolicyCreate -# [DEF:test_validation_policy_create_accepts_structured_custom_channels:Function] -# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas -# @PURPOSE: Ensure policy schema accepts structured custom channel objects with type/target fields. +# #region test_validation_policy_create_accepts_structured_custom_channels [TYPE Function] +# @BRIEF Ensure policy schema accepts structured custom channel objects with type/target fields. +# @RELATION BINDS_TO -> [TestSettingsAndHealthSchemas] def test_validation_policy_create_accepts_structured_custom_channels(): payload = { "name": "Daily Health", @@ -32,12 +31,12 @@ def test_validation_policy_create_accepts_structured_custom_channels(): assert len(policy.custom_channels) == 1 assert policy.custom_channels[0].type == "SLACK" assert policy.custom_channels[0].target == "#alerts" -# [/DEF:test_validation_policy_create_accepts_structured_custom_channels:Function] +# #endregion test_validation_policy_create_accepts_structured_custom_channels -# [DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function] -# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas -# @PURPOSE: Ensure legacy list[str] custom channel payload is rejected by typed channel contract. +# #region test_validation_policy_create_rejects_legacy_string_custom_channels [TYPE Function] +# @BRIEF Ensure legacy list[str] custom channel payload is rejected by typed channel contract. +# @RELATION BINDS_TO -> [TestSettingsAndHealthSchemas] def test_validation_policy_create_rejects_legacy_string_custom_channels(): payload = { "name": "Daily Health", @@ -52,12 +51,12 @@ def test_validation_policy_create_rejects_legacy_string_custom_channels(): with pytest.raises(ValidationError): ValidationPolicyCreate(**payload) -# [/DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function] +# #endregion test_validation_policy_create_rejects_legacy_string_custom_channels -# [DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function] -# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas -# @PURPOSE: Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses. +# #region test_dashboard_health_item_status_accepts_only_whitelisted_values [TYPE Function] +# @BRIEF Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses. +# @RELATION BINDS_TO -> [TestSettingsAndHealthSchemas] def test_dashboard_health_item_status_accepts_only_whitelisted_values(): valid = DashboardHealthItem( dashboard_id="dash-1", @@ -82,7 +81,7 @@ def test_dashboard_health_item_status_accepts_only_whitelisted_values(): status="FAIL ", last_check="2026-03-10T10:00:00", ) -# [/DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function] +# #endregion test_dashboard_health_item_status_accepts_only_whitelisted_values -# [/DEF:TestSettingsAndHealthSchemas:Module] \ No newline at end of file +# #endregion TestSettingsAndHealthSchemas diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py index 05dcc89a..cd6b91b0 100644 --- a/backend/src/schemas/auth.py +++ b/backend/src/schemas/auth.py @@ -1,12 +1,10 @@ -# [DEF:AuthSchemas:Module] +# #region AuthSchemas [C:3] [TYPE Module] [SEMANTICS auth, schemas, pydantic, user, token] +# @BRIEF Pydantic schemas for authentication requests and responses. +# @LAYER API +# @INVARIANT Sensitive fields like password must not be included in response schemas. +# @RELATION DEPENDS_ON -> [pydantic] # -# @COMPLEXITY: 3 -# @SEMANTICS: auth, schemas, pydantic, user, token -# @PURPOSE: Pydantic schemas for authentication requests and responses. -# @LAYER: API -# @RELATION: DEPENDS_ON -> pydantic # -# @INVARIANT: Sensitive fields like password must not be included in response schemas. # [SECTION: IMPORTS] from typing import List, Optional @@ -15,31 +13,28 @@ from datetime import datetime # [/SECTION] -# [DEF:Token:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents a JWT access token response. +# #region Token [C:1] [TYPE Class] +# @BRIEF Represents a JWT access token response. class Token(BaseModel): access_token: str token_type: str -# [/DEF:Token:Class] +# #endregion Token -# [DEF:TokenData:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents the data encoded in a JWT token. +# #region TokenData [C:1] [TYPE Class] +# @BRIEF Represents the data encoded in a JWT token. class TokenData(BaseModel): username: Optional[str] = None scopes: List[str] = [] -# [/DEF:TokenData:Class] +# #endregion TokenData -# [DEF:PermissionSchema:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Represents a permission in API responses. +# #region PermissionSchema [C:1] [TYPE Class] +# @BRIEF Represents a permission in API responses. class PermissionSchema(BaseModel): id: Optional[str] = None resource: str @@ -49,11 +44,11 @@ class PermissionSchema(BaseModel): from_attributes = True -# [/DEF:PermissionSchema:Class] +# #endregion PermissionSchema -# [DEF:RoleSchema:Class] -# @PURPOSE: Represents a role in API responses. +# #region RoleSchema [TYPE Class] +# @BRIEF Represents a role in API responses. class RoleSchema(BaseModel): id: str name: str @@ -64,33 +59,33 @@ class RoleSchema(BaseModel): from_attributes = True -# [/DEF:RoleSchema:Class] +# #endregion RoleSchema -# [DEF:RoleCreate:Class] -# @PURPOSE: Schema for creating a new role. +# #region RoleCreate [TYPE Class] +# @BRIEF Schema for creating a new role. class RoleCreate(BaseModel): name: str description: Optional[str] = None permissions: List[str] = [] # List of permission IDs or "resource:action" strings -# [/DEF:RoleCreate:Class] +# #endregion RoleCreate -# [DEF:RoleUpdate:Class] -# @PURPOSE: Schema for updating an existing role. +# #region RoleUpdate [TYPE Class] +# @BRIEF Schema for updating an existing role. class RoleUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None permissions: Optional[List[str]] = None -# [/DEF:RoleUpdate:Class] +# #endregion RoleUpdate -# [DEF:ADGroupMappingSchema:Class] -# @PURPOSE: Represents an AD Group to Role mapping in API responses. +# #region ADGroupMappingSchema [TYPE Class] +# @BRIEF Represents an AD Group to Role mapping in API responses. class ADGroupMappingSchema(BaseModel): id: str ad_group: str @@ -100,42 +95,42 @@ class ADGroupMappingSchema(BaseModel): from_attributes = True -# [/DEF:ADGroupMappingSchema:Class] +# #endregion ADGroupMappingSchema -# [DEF:ADGroupMappingCreate:Class] -# @PURPOSE: Schema for creating an AD Group mapping. +# #region ADGroupMappingCreate [TYPE Class] +# @BRIEF Schema for creating an AD Group mapping. class ADGroupMappingCreate(BaseModel): ad_group: str role_id: str -# [/DEF:ADGroupMappingCreate:Class] +# #endregion ADGroupMappingCreate -# [DEF:UserBase:Class] -# @PURPOSE: Base schema for user data. +# #region UserBase [TYPE Class] +# @BRIEF Base schema for user data. class UserBase(BaseModel): username: str email: Optional[EmailStr] = None is_active: bool = True -# [/DEF:UserBase:Class] +# #endregion UserBase -# [DEF:UserCreate:Class] -# @PURPOSE: Schema for creating a new user. +# #region UserCreate [TYPE Class] +# @BRIEF Schema for creating a new user. class UserCreate(UserBase): password: str roles: List[str] = [] -# [/DEF:UserCreate:Class] +# #endregion UserCreate -# [DEF:UserUpdate:Class] -# @PURPOSE: Schema for updating an existing user. +# #region UserUpdate [TYPE Class] +# @BRIEF Schema for updating an existing user. class UserUpdate(BaseModel): email: Optional[EmailStr] = None password: Optional[str] = None @@ -143,11 +138,11 @@ class UserUpdate(BaseModel): roles: Optional[List[str]] = None -# [/DEF:UserUpdate:Class] +# #endregion UserUpdate -# [DEF:User:Class] -# @PURPOSE: Schema for user data in API responses. +# #region User [TYPE Class] +# @BRIEF Schema for user data in API responses. class User(UserBase): id: str auth_source: str @@ -159,6 +154,6 @@ class User(UserBase): from_attributes = True -# [/DEF:User:Class] +# #endregion User -# [/DEF:AuthSchemas:Module] +# #endregion AuthSchemas diff --git a/backend/src/schemas/dataset_review.py b/backend/src/schemas/dataset_review.py index 028242d8..2ac2a4f0 100644 --- a/backend/src/schemas/dataset_review.py +++ b/backend/src/schemas/dataset_review.py @@ -1,10 +1,8 @@ -# [DEF:DatasetReviewSchemas:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: dataset_review, schemas, pydantic, session, profile, findings -# @PURPOSE: Thin facade re-exporting all dataset review API schemas from decomposed sub-modules. -# @LAYER: API -# @RATIONALE: Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules. -# @REJECTED: Keeping all schemas in a single file because it exceeded the fractal limit. +# #region DatasetReviewSchemas [C:2] [TYPE Module] [SEMANTICS dataset_review, schemas, pydantic, session, profile, findings] +# @BRIEF Thin facade re-exporting all dataset review API schemas from decomposed sub-modules. +# @LAYER API +# @RATIONALE Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules. +# @REJECTED Keeping all schemas in a single file because it exceeded the fractal limit. from src.schemas.dataset_review_pkg._dtos import ( # noqa: F401 SessionCollaboratorDto, @@ -27,4 +25,4 @@ from src.schemas.dataset_review_pkg._composites import ( # noqa: F401 SessionSummary, SessionDetail, ) -# [/DEF:DatasetReviewSchemas:Module] +# #endregion DatasetReviewSchemas diff --git a/backend/src/schemas/dataset_review_pkg/_composites.py b/backend/src/schemas/dataset_review_pkg/_composites.py index fd58ce41..e36b5372 100644 --- a/backend/src/schemas/dataset_review_pkg/_composites.py +++ b/backend/src/schemas/dataset_review_pkg/_composites.py @@ -1,8 +1,7 @@ -# [DEF:DatasetReviewSchemaComposites:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DatasetReviewSchemaDtos] +# #region DatasetReviewSchemaComposites [C:2] [TYPE Module] +# @BRIEF Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses. +# @LAYER API +# @RELATION DEPENDS_ON -> [DatasetReviewSchemaDtos] from datetime import datetime from typing import Any, List, Optional @@ -32,9 +31,8 @@ from src.schemas.dataset_review_pkg._dtos import ( ) -# [DEF:ClarificationOptionDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Clarification option DTO. +# #region ClarificationOptionDto [C:1] [TYPE Class] +# @BRIEF Clarification option DTO. class ClarificationOptionDto(BaseModel): option_id: str question_id: str @@ -47,12 +45,11 @@ class ClarificationOptionDto(BaseModel): from_attributes = True -# [/DEF:ClarificationOptionDto:Class] +# #endregion ClarificationOptionDto -# [DEF:ClarificationAnswerDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Clarification answer DTO with feedback. +# #region ClarificationAnswerDto [C:1] [TYPE Class] +# @BRIEF Clarification answer DTO with feedback. class ClarificationAnswerDto(BaseModel): answer_id: str question_id: str @@ -67,12 +64,11 @@ class ClarificationAnswerDto(BaseModel): from_attributes = True -# [/DEF:ClarificationAnswerDto:Class] +# #endregion ClarificationAnswerDto -# [DEF:ClarificationQuestionDto:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Clarification question DTO with nested options and answer. +# #region ClarificationQuestionDto [C:2] [TYPE Class] +# @BRIEF Clarification question DTO with nested options and answer. class ClarificationQuestionDto(BaseModel): question_id: str clarification_session_id: str @@ -91,12 +87,11 @@ class ClarificationQuestionDto(BaseModel): from_attributes = True -# [/DEF:ClarificationQuestionDto:Class] +# #endregion ClarificationQuestionDto -# [DEF:ClarificationSessionDto:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Clarification session DTO with nested questions. +# #region ClarificationSessionDto [C:2] [TYPE Class] +# @BRIEF Clarification session DTO with nested questions. class ClarificationSessionDto(BaseModel): clarification_session_id: str session_id: str @@ -114,12 +109,11 @@ class ClarificationSessionDto(BaseModel): from_attributes = True -# [/DEF:ClarificationSessionDto:Class] +# #endregion ClarificationSessionDto -# [DEF:CompiledPreviewDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Compiled preview DTO with fingerprint and session version. +# #region CompiledPreviewDto [C:1] [TYPE Class] +# @BRIEF Compiled preview DTO with fingerprint and session version. class CompiledPreviewDto(BaseModel): preview_id: str session_id: str @@ -137,12 +131,11 @@ class CompiledPreviewDto(BaseModel): from_attributes = True -# [/DEF:CompiledPreviewDto:Class] +# #endregion CompiledPreviewDto -# [DEF:DatasetRunContextDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Run context DTO with launch audit data and session version. +# #region DatasetRunContextDto [C:1] [TYPE Class] +# @BRIEF Run context DTO with launch audit data and session version. class DatasetRunContextDto(BaseModel): run_context_id: str session_id: str @@ -164,12 +157,11 @@ class DatasetRunContextDto(BaseModel): from_attributes = True -# [/DEF:DatasetRunContextDto:Class] +# #endregion DatasetRunContextDto -# [DEF:SessionSummary:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Lightweight session summary DTO for list responses. +# #region SessionSummary [C:2] [TYPE Class] +# @BRIEF Lightweight session summary DTO for list responses. class SessionSummary(BaseModel): session_id: str user_id: str @@ -192,13 +184,12 @@ class SessionSummary(BaseModel): from_attributes = True -# [/DEF:SessionSummary:Class] +# #endregion SessionSummary -# [DEF:SessionDetail:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Full session detail DTO with all nested aggregates for detail views. -# @RELATION: INHERITS -> [SessionSummary] +# #region SessionDetail [C:2] [TYPE Class] +# @BRIEF Full session detail DTO with all nested aggregates for detail views. +# @RELATION INHERITS -> [SessionSummary] class SessionDetail(SessionSummary): collaborators: List[SessionCollaboratorDto] = [] profile: Optional[DatasetProfileDto] = None @@ -213,7 +204,7 @@ class SessionDetail(SessionSummary): run_contexts: List[DatasetRunContextDto] = [] -# [/DEF:SessionDetail:Class] +# #endregion SessionDetail -# [/DEF:DatasetReviewSchemaComposites:Module] +# #endregion DatasetReviewSchemaComposites diff --git a/backend/src/schemas/dataset_review_pkg/_dtos.py b/backend/src/schemas/dataset_review_pkg/_dtos.py index deefce98..1b2b4a78 100644 --- a/backend/src/schemas/dataset_review_pkg/_dtos.py +++ b/backend/src/schemas/dataset_review_pkg/_dtos.py @@ -1,8 +1,7 @@ -# [DEF:DatasetReviewSchemaDtos:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads. -# @LAYER: API -# @RELATION: DEPENDS_ON -> [DatasetReviewModels] +# #region DatasetReviewSchemaDtos [C:2] [TYPE Module] +# @BRIEF Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads. +# @LAYER API +# @RELATION DEPENDS_ON -> [DatasetReviewModels] from datetime import datetime from typing import List, Optional, Any @@ -38,9 +37,8 @@ from src.models.dataset_review import ( ) -# [DEF:SessionCollaboratorDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Collaborator DTO for session access control. +# #region SessionCollaboratorDto [C:1] [TYPE Class] +# @BRIEF Collaborator DTO for session access control. class SessionCollaboratorDto(BaseModel): user_id: str role: SessionCollaboratorRole @@ -50,12 +48,11 @@ class SessionCollaboratorDto(BaseModel): from_attributes = True -# [/DEF:SessionCollaboratorDto:Class] +# #endregion SessionCollaboratorDto -# [DEF:DatasetProfileDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Dataset profile DTO with business summary and confidence metadata. +# #region DatasetProfileDto [C:1] [TYPE Class] +# @BRIEF Dataset profile DTO with business summary and confidence metadata. class DatasetProfileDto(BaseModel): profile_id: str session_id: str @@ -79,12 +76,11 @@ class DatasetProfileDto(BaseModel): from_attributes = True -# [/DEF:DatasetProfileDto:Class] +# #endregion DatasetProfileDto -# [DEF:ValidationFindingDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Validation finding DTO with resolution tracking. +# #region ValidationFindingDto [C:1] [TYPE Class] +# @BRIEF Validation finding DTO with resolution tracking. class ValidationFindingDto(BaseModel): finding_id: str session_id: str @@ -103,12 +99,11 @@ class ValidationFindingDto(BaseModel): from_attributes = True -# [/DEF:ValidationFindingDto:Class] +# #endregion ValidationFindingDto -# [DEF:SemanticSourceDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Semantic source DTO with trust level and status. +# #region SemanticSourceDto [C:1] [TYPE Class] +# @BRIEF Semantic source DTO with trust level and status. class SemanticSourceDto(BaseModel): source_id: str session_id: str @@ -125,12 +120,11 @@ class SemanticSourceDto(BaseModel): from_attributes = True -# [/DEF:SemanticSourceDto:Class] +# #endregion SemanticSourceDto -# [DEF:SemanticCandidateDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Semantic candidate DTO with match type and confidence score. +# #region SemanticCandidateDto [C:1] [TYPE Class] +# @BRIEF Semantic candidate DTO with match type and confidence score. class SemanticCandidateDto(BaseModel): candidate_id: str field_id: str @@ -148,12 +142,11 @@ class SemanticCandidateDto(BaseModel): from_attributes = True -# [/DEF:SemanticCandidateDto:Class] +# #endregion SemanticCandidateDto -# [DEF:SemanticFieldEntryDto:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Semantic field entry DTO with nested candidates and session version. +# #region SemanticFieldEntryDto [C:2] [TYPE Class] +# @BRIEF Semantic field entry DTO with nested candidates and session version. class SemanticFieldEntryDto(BaseModel): field_id: str session_id: str @@ -180,12 +173,11 @@ class SemanticFieldEntryDto(BaseModel): from_attributes = True -# [/DEF:SemanticFieldEntryDto:Class] +# #endregion SemanticFieldEntryDto -# [DEF:ImportedFilterDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Imported filter DTO with confidence and recovery status. +# #region ImportedFilterDto [C:1] [TYPE Class] +# @BRIEF Imported filter DTO with confidence and recovery status. class ImportedFilterDto(BaseModel): filter_id: str session_id: str @@ -206,12 +198,11 @@ class ImportedFilterDto(BaseModel): from_attributes = True -# [/DEF:ImportedFilterDto:Class] +# #endregion ImportedFilterDto -# [DEF:TemplateVariableDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Template variable DTO with mapping status. +# #region TemplateVariableDto [C:1] [TYPE Class] +# @BRIEF Template variable DTO with mapping status. class TemplateVariableDto(BaseModel): variable_id: str session_id: str @@ -228,12 +219,11 @@ class TemplateVariableDto(BaseModel): from_attributes = True -# [/DEF:TemplateVariableDto:Class] +# #endregion TemplateVariableDto -# [DEF:ExecutionMappingDto:Class] -# @COMPLEXITY: 1 -# @PURPOSE: Execution mapping DTO with approval state and session version. +# #region ExecutionMappingDto [C:1] [TYPE Class] +# @BRIEF Execution mapping DTO with approval state and session version. class ExecutionMappingDto(BaseModel): mapping_id: str session_id: str @@ -256,7 +246,7 @@ class ExecutionMappingDto(BaseModel): from_attributes = True -# [/DEF:ExecutionMappingDto:Class] +# #endregion ExecutionMappingDto -# [/DEF:DatasetReviewSchemaDtos:Module] +# #endregion DatasetReviewSchemaDtos diff --git a/backend/src/schemas/health.py b/backend/src/schemas/health.py index 862f9523..d4bfce19 100644 --- a/backend/src/schemas/health.py +++ b/backend/src/schemas/health.py @@ -1,17 +1,15 @@ -# [DEF:HealthSchemas:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: health, schemas, pydantic -# @PURPOSE: Pydantic schemas for dashboard health summary. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> pydantic +# #region HealthSchemas [C:3] [TYPE Module] [SEMANTICS health, schemas, pydantic] +# @BRIEF Pydantic schemas for dashboard health summary. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [pydantic] from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime -# [DEF:DashboardHealthItem:Class] -# @PURPOSE: Represents the latest health status of a single dashboard. +# #region DashboardHealthItem [TYPE Class] +# @BRIEF Represents the latest health status of a single dashboard. class DashboardHealthItem(BaseModel): record_id: Optional[str] = None dashboard_id: str @@ -24,11 +22,11 @@ class DashboardHealthItem(BaseModel): summary: Optional[str] = None -# [/DEF:DashboardHealthItem:Class] +# #endregion DashboardHealthItem -# [DEF:HealthSummaryResponse:Class] -# @PURPOSE: Aggregated health summary for all dashboards. +# #region HealthSummaryResponse [TYPE Class] +# @BRIEF Aggregated health summary for all dashboards. class HealthSummaryResponse(BaseModel): items: List[DashboardHealthItem] pass_count: int @@ -37,6 +35,6 @@ class HealthSummaryResponse(BaseModel): unknown_count: int -# [/DEF:HealthSummaryResponse:Class] +# #endregion HealthSummaryResponse -# [/DEF:HealthSchemas:Module] +# #endregion HealthSchemas diff --git a/backend/src/schemas/profile.py b/backend/src/schemas/profile.py index a7ba3283..db4e7436 100644 --- a/backend/src/schemas/profile.py +++ b/backend/src/schemas/profile.py @@ -1,12 +1,10 @@ -# [DEF:ProfileSchemas:Module] +# #region ProfileSchemas [C:3] [TYPE Module] [SEMANTICS profile, schemas, pydantic, preferences, superset, lookup, security, git, ux] +# @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup. +# @LAYER API +# @INVARIANT Schema shapes stay stable for profile UI states and backend preference contracts. +# @RELATION DEPENDS_ON -> [pydantic] # -# @COMPLEXITY: 3 -# @SEMANTICS: profile, schemas, pydantic, preferences, superset, lookup, security, git, ux -# @PURPOSE: Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup. -# @LAYER: API -# @RELATION: DEPENDS_ON -> pydantic # -# @INVARIANT: Schema shapes stay stable for profile UI states and backend preference contracts. # [SECTION: IMPORTS] from datetime import datetime @@ -15,22 +13,20 @@ from pydantic import BaseModel, Field # [/SECTION] -# [DEF:ProfilePermissionState:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Represents one permission badge state for profile read-only security view. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region ProfilePermissionState [C:3] [TYPE Class] +# @BRIEF Represents one permission badge state for profile read-only security view. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class ProfilePermissionState(BaseModel): key: str allowed: bool -# [/DEF:ProfilePermissionState:Class] +# #endregion ProfilePermissionState -# [DEF:ProfileSecuritySummary:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Read-only security and access snapshot for current user. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region ProfileSecuritySummary [C:3] [TYPE Class] +# @BRIEF Read-only security and access snapshot for current user. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class ProfileSecuritySummary(BaseModel): read_only: bool = True auth_source: Optional[str] = None @@ -40,13 +36,12 @@ class ProfileSecuritySummary(BaseModel): permissions: List[ProfilePermissionState] = Field(default_factory=list) -# [/DEF:ProfileSecuritySummary:Class] +# #endregion ProfileSecuritySummary -# [DEF:ProfilePreference:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Represents persisted profile preference for a single authenticated user. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region ProfilePreference [C:3] [TYPE Class] +# @BRIEF Represents persisted profile preference for a single authenticated user. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class ProfilePreference(BaseModel): user_id: str superset_username: Optional[str] = None @@ -74,13 +69,12 @@ class ProfilePreference(BaseModel): from_attributes = True -# [/DEF:ProfilePreference:Class] +# #endregion ProfilePreference -# [DEF:ProfilePreferenceUpdateRequest:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Request payload for updating current user's profile settings. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region ProfilePreferenceUpdateRequest [C:3] [TYPE Class] +# @BRIEF Request payload for updating current user's profile settings. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class ProfilePreferenceUpdateRequest(BaseModel): superset_username: Optional[str] = Field( default=None, @@ -136,13 +130,12 @@ class ProfilePreferenceUpdateRequest(BaseModel): ) -# [/DEF:ProfilePreferenceUpdateRequest:Class] +# #endregion ProfilePreferenceUpdateRequest -# [DEF:ProfilePreferenceResponse:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Response envelope for profile preference read/update endpoints. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region ProfilePreferenceResponse [C:3] [TYPE Class] +# @BRIEF Response envelope for profile preference read/update endpoints. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class ProfilePreferenceResponse(BaseModel): status: Literal["success", "error"] = "success" message: Optional[str] = None @@ -151,13 +144,12 @@ class ProfilePreferenceResponse(BaseModel): security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary) -# [/DEF:ProfilePreferenceResponse:Class] +# #endregion ProfilePreferenceResponse -# [DEF:SupersetAccountLookupRequest:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Query contract for Superset account lookup by selected environment. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region SupersetAccountLookupRequest [C:3] [TYPE Class] +# @BRIEF Query contract for Superset account lookup by selected environment. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class SupersetAccountLookupRequest(BaseModel): environment_id: str search: Optional[str] = None @@ -167,13 +159,12 @@ class SupersetAccountLookupRequest(BaseModel): sort_order: str = Field(default="desc") -# [/DEF:SupersetAccountLookupRequest:Class] +# #endregion SupersetAccountLookupRequest -# [DEF:SupersetAccountCandidate:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Canonical account candidate projected from Superset users payload. -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region SupersetAccountCandidate [C:3] [TYPE Class] +# @BRIEF Canonical account candidate projected from Superset users payload. +# @RELATION DEPENDS_ON -> [ProfileSchemas] class SupersetAccountCandidate(BaseModel): environment_id: str username: str @@ -182,13 +173,12 @@ class SupersetAccountCandidate(BaseModel): is_active: Optional[bool] = None -# [/DEF:SupersetAccountCandidate:Class] +# #endregion SupersetAccountCandidate -# [DEF:SupersetAccountLookupResponse:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Response envelope for Superset account lookup (success or degraded mode). -# @RELATION: DEPENDS_ON -> ProfileSchemas +# #region SupersetAccountLookupResponse [C:3] [TYPE Class] +# @BRIEF Response envelope for Superset account lookup (success or degraded mode). +# @RELATION DEPENDS_ON -> [ProfileSchemas] class SupersetAccountLookupResponse(BaseModel): status: Literal["success", "degraded"] environment_id: str @@ -199,6 +189,6 @@ class SupersetAccountLookupResponse(BaseModel): items: List[SupersetAccountCandidate] = Field(default_factory=list) -# [/DEF:SupersetAccountLookupResponse:Class] +# #endregion SupersetAccountLookupResponse -# [/DEF:ProfileSchemas:Module] +# #endregion ProfileSchemas diff --git a/backend/src/schemas/settings.py b/backend/src/schemas/settings.py index 0d256f16..02001cd5 100644 --- a/backend/src/schemas/settings.py +++ b/backend/src/schemas/settings.py @@ -1,17 +1,15 @@ -# [DEF:SettingsSchemas:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: settings, schemas, pydantic, validation -# @PURPOSE: Pydantic schemas for application settings and automation policies. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> pydantic +# #region SettingsSchemas [C:3] [TYPE Module] [SEMANTICS settings, schemas, pydantic, validation] +# @BRIEF Pydantic schemas for application settings and automation policies. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [pydantic] from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime, time -# [DEF:NotificationChannel:Class] -# @PURPOSE: Structured notification channel definition for policy-level custom routing. +# #region NotificationChannel [TYPE Class] +# @BRIEF Structured notification channel definition for policy-level custom routing. class NotificationChannel(BaseModel): type: str = Field( ..., description="Notification channel type (e.g., SLACK, SMTP, TELEGRAM)" @@ -21,11 +19,11 @@ class NotificationChannel(BaseModel): ) -# [/DEF:NotificationChannel:Class] +# #endregion NotificationChannel -# [DEF:ValidationPolicyBase:Class] -# @PURPOSE: Base schema for validation policy data. +# #region ValidationPolicyBase [TYPE Class] +# @BRIEF Base schema for validation policy data. class ValidationPolicyBase(BaseModel): name: str = Field(..., description="Name of the policy") environment_id: str = Field(..., description="Target Superset environment ID") @@ -51,20 +49,20 @@ class ValidationPolicyBase(BaseModel): ) -# [/DEF:ValidationPolicyBase:Class] +# #endregion ValidationPolicyBase -# [DEF:ValidationPolicyCreate:Class] -# @PURPOSE: Schema for creating a new validation policy. +# #region ValidationPolicyCreate [TYPE Class] +# @BRIEF Schema for creating a new validation policy. class ValidationPolicyCreate(ValidationPolicyBase): pass -# [/DEF:ValidationPolicyCreate:Class] +# #endregion ValidationPolicyCreate -# [DEF:ValidationPolicyUpdate:Class] -# @PURPOSE: Schema for updating an existing validation policy. +# #region ValidationPolicyUpdate [TYPE Class] +# @BRIEF Schema for updating an existing validation policy. class ValidationPolicyUpdate(BaseModel): name: Optional[str] = None environment_id: Optional[str] = None @@ -78,11 +76,11 @@ class ValidationPolicyUpdate(BaseModel): alert_condition: Optional[str] = None -# [/DEF:ValidationPolicyUpdate:Class] +# #endregion ValidationPolicyUpdate -# [DEF:ValidationPolicyResponse:Class] -# @PURPOSE: Schema for validation policy response data. +# #region ValidationPolicyResponse [TYPE Class] +# @BRIEF Schema for validation policy response data. class ValidationPolicyResponse(ValidationPolicyBase): id: str created_at: datetime @@ -92,6 +90,6 @@ class ValidationPolicyResponse(ValidationPolicyBase): from_attributes = True -# [/DEF:ValidationPolicyResponse:Class] +# #endregion ValidationPolicyResponse -# [/DEF:SettingsSchemas:Module] +# #endregion SettingsSchemas diff --git a/backend/src/scripts/__init__.py b/backend/src/scripts/__init__.py index c26d217a..ee7232a3 100644 --- a/backend/src/scripts/__init__.py +++ b/backend/src/scripts/__init__.py @@ -1,3 +1,3 @@ -# [DEF:ScriptsPackage:Package] -# @PURPOSE: Script entrypoint package root. -# [/DEF:ScriptsPackage:Package] +# #region ScriptsPackage [TYPE Package] +# @BRIEF Script entrypoint package root. +# #endregion ScriptsPackage diff --git a/backend/src/scripts/clean_release_cli.py b/backend/src/scripts/clean_release_cli.py index f3933444..e17c107c 100644 --- a/backend/src/scripts/clean_release_cli.py +++ b/backend/src/scripts/clean_release_cli.py @@ -1,9 +1,7 @@ -# [DEF:CleanReleaseCliScript:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: cli, clean-release, candidate, artifacts, manifest -# @PURPOSE: Provide headless CLI commands for candidate registration, artifact import and manifest build. -# @LAYER: Scripts -# @RELATION: CALLS -> ComplianceOrchestrator +# #region CleanReleaseCliScript [C:3] [TYPE Module] [SEMANTICS cli, clean-release, candidate, artifacts, manifest] +# @BRIEF Provide headless CLI commands for candidate registration, artifact import and manifest build. +# @LAYER Scripts +# @RELATION CALLS -> [ComplianceOrchestrator] from __future__ import annotations @@ -27,8 +25,8 @@ from ..services.clean_release.publication_service import ( ) -# [DEF:build_parser:Function] -# @PURPOSE: Build argparse parser for clean release CLI. +# #region build_parser [TYPE Function] +# @BRIEF Build argparse parser for clean release CLI. def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="clean-release-cli") subparsers = parser.add_subparsers(dest="command", required=True) @@ -99,13 +97,13 @@ def build_parser() -> argparse.ArgumentParser: return parser -# [/DEF:build_parser:Function] +# #endregion build_parser -# [DEF:run_candidate_register:Function] -# @PURPOSE: Register candidate in repository via CLI command. -# @PRE: Candidate ID must be unique. -# @POST: Candidate is persisted in DRAFT status. +# #region run_candidate_register [TYPE Function] +# @BRIEF Register candidate in repository via CLI command. +# @PRE Candidate ID must be unique. +# @POST Candidate is persisted in DRAFT status. def run_candidate_register(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -128,13 +126,13 @@ def run_candidate_register(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_candidate_register:Function] +# #endregion run_candidate_register -# [DEF:run_artifact_import:Function] -# @PURPOSE: Import single artifact for existing candidate. -# @PRE: Candidate must exist. -# @POST: Artifact is persisted for candidate. +# #region run_artifact_import [TYPE Function] +# @BRIEF Import single artifact for existing candidate. +# @PRE Candidate must exist. +# @POST Artifact is persisted for candidate. def run_artifact_import(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -161,13 +159,13 @@ def run_artifact_import(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_artifact_import:Function] +# #endregion run_artifact_import -# [DEF:run_manifest_build:Function] -# @PURPOSE: Build immutable manifest snapshot for candidate. -# @PRE: Candidate must exist. -# @POST: New manifest version is persisted. +# #region run_manifest_build [TYPE Function] +# @BRIEF Build immutable manifest snapshot for candidate. +# @PRE Candidate must exist. +# @POST New manifest version is persisted. def run_manifest_build(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository from ..services.clean_release.manifest_service import build_manifest_snapshot @@ -195,13 +193,13 @@ def run_manifest_build(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_manifest_build:Function] +# #endregion run_manifest_build -# [DEF:run_compliance_run:Function] -# @PURPOSE: Execute compliance run for candidate with optional manifest fallback. -# @PRE: Candidate exists and trusted snapshots are configured. -# @POST: Returns run payload and exit code 0 on success. +# #region run_compliance_run [TYPE Function] +# @BRIEF Execute compliance run for candidate with optional manifest fallback. +# @PRE Candidate exists and trusted snapshots are configured. +# @POST Returns run payload and exit code 0 on success. def run_compliance_run(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository, get_config_manager @@ -234,13 +232,13 @@ def run_compliance_run(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_compliance_run:Function] +# #endregion run_compliance_run -# [DEF:run_compliance_status:Function] -# @PURPOSE: Read run status by run id. -# @PRE: Run exists. -# @POST: Returns run status payload. +# #region run_compliance_status [TYPE Function] +# @BRIEF Read run status by run id. +# @PRE Run exists. +# @POST Returns run status payload. def run_compliance_status(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -266,13 +264,13 @@ def run_compliance_status(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_compliance_status:Function] +# #endregion run_compliance_status -# [DEF:_to_payload:Function] -# @PURPOSE: Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants. -# @PRE: value is serializable model or primitive object. -# @POST: Returns dictionary payload without mutating value. +# #region _to_payload [TYPE Function] +# @BRIEF Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants. +# @PRE value is serializable model or primitive object. +# @POST Returns dictionary payload without mutating value. def _to_payload(value: Any) -> Dict[str, Any]: def _normalize(raw: Any) -> Any: if isinstance(raw, datetime): @@ -296,13 +294,13 @@ def _to_payload(value: Any) -> Dict[str, Any]: raise TypeError(f"unsupported payload type: {type(value)!r}") -# [/DEF:_to_payload:Function] +# #endregion _to_payload -# [DEF:run_compliance_report:Function] -# @PURPOSE: Read immutable report by run id. -# @PRE: Run and report exist. -# @POST: Returns report payload. +# #region run_compliance_report [TYPE Function] +# @BRIEF Read immutable report by run id. +# @PRE Run and report exist. +# @POST Returns report payload. def run_compliance_report(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -323,13 +321,13 @@ def run_compliance_report(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_compliance_report:Function] +# #endregion run_compliance_report -# [DEF:run_compliance_violations:Function] -# @PURPOSE: Read run violations by run id. -# @PRE: Run exists. -# @POST: Returns violations payload. +# #region run_compliance_violations [TYPE Function] +# @BRIEF Read run violations by run id. +# @PRE Run exists. +# @POST Returns violations payload. def run_compliance_violations(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -348,13 +346,13 @@ def run_compliance_violations(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_compliance_violations:Function] +# #endregion run_compliance_violations -# [DEF:run_approve:Function] -# @PURPOSE: Approve candidate based on immutable PASSED report. -# @PRE: Candidate and report exist; report is PASSED. -# @POST: Persists APPROVED decision and returns success payload. +# #region run_approve [TYPE Function] +# @BRIEF Approve candidate based on immutable PASSED report. +# @PRE Candidate and report exist; report is PASSED. +# @POST Persists APPROVED decision and returns success payload. def run_approve(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -379,13 +377,13 @@ def run_approve(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_approve:Function] +# #endregion run_approve -# [DEF:run_reject:Function] -# @PURPOSE: Reject candidate without mutating compliance evidence. -# @PRE: Candidate and report exist. -# @POST: Persists REJECTED decision and returns success payload. +# #region run_reject [TYPE Function] +# @BRIEF Reject candidate without mutating compliance evidence. +# @PRE Candidate and report exist. +# @POST Persists REJECTED decision and returns success payload. def run_reject(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -410,13 +408,13 @@ def run_reject(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_reject:Function] +# #endregion run_reject -# [DEF:run_publish:Function] -# @PURPOSE: Publish approved candidate to target channel. -# @PRE: Candidate is approved and report belongs to candidate. -# @POST: Appends ACTIVE publication record and returns payload. +# #region run_publish [TYPE Function] +# @BRIEF Publish approved candidate to target channel. +# @PRE Candidate is approved and report belongs to candidate. +# @POST Appends ACTIVE publication record and returns payload. def run_publish(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -438,13 +436,13 @@ def run_publish(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_publish:Function] +# #endregion run_publish -# [DEF:run_revoke:Function] -# @PURPOSE: Revoke active publication record. -# @PRE: Publication id exists and is ACTIVE. -# @POST: Publication record status becomes REVOKED. +# #region run_revoke [TYPE Function] +# @BRIEF Revoke active publication record. +# @PRE Publication id exists and is ACTIVE. +# @POST Publication record status becomes REVOKED. def run_revoke(args: argparse.Namespace) -> int: from ..dependencies import get_clean_release_repository @@ -464,11 +462,11 @@ def run_revoke(args: argparse.Namespace) -> int: return 0 -# [/DEF:run_revoke:Function] +# #endregion run_revoke -# [DEF:main:Function] -# @PURPOSE: CLI entrypoint for clean release commands. +# #region main [TYPE Function] +# @BRIEF CLI entrypoint for clean release commands. def main(argv: Optional[List[str]] = None) -> int: parser = build_parser() args = parser.parse_args(argv) @@ -500,10 +498,10 @@ def main(argv: Optional[List[str]] = None) -> int: return 2 -# [/DEF:main:Function] +# #endregion main if __name__ == "__main__": raise SystemExit(main()) -# [/DEF:CleanReleaseCliScript:Module] +# #endregion CleanReleaseCliScript diff --git a/backend/src/scripts/clean_release_tui.py b/backend/src/scripts/clean_release_tui.py index 1a09d15b..21399984 100644 --- a/backend/src/scripts/clean_release_tui.py +++ b/backend/src/scripts/clean_release_tui.py @@ -1,11 +1,9 @@ -# [DEF:CleanReleaseTuiScript:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, tui, ncurses, interactive-validator -# @PURPOSE: Interactive terminal interface for Enterprise Clean Release compliance validation. -# @LAYER: UI -# @RELATION: DEPENDS_ON -> [ComplianceExecutionService] -# @RELATION: DEPENDS_ON -> [CleanReleaseRepository] -# @INVARIANT: TUI refuses startup in non-TTY environments; headless flow is CLI/API only. +# #region CleanReleaseTuiScript [C:3] [TYPE Module] [SEMANTICS clean-release, tui, ncurses, interactive-validator] +# @BRIEF Interactive terminal interface for Enterprise Clean Release compliance validation. +# @LAYER UI +# @INVARIANT TUI refuses startup in non-TTY environments; headless flow is CLI/API only. +# @RELATION DEPENDS_ON -> [ComplianceExecutionService] +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] import curses import json @@ -46,10 +44,10 @@ from src.services.clean_release.publication_service import publish_candidate from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:TuiFacadeAdapter:Class] -# @PURPOSE: Thin TUI adapter that routes business mutations through application services. -# @PRE: repository contains candidate and trusted policy/registry snapshots for execution. -# @POST: Business actions return service results/errors without direct TUI-owned mutations. +# #region TuiFacadeAdapter [TYPE Class] +# @BRIEF Thin TUI adapter that routes business mutations through application services. +# @PRE repository contains candidate and trusted policy/registry snapshots for execution. +# @POST Business actions return service results/errors without direct TUI-owned mutations. class TuiFacadeAdapter: def __init__(self, repository: CleanReleaseRepository): self.repository = repository @@ -189,11 +187,11 @@ class TuiFacadeAdapter: } -# [/DEF:TuiFacadeAdapter:Class] +# #endregion TuiFacadeAdapter -# [DEF:CleanReleaseTUI:Class] -# @PURPOSE: Curses-based application for compliance monitoring. +# #region CleanReleaseTUI [TYPE Class] +# @BRIEF Curses-based application for compliance monitoring. # @UX_STATE: READY -> Waiting for operator to start checks (F5). # @UX_STATE: RUNNING -> Executing compliance stages with progress feedback. # @UX_STATE: COMPLIANT -> Release candidate passed all checks. @@ -508,10 +506,10 @@ class CleanReleaseTUI: self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x]) self.stdscr.attroff(curses.color_pair(1)) - # [DEF:run_checks:Function] - # @PURPOSE: Execute compliance run via facade adapter and update UI state. - # @PRE: Candidate and policy snapshots are present in repository. - # @POST: UI reflects final run/report/violation state from service result. + # #region run_checks [TYPE Function] + # @BRIEF Execute compliance run via facade adapter and update UI state. + # @PRE Candidate and policy snapshots are present in repository. + # @POST UI reflects final run/report/violation state from service result. def run_checks(self): self.status = "RUNNING" self.report_id = None @@ -552,7 +550,7 @@ class CleanReleaseTUI: self.refresh_overview() self.refresh_screen() - # [/DEF:run_checks:Function] + # #endregion run_checks def build_manifest(self): try: @@ -654,7 +652,7 @@ class CleanReleaseTUI: self.publish_latest() -# [/DEF:CleanReleaseTUI:Class] +# #endregion CleanReleaseTUI def tui_main(stdscr: curses.window): @@ -681,4 +679,4 @@ def main() -> int: if __name__ == "__main__": sys.exit(main()) -# [/DEF:CleanReleaseTuiScript:Module] +# #endregion CleanReleaseTuiScript diff --git a/backend/src/scripts/create_admin.py b/backend/src/scripts/create_admin.py index 9e9c2428..33b3442c 100644 --- a/backend/src/scripts/create_admin.py +++ b/backend/src/scripts/create_admin.py @@ -1,14 +1,12 @@ -# [DEF:CreateAdminScript:Module] +# #region CreateAdminScript [C:3] [TYPE Module] [SEMANTICS admin, setup, user, auth, cli] +# @BRIEF CLI tool for creating the initial admin user. +# @LAYER Scripts +# @INVARIANT Admin user must have the "Admin" role. +# @RELATION USES -> [AuthSecurityModule] +# @RELATION USES -> [DatabaseModule] +# @RELATION USES -> [AuthModels] # -# @COMPLEXITY: 3 -# @SEMANTICS: admin, setup, user, auth, cli -# @PURPOSE: CLI tool for creating the initial admin user. -# @LAYER: Scripts -# @RELATION: USES -> [AuthSecurityModule] -# @RELATION: USES -> [DatabaseModule] -# @RELATION: USES -> [AuthModels] # -# @INVARIANT: Admin user must have the "Admin" role. # [SECTION: IMPORTS] import sys @@ -25,10 +23,10 @@ from src.core.logger import logger, belief_scope # [/SECTION] -# [DEF:create_admin:Function] -# @PURPOSE: Creates an admin user and necessary roles/permissions. -# @PRE: username and password provided via CLI. -# @POST: Admin user exists in auth.db. +# #region create_admin [TYPE Function] +# @BRIEF Creates an admin user and necessary roles/permissions. +# @PRE username and password provided via CLI. +# @POST Admin user exists in auth.db. # # @PARAM: username (str) - Admin username. # @PARAM: password (str) - Admin password. @@ -79,7 +77,7 @@ def create_admin(username, password, email=None): db.close() -# [/DEF:create_admin:Function] +# #endregion create_admin if __name__ == "__main__": parser = argparse.ArgumentParser(description="Create initial admin user") @@ -96,4 +94,4 @@ if __name__ == "__main__": except Exception: sys.exit(1) -# [/DEF:CreateAdminScript:Module] +# #endregion CreateAdminScript diff --git a/backend/src/scripts/init_auth_db.py b/backend/src/scripts/init_auth_db.py index 83627c5a..aa950bfb 100644 --- a/backend/src/scripts/init_auth_db.py +++ b/backend/src/scripts/init_auth_db.py @@ -1,14 +1,12 @@ -# [DEF:InitAuthDbScript:Module] +# #region InitAuthDbScript [C:2] [TYPE Module] [SEMANTICS setup, database, auth, migration] +# @BRIEF Initializes the auth database and creates the necessary tables. +# @LAYER Scripts +# @INVARIANT Safe to run multiple times (idempotent). +# @RELATION CALLS -> [init_db] +# @RELATION CALLS -> [ensure_encryption_key] +# @RELATION CALLS -> [seed_permissions] # -# @SEMANTICS: setup, database, auth, migration -# @PURPOSE: Initializes the auth database and creates the necessary tables. -# @COMPLEXITY: 2 -# @LAYER: Scripts -# @RELATION: CALLS -> init_db -# @RELATION: CALLS -> ensure_encryption_key -# @RELATION: CALLS -> seed_permissions # -# @INVARIANT: Safe to run multiple times (idempotent). # [SECTION: IMPORTS] import sys @@ -24,13 +22,12 @@ from src.scripts.seed_permissions import seed_permissions # [/SECTION] -# [DEF:run_init:Function] -# @PURPOSE: Main entry point for the initialization script. -# @COMPLEXITY: 3 -# @POST: auth.db is initialized with the correct schema and seeded permissions. -# @RELATION: CALLS -> ensure_encryption_key -# @RELATION: CALLS -> init_db -# @RELATION: CALLS -> seed_permissions +# #region run_init [C:3] [TYPE Function] +# @BRIEF Main entry point for the initialization script. +# @POST auth.db is initialized with the correct schema and seeded permissions. +# @RELATION CALLS -> [ensure_encryption_key] +# @RELATION CALLS -> [init_db] +# @RELATION CALLS -> [seed_permissions] def run_init(): with belief_scope("init_auth_db"): logger.info("Initializing authentication database...") @@ -47,9 +44,9 @@ def run_init(): sys.exit(1) -# [/DEF:run_init:Function] +# #endregion run_init if __name__ == "__main__": run_init() -# [/DEF:InitAuthDbScript:Module] +# #endregion InitAuthDbScript diff --git a/backend/src/scripts/migrate_sqlite_to_postgres.py b/backend/src/scripts/migrate_sqlite_to_postgres.py index c9b190f5..034be60a 100644 --- a/backend/src/scripts/migrate_sqlite_to_postgres.py +++ b/backend/src/scripts/migrate_sqlite_to_postgres.py @@ -1,16 +1,14 @@ -# [DEF:MigrateSqliteToPostgresScript:Module] +# #region MigrateSqliteToPostgresScript [C:3] [TYPE Module] [SEMANTICS migration, sqlite, postgresql, config, task_logs, task_records] +# @BRIEF Migrates legacy config and task history from SQLite/file storage to PostgreSQL. +# @LAYER Scripts +# @INVARIANT Script is idempotent for task_records and app_configurations. +# @RELATION READS_FROM -> [backend/tasks.db] +# @RELATION READS_FROM -> [backend/config.json] +# @RELATION WRITES_TO -> [postgresql.task_records] +# @RELATION WRITES_TO -> [postgresql.task_logs] +# @RELATION WRITES_TO -> [postgresql.app_configurations] # -# @COMPLEXITY: 3 -# @SEMANTICS: migration, sqlite, postgresql, config, task_logs, task_records -# @PURPOSE: Migrates legacy config and task history from SQLite/file storage to PostgreSQL. -# @LAYER: Scripts -# @RELATION: READS_FROM -> backend/tasks.db -# @RELATION: READS_FROM -> backend/config.json -# @RELATION: WRITES_TO -> postgresql.task_records -# @RELATION: WRITES_TO -> postgresql.task_logs -# @RELATION: WRITES_TO -> postgresql.app_configurations # -# @INVARIANT: Script is idempotent for task_records and app_configurations. # [SECTION: IMPORTS] import argparse @@ -30,7 +28,7 @@ log = MarkerLogger("MigrateSqliteToPostgres") # [/SECTION] -# [DEF:Constants:Section] +# #region Constants [TYPE Section] DEFAULT_TARGET_URL = os.getenv( "DATABASE_URL", os.getenv( @@ -38,14 +36,13 @@ DEFAULT_TARGET_URL = os.getenv( "postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools", ), ) -# [/DEF:Constants:Section] +# #endregion Constants -# [DEF:_json_load_if_needed:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Parses JSON-like values from SQLite TEXT/JSON columns to Python objects. -# @PRE: value is scalar JSON/text/list/dict or None. -# @POST: Returns normalized Python object or original scalar value. +# #region _json_load_if_needed [C:3] [TYPE Function] +# @BRIEF Parses JSON-like values from SQLite TEXT/JSON columns to Python objects. +# @PRE value is scalar JSON/text/list/dict or None. +# @POST Returns normalized Python object or original scalar value. def _json_load_if_needed(value: Any) -> Any: with belief_scope("_json_load_if_needed"): if value is None: @@ -64,11 +61,11 @@ def _json_load_if_needed(value: Any) -> Any: return value -# [/DEF:_json_load_if_needed:Function] +# #endregion _json_load_if_needed -# [DEF:_find_legacy_config_path:Function] -# @PURPOSE: Resolves the existing legacy config.json path from candidates. +# #region _find_legacy_config_path [TYPE Function] +# @BRIEF Resolves the existing legacy config.json path from candidates. def _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]: with belief_scope("_find_legacy_config_path"): if explicit_path: @@ -85,11 +82,11 @@ def _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]: return None -# [/DEF:_find_legacy_config_path:Function] +# #endregion _find_legacy_config_path -# [DEF:_connect_sqlite:Function] -# @PURPOSE: Opens a SQLite connection with row factory. +# #region _connect_sqlite [TYPE Function] +# @BRIEF Opens a SQLite connection with row factory. def _connect_sqlite(path: Path) -> sqlite3.Connection: with belief_scope("_connect_sqlite"): conn = sqlite3.connect(str(path)) @@ -97,11 +94,11 @@ def _connect_sqlite(path: Path) -> sqlite3.Connection: return conn -# [/DEF:_connect_sqlite:Function] +# #endregion _connect_sqlite -# [DEF:_ensure_target_schema:Function] -# @PURPOSE: Ensures required PostgreSQL tables exist before migration. +# #region _ensure_target_schema [TYPE Function] +# @BRIEF Ensures required PostgreSQL tables exist before migration. def _ensure_target_schema(engine) -> None: with belief_scope("_ensure_target_schema"): stmts: Iterable[str] = ( @@ -164,11 +161,11 @@ def _ensure_target_schema(engine) -> None: conn.execute(text(stmt)) -# [/DEF:_ensure_target_schema:Function] +# #endregion _ensure_target_schema -# [DEF:_migrate_config:Function] -# @PURPOSE: Migrates legacy config.json into app_configurations(global). +# #region _migrate_config [TYPE Function] +# @BRIEF Migrates legacy config.json into app_configurations(global). def _migrate_config(engine, legacy_config_path: Optional[Path]) -> int: with belief_scope("_migrate_config"): if legacy_config_path is None: @@ -195,11 +192,11 @@ def _migrate_config(engine, legacy_config_path: Optional[Path]) -> int: return 1 -# [/DEF:_migrate_config:Function] +# #endregion _migrate_config -# [DEF:_migrate_tasks_and_logs:Function] -# @PURPOSE: Migrates task_records and task_logs from SQLite into PostgreSQL. +# #region _migrate_tasks_and_logs [TYPE Function] +# @BRIEF Migrates task_records and task_logs from SQLite into PostgreSQL. def _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str, int]: with belief_scope("_migrate_tasks_and_logs"): stats = { @@ -324,11 +321,11 @@ def _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str return stats -# [/DEF:_migrate_tasks_and_logs:Function] +# #endregion _migrate_tasks_and_logs -# [DEF:run_migration:Function] -# @PURPOSE: Orchestrates migration from SQLite/file to PostgreSQL. +# #region run_migration [TYPE Function] +# @BRIEF Orchestrates migration from SQLite/file to PostgreSQL. def run_migration( sqlite_path: Path, target_url: str, legacy_config_path: Optional[Path] ) -> Dict[str, int]: @@ -349,11 +346,11 @@ def run_migration( sqlite_conn.close() -# [/DEF:run_migration:Function] +# #endregion run_migration -# [DEF:main:Function] -# @PURPOSE: CLI entrypoint. +# #region main [TYPE Function] +# @BRIEF CLI entrypoint. def main() -> int: with belief_scope("main"): parser = argparse.ArgumentParser( @@ -396,6 +393,6 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) -# [/DEF:main:Function] +# #endregion main -# [/DEF:MigrateSqliteToPostgresScript:Module] +# #endregion MigrateSqliteToPostgresScript diff --git a/backend/src/scripts/seed_permissions.py b/backend/src/scripts/seed_permissions.py index 0ba255dd..91eb11d7 100644 --- a/backend/src/scripts/seed_permissions.py +++ b/backend/src/scripts/seed_permissions.py @@ -1,15 +1,13 @@ -# [DEF:SeedPermissionsScript:Module] +# #region SeedPermissionsScript [C:3] [TYPE Module] [SEMANTICS setup, database, auth, permissions, seeding] +# @BRIEF Populates the auth database with initial system permissions. +# @LAYER Scripts +# @INVARIANT Safe to run multiple times (idempotent). +# @RELATION DEPENDS_ON -> [AuthSessionLocal] +# @RELATION DEPENDS_ON -> [Permission] +# @RELATION DEPENDS_ON -> [Role] +# @RELATION DEPENDS_ON -> [AuthRepository] # -# @SEMANTICS: setup, database, auth, permissions, seeding -# @PURPOSE: Populates the auth database with initial system permissions. -# @COMPLEXITY: 3 -# @LAYER: Scripts -# @RELATION: DEPENDS_ON -> AuthSessionLocal -# @RELATION: DEPENDS_ON -> Permission -# @RELATION: DEPENDS_ON -> Role -# @RELATION: DEPENDS_ON -> AuthRepository # -# @INVARIANT: Safe to run multiple times (idempotent). # [SECTION: IMPORTS] import sys @@ -24,10 +22,9 @@ from src.core.auth.repository import AuthRepository from src.core.logger import logger, belief_scope # [/SECTION] -# [DEF:INITIAL_PERMISSIONS:Constant] -# @PURPOSE: Canonical bootstrap permission tuples seeded into auth storage. -# @COMPLEXITY: 3 -# @RELATION: DEPENDS_ON -> SeedPermissionsScript +# #region INITIAL_PERMISSIONS [C:3] [TYPE Constant] +# @BRIEF Canonical bootstrap permission tuples seeded into auth storage. +# @RELATION DEPENDS_ON -> [SeedPermissionsScript] INITIAL_PERMISSIONS = [ # Admin Permissions {"resource": "admin:users", "action": "READ"}, @@ -59,18 +56,17 @@ INITIAL_PERMISSIONS = [ {"resource": "dataset:execution", "action": "LAUNCH"}, {"resource": "dataset:execution", "action": "LAUNCH_PROD"}, ] -# [/DEF:INITIAL_PERMISSIONS:Constant] +# #endregion INITIAL_PERMISSIONS -# [DEF:seed_permissions:Function] -# @PURPOSE: Inserts missing permissions into the database. -# @COMPLEXITY: 3 -# @POST: All INITIAL_PERMISSIONS exist in the DB. -# @RELATION: DEPENDS_ON -> AuthSessionLocal -# @RELATION: DEPENDS_ON -> Permission -# @RELATION: DEPENDS_ON -> Role -# @RELATION: DEPENDS_ON -> AuthRepository -# @RELATION: DEPENDS_ON -> INITIAL_PERMISSIONS +# #region seed_permissions [C:3] [TYPE Function] +# @BRIEF Inserts missing permissions into the database. +# @POST All INITIAL_PERMISSIONS exist in the DB. +# @RELATION DEPENDS_ON -> [AuthSessionLocal] +# @RELATION DEPENDS_ON -> [Permission] +# @RELATION DEPENDS_ON -> [Role] +# @RELATION DEPENDS_ON -> [AuthRepository] +# @RELATION DEPENDS_ON -> [INITIAL_PERMISSIONS] def seed_permissions(): with belief_scope("seed_permissions"): db = AuthSessionLocal() @@ -140,9 +136,9 @@ def seed_permissions(): db.close() -# [/DEF:seed_permissions:Function] +# #endregion seed_permissions if __name__ == "__main__": seed_permissions() -# [/DEF:SeedPermissionsScript:Module] +# #endregion SeedPermissionsScript diff --git a/backend/src/scripts/seed_superset_load_test.py b/backend/src/scripts/seed_superset_load_test.py index 16fa3dc7..2a976b85 100644 --- a/backend/src/scripts/seed_superset_load_test.py +++ b/backend/src/scripts/seed_superset_load_test.py @@ -1,12 +1,10 @@ -# [DEF:SeedSupersetLoadTestScript:Module] +# #region SeedSupersetLoadTestScript [C:3] [TYPE Module] [SEMANTICS superset, load-test, charts, dashboards, seed, stress] +# @BRIEF Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments. +# @LAYER Scripts +# @INVARIANT Created chart and dashboard names are globally unique for one script run. +# @RELATION USES -> [ConfigManager] +# @RELATION USES -> [SupersetClient] # -# @COMPLEXITY: 3 -# @SEMANTICS: superset, load-test, charts, dashboards, seed, stress -# @PURPOSE: Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments. -# @LAYER: Scripts -# @RELATION: USES -> [ConfigManager] -# @RELATION: USES -> [SupersetClient] -# @INVARIANT: Created chart and dashboard names are globally unique for one script run. # [SECTION: IMPORTS] import argparse @@ -29,10 +27,10 @@ log = MarkerLogger("SeedSupersetLoadTest") # [/SECTION] -# [DEF:_parse_args:Function] -# @PURPOSE: Parses CLI arguments for load-test data generation. -# @PRE: Script is called from CLI. -# @POST: Returns validated argument namespace. +# #region _parse_args [TYPE Function] +# @BRIEF Parses CLI arguments for load-test data generation. +# @PRE Script is called from CLI. +# @POST Returns validated argument namespace. def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Seed Superset with load-test charts and dashboards" @@ -70,13 +68,13 @@ def _parse_args() -> argparse.Namespace: return parser.parse_args() -# [/DEF:_parse_args:Function] +# #endregion _parse_args -# [DEF:_extract_result_payload:Function] -# @PURPOSE: Normalizes Superset API payloads that may be wrapped in `result`. -# @PRE: payload is a JSON-decoded API response. -# @POST: Returns the unwrapped object when present. +# #region _extract_result_payload [TYPE Function] +# @BRIEF Normalizes Superset API payloads that may be wrapped in `result`. +# @PRE payload is a JSON-decoded API response. +# @POST Returns the unwrapped object when present. def _extract_result_payload(payload: Dict) -> Dict: result = payload.get("result") if isinstance(result, dict): @@ -84,13 +82,13 @@ def _extract_result_payload(payload: Dict) -> Dict: return payload -# [/DEF:_extract_result_payload:Function] +# #endregion _extract_result_payload -# [DEF:_extract_created_id:Function] -# @PURPOSE: Extracts object ID from create/update API response. -# @PRE: payload is a JSON-decoded API response. -# @POST: Returns integer object ID or None if missing. +# #region _extract_created_id [TYPE Function] +# @BRIEF Extracts object ID from create/update API response. +# @PRE payload is a JSON-decoded API response. +# @POST Returns integer object ID or None if missing. def _extract_created_id(payload: Dict) -> Optional[int]: direct_id = payload.get("id") if isinstance(direct_id, int): @@ -101,13 +99,13 @@ def _extract_created_id(payload: Dict) -> Optional[int]: return None -# [/DEF:_extract_created_id:Function] +# #endregion _extract_created_id -# [DEF:_generate_unique_name:Function] -# @PURPOSE: Generates globally unique random names for charts/dashboards. -# @PRE: used_names is mutable set for collision tracking. -# @POST: Returns a unique string and stores it in used_names. +# #region _generate_unique_name [TYPE Function] +# @BRIEF Generates globally unique random names for charts/dashboards. +# @PRE used_names is mutable set for collision tracking. +# @POST Returns a unique string and stores it in used_names. def _generate_unique_name(prefix: str, used_names: set[str], rng: random.Random) -> str: adjectives = [ "amber", @@ -141,13 +139,13 @@ def _generate_unique_name(prefix: str, used_names: set[str], rng: random.Random) return candidate -# [/DEF:_generate_unique_name:Function] +# #endregion _generate_unique_name -# [DEF:_resolve_target_envs:Function] -# @PURPOSE: Resolves requested environment IDs from configuration. -# @PRE: env_ids is non-empty. -# @POST: Returns mapping env_id -> configured environment object. +# #region _resolve_target_envs [TYPE Function] +# @BRIEF Resolves requested environment IDs from configuration. +# @PRE env_ids is non-empty. +# @POST Returns mapping env_id -> configured environment object. def _resolve_target_envs(env_ids: List[str]) -> Dict[str, Environment]: config_manager = ConfigManager() configured = {env.id: env for env in config_manager.get_environments()} @@ -175,13 +173,13 @@ def _resolve_target_envs(env_ids: List[str]) -> Dict[str, Environment]: return resolved -# [/DEF:_resolve_target_envs:Function] +# #endregion _resolve_target_envs -# [DEF:_build_chart_template_pool:Function] -# @PURPOSE: Builds a pool of source chart templates to clone in one environment. -# @PRE: Client is authenticated. -# @POST: Returns non-empty list of chart payload templates. +# #region _build_chart_template_pool [TYPE Function] +# @BRIEF Builds a pool of source chart templates to clone in one environment. +# @PRE Client is authenticated. +# @POST Returns non-empty list of chart payload templates. def _build_chart_template_pool( client: SupersetClient, pool_size: int, rng: random.Random ) -> List[Dict]: @@ -250,14 +248,14 @@ def _build_chart_template_pool( return templates -# [/DEF:_build_chart_template_pool:Function] +# #endregion _build_chart_template_pool -# [DEF:seed_superset_load_data:Function] -# @PURPOSE: Creates dashboards and cloned charts for load testing across target environments. -# @PRE: Target environments must be reachable and authenticated. -# @POST: Returns execution statistics dictionary. -# @SIDE_EFFECT: Creates objects in Superset environments. +# #region seed_superset_load_data [TYPE Function] +# @BRIEF Creates dashboards and cloned charts for load testing across target environments. +# @PRE Target environments must be reachable and authenticated. +# @POST Returns execution statistics dictionary. +# @SIDE_EFFECT Creates objects in Superset environments. def seed_superset_load_data(args: argparse.Namespace) -> Dict: rng = random.Random(args.seed) env_map = _resolve_target_envs(args.envs) @@ -371,13 +369,13 @@ def seed_superset_load_data(args: argparse.Namespace) -> Dict: } -# [/DEF:seed_superset_load_data:Function] +# #endregion seed_superset_load_data -# [DEF:main:Function] -# @PURPOSE: CLI entrypoint for Superset load-test data seeding. -# @PRE: Command line arguments are valid. -# @POST: Prints summary and exits with non-zero status on failure. +# #region main [TYPE Function] +# @BRIEF CLI entrypoint for Superset load-test data seeding. +# @PRE Command line arguments are valid. +# @POST Prints summary and exits with non-zero status on failure. def main() -> None: with belief_scope("seed_superset_load_test.main"): args = _parse_args() @@ -385,10 +383,10 @@ def main() -> None: log.reflect("Seed load test complete", payload={"summary": result}) -# [/DEF:main:Function] +# #endregion main if __name__ == "__main__": main() -# [/DEF:SeedSupersetLoadTestScript:Module] +# #endregion SeedSupersetLoadTestScript diff --git a/backend/src/scripts/test_dataset_dashboard_relations.py b/backend/src/scripts/test_dataset_dashboard_relations.py index 96844e6b..83b83f4c 100644 --- a/backend/src/scripts/test_dataset_dashboard_relations.py +++ b/backend/src/scripts/test_dataset_dashboard_relations.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -# [DEF:test_dataset_dashboard_relations_script:Module] -# @SEMANTICS: scripts, test, dataset, dashboard, superset, relations -# @PURPOSE: Tests and inspects dataset-to-dashboard relationship responses from Superset API. -# @COMPLEXITY: 2 +# #region test_dataset_dashboard_relations_script [C:2] [TYPE Module] [SEMANTICS scripts, test, dataset, dashboard, superset, relations] +# @BRIEF Tests and inspects dataset-to-dashboard relationship responses from Superset API. """ Script to test dataset-to-dashboard relationships from Superset API. @@ -169,4 +167,4 @@ def test_dashboard_dataset_relations(): if __name__ == "__main__": test_dashboard_dataset_relations() -# [/DEF:test_dataset_dashboard_relations_script:Module] +# #endregion test_dataset_dashboard_relations_script diff --git a/backend/src/services/__init__.py b/backend/src/services/__init__.py index 06d50838..a53db318 100644 --- a/backend/src/services/__init__.py +++ b/backend/src/services/__init__.py @@ -1,6 +1,5 @@ -# [DEF:services:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Package initialization for services module +# #region services [C:2] [TYPE Module] +# @BRIEF Package initialization for services module # @NOTE: Only export services that don't cause circular imports # @NOTE: GitService, AuthService, LLMProviderService have circular import issues - import directly when needed @@ -15,4 +14,4 @@ def __getattr__(name): from .resource_service import ResourceService return ResourceService raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -# [/DEF:services:Module] +# #endregion services diff --git a/backend/src/services/__tests__/test_encryption_manager.py b/backend/src/services/__tests__/test_encryption_manager.py index 8fc7ea41..77d58364 100644 --- a/backend/src/services/__tests__/test_encryption_manager.py +++ b/backend/src/services/__tests__/test_encryption_manager.py @@ -1,10 +1,8 @@ -# [DEF:test_encryption_manager:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: encryption, security, fernet, api-keys, tests -# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality. -# @LAYER: Domain -# @INVARIANT: Encrypt+decrypt roundtrip always returns original plaintext. +# #region test_encryption_manager [C:3] [TYPE Module] [SEMANTICS encryption, security, fernet, api-keys, tests] +# @BRIEF Unit tests for EncryptionManager encrypt/decrypt functionality. +# @LAYER Domain +# @INVARIANT Encrypt+decrypt roundtrip always returns original plaintext. +# @RELATION BELONGS_TO -> [SrcRoot] import sys from pathlib import Path @@ -15,11 +13,11 @@ from unittest.mock import patch from cryptography.fernet import Fernet, InvalidToken -# [DEF:TestEncryptionManager:Class] -# @RELATION: BINDS_TO -> test_encryption_manager -# @PURPOSE: Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling. -# @PRE: cryptography package installed. -# @POST: All encrypt/decrypt invariants verified. +# #region TestEncryptionManager [TYPE Class] +# @BRIEF Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling. +# @PRE cryptography package installed. +# @POST All encrypt/decrypt invariants verified. +# @RELATION BINDS_TO -> [test_encryption_manager] class TestEncryptionManager: """Tests for the EncryptionManager class.""" @@ -41,10 +39,10 @@ class TestEncryptionManager: return EncryptionManager() - # [DEF:test_encrypt_decrypt_roundtrip:Function] - # @PURPOSE: Encrypt then decrypt returns original plaintext. - # @PRE: Valid plaintext string. - # @POST: Decrypted output equals original input. + # #region test_encrypt_decrypt_roundtrip [TYPE Function] + # @BRIEF Encrypt then decrypt returns original plaintext. + # @PRE Valid plaintext string. + # @POST Decrypted output equals original input. def test_encrypt_decrypt_roundtrip(self): mgr = self._make_manager() original = "my-secret-api-key-12345" @@ -52,69 +50,69 @@ class TestEncryptionManager: assert encrypted != original decrypted = mgr.decrypt(encrypted) assert decrypted == original - # [/DEF:test_encrypt_decrypt_roundtrip:Function] + # #endregion test_encrypt_decrypt_roundtrip - # [DEF:test_encrypt_produces_different_output:Function] - # @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV). - # @PRE: Two encrypt calls with same input. - # @POST: Ciphertexts differ but both decrypt to same value. + # #region test_encrypt_produces_different_output [TYPE Function] + # @BRIEF Same plaintext produces different ciphertext (Fernet uses random IV). + # @PRE Two encrypt calls with same input. + # @POST Ciphertexts differ but both decrypt to same value. def test_encrypt_produces_different_output(self): mgr = self._make_manager() ct1 = mgr.encrypt("same-key") ct2 = mgr.encrypt("same-key") assert ct1 != ct2 assert mgr.decrypt(ct1) == mgr.decrypt(ct2) == "same-key" - # [/DEF:test_encrypt_produces_different_output:Function] + # #endregion test_encrypt_produces_different_output - # [DEF:test_different_inputs_yield_different_ciphertext:Function] - # @PURPOSE: Different inputs produce different ciphertexts. - # @PRE: Two different plaintext values. - # @POST: Encrypted outputs differ. + # #region test_different_inputs_yield_different_ciphertext [TYPE Function] + # @BRIEF Different inputs produce different ciphertexts. + # @PRE Two different plaintext values. + # @POST Encrypted outputs differ. def test_different_inputs_yield_different_ciphertext(self): mgr = self._make_manager() ct1 = mgr.encrypt("key-one") ct2 = mgr.encrypt("key-two") assert ct1 != ct2 - # [/DEF:test_different_inputs_yield_different_ciphertext:Function] + # #endregion test_different_inputs_yield_different_ciphertext - # [DEF:test_decrypt_invalid_data_raises:Function] - # @PURPOSE: Decrypting invalid data raises InvalidToken. - # @PRE: Invalid ciphertext string. - # @POST: Exception raised. + # #region test_decrypt_invalid_data_raises [TYPE Function] + # @BRIEF Decrypting invalid data raises InvalidToken. + # @PRE Invalid ciphertext string. + # @POST Exception raised. def test_decrypt_invalid_data_raises(self): mgr = self._make_manager() with pytest.raises(Exception): mgr.decrypt("not-a-valid-fernet-token") - # [/DEF:test_decrypt_invalid_data_raises:Function] + # #endregion test_decrypt_invalid_data_raises - # [DEF:test_encrypt_empty_string:Function] - # @PURPOSE: Encrypting and decrypting an empty string works. - # @PRE: Empty string input. - # @POST: Decrypted output equals empty string. + # #region test_encrypt_empty_string [TYPE Function] + # @BRIEF Encrypting and decrypting an empty string works. + # @PRE Empty string input. + # @POST Decrypted output equals empty string. def test_encrypt_empty_string(self): mgr = self._make_manager() encrypted = mgr.encrypt("") assert encrypted decrypted = mgr.decrypt(encrypted) assert decrypted == "" - # [/DEF:test_encrypt_empty_string:Function] + # #endregion test_encrypt_empty_string - # [DEF:test_missing_key_fails_fast:Function] - # @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret. - # @PRE: ENCRYPTION_KEY is unset. - # @POST: RuntimeError raised during EncryptionManager construction. + # #region test_missing_key_fails_fast [TYPE Function] + # @BRIEF Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret. + # @PRE ENCRYPTION_KEY is unset. + # @POST RuntimeError raised during EncryptionManager construction. def test_missing_key_fails_fast(self): from src.services.llm_provider import EncryptionManager with patch.dict("os.environ", {}, clear=True): with pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"): EncryptionManager() - # [/DEF:test_missing_key_fails_fast:Function] + # #endregion test_missing_key_fails_fast - # [DEF:test_custom_key_roundtrip:Function] - # @PURPOSE: Custom Fernet key produces valid roundtrip. - # @PRE: Generated Fernet key. - # @POST: Encrypt/decrypt with custom key succeeds. + # #region test_custom_key_roundtrip [TYPE Function] + # @BRIEF Custom Fernet key produces valid roundtrip. + # @PRE Generated Fernet key. + # @POST Encrypt/decrypt with custom key succeeds. def test_custom_key_roundtrip(self): custom_key = Fernet.generate_key() fernet = Fernet(custom_key) @@ -132,7 +130,7 @@ class TestEncryptionManager: encrypted = mgr.encrypt("test-with-custom-key") decrypted = mgr.decrypt(encrypted) assert decrypted == "test-with-custom-key" - # [/DEF:test_custom_key_roundtrip:Function] + # #endregion test_custom_key_roundtrip -# [/DEF:TestEncryptionManager:Class] -# [/DEF:test_encryption_manager:Module] +# #endregion TestEncryptionManager +# #endregion test_encryption_manager diff --git a/backend/src/services/__tests__/test_health_service.py b/backend/src/services/__tests__/test_health_service.py index 6903c552..a9f925b7 100644 --- a/backend/src/services/__tests__/test_health_service.py +++ b/backend/src/services/__tests__/test_health_service.py @@ -4,10 +4,9 @@ from unittest.mock import MagicMock, patch from src.services.health_service import HealthService from src.models.llm import ValidationRecord -# [DEF:test_health_service:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Unit tests for HealthService aggregation logic. -# @RELATION: VERIFIES ->[src.services.health_service.HealthService] +# #region test_health_service [C:3] [TYPE Module] +# @BRIEF Unit tests for HealthService aggregation logic. +# @RELATION VERIFIES -> [src.services.health_service.HealthService] @pytest.mark.asyncio @@ -162,9 +161,9 @@ async def test_get_health_summary_reuses_dashboard_metadata_cache_across_service HealthService._dashboard_summary_cache.clear() -# [DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function] -# @RELATION: BINDS_TO ->[test_health_service] -# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks. +# #region test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks [TYPE Function] +# @BRIEF Verify that deleting a validation report also removes dashboard scope and linked tasks. +# @RELATION BINDS_TO -> [test_health_service] def test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks(): db = MagicMock() config_manager = MagicMock() @@ -235,12 +234,12 @@ def test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks(): assert "task-3" in task_manager.tasks -# [/DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function] +# #endregion test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks -# [DEF:test_delete_validation_report_returns_false_for_unknown_record:Function] -# @RELATION: BINDS_TO ->[test_health_service] -# @PURPOSE: Verify delete returns False when validation record does not exist. +# #region test_delete_validation_report_returns_false_for_unknown_record [TYPE Function] +# @BRIEF Verify delete returns False when validation record does not exist. +# @RELATION BINDS_TO -> [test_health_service] def test_delete_validation_report_returns_false_for_unknown_record(): db = MagicMock() db.query.return_value.filter.return_value.first.return_value = None @@ -250,12 +249,12 @@ def test_delete_validation_report_returns_false_for_unknown_record(): assert service.delete_validation_report("missing") is False -# [/DEF:test_delete_validation_report_returns_false_for_unknown_record:Function] +# #endregion test_delete_validation_report_returns_false_for_unknown_record -# [DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function] -# @RELATION: BINDS_TO ->[test_health_service] -# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks. +# #region test_delete_validation_report_swallows_linked_task_cleanup_failure [TYPE Function] +# @BRIEF Verify delete swallows exceptions when cleaning up linked tasks. +# @RELATION BINDS_TO -> [test_health_service] def test_delete_validation_report_swallows_linked_task_cleanup_failure(): db = MagicMock() config_manager = MagicMock() @@ -305,5 +304,5 @@ def test_delete_validation_report_swallows_linked_task_cleanup_failure(): assert "task-1" not in task_manager.tasks -# [/DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function] -# [/DEF:test_health_service:Module] +# #endregion test_delete_validation_report_swallows_linked_task_cleanup_failure +# #endregion test_health_service diff --git a/backend/src/services/__tests__/test_llm_plugin_persistence.py b/backend/src/services/__tests__/test_llm_plugin_persistence.py index 1050a312..a0b9a838 100644 --- a/backend/src/services/__tests__/test_llm_plugin_persistence.py +++ b/backend/src/services/__tests__/test_llm_plugin_persistence.py @@ -1,7 +1,6 @@ -# [DEF:test_llm_plugin_persistence:Module] -# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context. +# #region test_llm_plugin_persistence [C:3] [TYPE Module] +# @BRIEF Regression test for ValidationRecord persistence fields populated from task context. +# @RELATION VERIFIES -> [DashboardValidationPlugin:Class] import types import pytest @@ -9,11 +8,10 @@ import pytest from src.plugins.llm_analysis import plugin as plugin_module -# [DEF:_DummyLogger:Class] -# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] -# @COMPLEXITY: 1 -# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests. -# @INVARIANT: Logging methods are no-ops and must not mutate test state. +# #region _DummyLogger [C:1] [TYPE Class] +# @BRIEF Minimal logger shim for TaskContext-like objects used in tests. +# @INVARIANT Logging methods are no-ops and must not mutate test state. +# @RELATION BINDS_TO -> [test_llm_plugin_persistence:Module] class _DummyLogger: def with_source(self, _source: str): return self @@ -31,14 +29,13 @@ class _DummyLogger: return None -# [/DEF:_DummyLogger:Class] +# #endregion _DummyLogger -# [DEF:_FakeDBSession:Class] -# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin. -# @INVARIANT: add/commit/close provide only persistence signals asserted by this test. +# #region _FakeDBSession [C:2] [TYPE Class] +# @BRIEF Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin. +# @INVARIANT add/commit/close provide only persistence signals asserted by this test. +# @RELATION BINDS_TO -> [test_llm_plugin_persistence:Module] class _FakeDBSession: def __init__(self): self.added = None @@ -55,15 +52,14 @@ class _FakeDBSession: self.closed = True -# [/DEF:_FakeDBSession:Class] +# #endregion _FakeDBSession -# [DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] -# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] -# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id. -# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals. +# #region test_dashboard_validation_plugin_persists_task_and_environment_ids [C:2] [TYPE Function] +# @BRIEF Ensure db ValidationRecord includes context.task_id and params.environment_id. +# @INVARIANT Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals. +# @RELATION BINDS_TO -> [test_llm_plugin_persistence:Module] +# @RELATION VERIFIES -> [DashboardValidationPlugin:Class] @pytest.mark.asyncio async def test_dashboard_validation_plugin_persists_task_and_environment_ids( tmp_path, monkeypatch @@ -80,11 +76,10 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( is_active=True, ) - # [DEF:_FakeProviderService:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 1 - # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests. - # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic. + # #region _FakeProviderService [C:1] [TYPE Class] + # @BRIEF LLM provider service stub returning deterministic provider and decrypted API key for plugin tests. + # @INVARIANT Returns same provider and key regardless of provider_id argument; no lookup logic. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeProviderService: def __init__(self, _db): return None @@ -95,13 +90,12 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( def get_decrypted_api_key(self, _provider_id): return "a" * 32 - # [/DEF:_FakeProviderService:Class] + # #endregion _FakeProviderService - # [DEF:_FakeScreenshotService:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects. - # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values. + # #region _FakeScreenshotService [C:1] [TYPE Class] + # @BRIEF Screenshot service stub that accepts capture_dashboard calls without side effects. + # @INVARIANT capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeScreenshotService: def __init__(self, _env): return None @@ -109,13 +103,12 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( async def capture_dashboard(self, _dashboard_id, _screenshot_path): return None - # [/DEF:_FakeScreenshotService:Class] + # #endregion _FakeScreenshotService - # [DEF:_FakeLLMClient:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions. - # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result. + # #region _FakeLLMClient [C:2] [TYPE Class] + # @BRIEF Deterministic LLM client double returning canonical analysis payload for persistence-path assertions. + # @INVARIANT analyze_dashboard is side-effect free and returns schema-compatible PASS result. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeLLMClient: """Fake LLM client for persistence tests. @@ -133,13 +126,12 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( "issues": [], } - # [/DEF:_FakeLLMClient:Class] + # #endregion _FakeLLMClient - # [DEF:_FakeNotificationService:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects. - # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema. + # #region _FakeNotificationService [C:1] [TYPE Class] + # @BRIEF Notification service stub that accepts plugin dispatch_report payload without introducing side effects. + # @INVARIANT dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeNotificationService: def __init__(self, *_args, **_kwargs): return None @@ -147,13 +139,12 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( async def dispatch_report(self, **_kwargs): return None - # [/DEF:_FakeNotificationService:Class] + # #endregion _FakeNotificationService - # [DEF:_FakeConfigManager:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path. - # @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent. + # #region _FakeConfigManager [C:1] [TYPE Class] + # @BRIEF Config manager stub providing storage root path and minimal settings for plugin execution path. + # @INVARIANT Only storage.root_path and llm fields are safe to access; all other settings fields are absent. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeConfigManager: def get_environment(self, _env_id): return env @@ -166,20 +157,19 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( ) ) - # [/DEF:_FakeConfigManager:Class] + # #endregion _FakeConfigManager - # [DEF:_FakeSupersetClient:Class] - # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list. - # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency. + # #region _FakeSupersetClient [C:1] [TYPE Class] + # @BRIEF Superset client stub exposing network.request as a lambda that returns empty result list. + # @INVARIANT network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency. + # @RELATION BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] class _FakeSupersetClient: def __init__(self, _env): self.network = types.SimpleNamespace( request=lambda **_kwargs: {"result": []} ) - # [/DEF:_FakeSupersetClient:Class] + # #endregion _FakeSupersetClient monkeypatch.setattr(plugin_module, "SessionLocal", lambda: fake_db) monkeypatch.setattr(plugin_module, "LLMProviderService", _FakeProviderService) @@ -215,7 +205,4 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids( assert fake_db.added.environment_id == "env-42" -# [/DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] - - -# [/DEF:test_llm_plugin_persistence:Module] +# #endregion test_dashboard_validation_plugin_persists_task_and_environment_ids diff --git a/backend/src/services/__tests__/test_llm_prompt_templates.py b/backend/src/services/__tests__/test_llm_prompt_templates.py index c90ef225..fc890c52 100644 --- a/backend/src/services/__tests__/test_llm_prompt_templates.py +++ b/backend/src/services/__tests__/test_llm_prompt_templates.py @@ -1,10 +1,8 @@ -# [DEF:test_llm_prompt_templates:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, llm, prompts, templates, settings -# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates. -# @LAYER: Domain Tests -# @RELATION: DEPENDS_ON -> [llm_prompt_templates] -# @INVARIANT: All required prompt keys remain available after normalization. +# #region test_llm_prompt_templates [C:3] [TYPE Module] [SEMANTICS tests, llm, prompts, templates, settings] +# @BRIEF Validate normalization and rendering behavior for configurable LLM prompt templates. +# @LAYER Domain Tests +# @INVARIANT All required prompt keys remain available after normalization. +# @RELATION DEPENDS_ON -> [llm_prompt_templates] from src.services.llm_prompt_templates import ( DEFAULT_LLM_ASSISTANT_SETTINGS, @@ -17,12 +15,11 @@ from src.services.llm_prompt_templates import ( ) -# [DEF:test_normalize_llm_settings_adds_default_prompts:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 3 -# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults. -# @PRE: Input llm settings do not contain complete prompts object. -# @POST: Returned structure includes required prompt templates with fallback defaults. +# #region test_normalize_llm_settings_adds_default_prompts [C:3] [TYPE Function] +# @BRIEF Ensure legacy/partial llm settings are expanded with all prompt defaults. +# @PRE Input llm settings do not contain complete prompts object. +# @POST Returned structure includes required prompt templates with fallback defaults. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_normalize_llm_settings_adds_default_prompts(): normalized = normalize_llm_settings({"default_provider": "x"}) @@ -38,15 +35,14 @@ def test_normalize_llm_settings_adds_default_prompts(): assert key in normalized -# [/DEF:test_normalize_llm_settings_adds_default_prompts:Function] +# #endregion test_normalize_llm_settings_adds_default_prompts -# [DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 3 -# @PURPOSE: Ensure user-customized prompt values are preserved during normalization. -# @PRE: Input llm settings contain custom prompt override. -# @POST: Custom prompt value remains unchanged in normalized output. +# #region test_normalize_llm_settings_keeps_custom_prompt_values [C:3] [TYPE Function] +# @BRIEF Ensure user-customized prompt values are preserved during normalization. +# @PRE Input llm settings contain custom prompt override. +# @POST Custom prompt value remains unchanged in normalized output. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_normalize_llm_settings_keeps_custom_prompt_values(): custom = "Doc for {dataset_name} using {columns_json}" normalized = normalize_llm_settings({"prompts": {"documentation_prompt": custom}}) @@ -54,15 +50,14 @@ def test_normalize_llm_settings_keeps_custom_prompt_values(): assert normalized["prompts"]["documentation_prompt"] == custom -# [/DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function] +# #endregion test_normalize_llm_settings_keeps_custom_prompt_values -# [DEF:test_render_prompt_replaces_known_placeholders:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 3 -# @PURPOSE: Ensure template placeholders are deterministically replaced. -# @PRE: Template contains placeholders matching provided variables. -# @POST: Rendered prompt string contains substituted values. +# #region test_render_prompt_replaces_known_placeholders [C:3] [TYPE Function] +# @BRIEF Ensure template placeholders are deterministically replaced. +# @PRE Template contains placeholders matching provided variables. +# @POST Rendered prompt string contains substituted values. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_render_prompt_replaces_known_placeholders(): rendered = render_prompt( "Hello {name}, diff={diff}", @@ -72,13 +67,12 @@ def test_render_prompt_replaces_known_placeholders(): assert rendered == "Hello bot, diff=A->B" -# [/DEF:test_render_prompt_replaces_known_placeholders:Function] +# #endregion test_render_prompt_replaces_known_placeholders -# [DEF:test_is_multimodal_model_detects_known_vision_models:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 2 -# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names. +# #region test_is_multimodal_model_detects_known_vision_models [C:2] [TYPE Function] +# @BRIEF Ensure multimodal model detection recognizes common vision-capable model names. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_is_multimodal_model_detects_known_vision_models(): assert is_multimodal_model("gpt-4o") is True assert is_multimodal_model("claude-3-5-sonnet") is True @@ -86,13 +80,12 @@ def test_is_multimodal_model_detects_known_vision_models(): assert is_multimodal_model("text-only-model") is False -# [/DEF:test_is_multimodal_model_detects_known_vision_models:Function] +# #endregion test_is_multimodal_model_detects_known_vision_models -# [DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 2 -# @PURPOSE: Verify provider binding resolution priority. +# #region test_resolve_bound_provider_id_prefers_binding_then_default [C:2] [TYPE Function] +# @BRIEF Verify provider binding resolution priority. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_resolve_bound_provider_id_prefers_binding_then_default(): settings = { "default_provider": "default-1", @@ -102,13 +95,12 @@ def test_resolve_bound_provider_id_prefers_binding_then_default(): assert resolve_bound_provider_id(settings, "documentation") == "default-1" -# [/DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function] +# #endregion test_resolve_bound_provider_id_prefers_binding_then_default -# [DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function] -# @RELATION: BINDS_TO -> test_llm_prompt_templates -# @COMPLEXITY: 2 -# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized. +# #region test_normalize_llm_settings_keeps_assistant_planner_settings [C:2] [TYPE Function] +# @BRIEF Ensure assistant planner provider/model fields are preserved and normalized. +# @RELATION BINDS_TO -> [test_llm_prompt_templates] def test_normalize_llm_settings_keeps_assistant_planner_settings(): normalized = normalize_llm_settings( { @@ -120,7 +112,7 @@ def test_normalize_llm_settings_keeps_assistant_planner_settings(): assert normalized["assistant_planner_model"] == "gpt-4.1-mini" -# [/DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function] +# #endregion test_normalize_llm_settings_keeps_assistant_planner_settings -# [/DEF:test_llm_prompt_templates:Module] +# #endregion test_llm_prompt_templates diff --git a/backend/src/services/__tests__/test_llm_provider.py b/backend/src/services/__tests__/test_llm_provider.py index a4240646..0bac34e8 100644 --- a/backend/src/services/__tests__/test_llm_provider.py +++ b/backend/src/services/__tests__/test_llm_provider.py @@ -1,9 +1,7 @@ -# [DEF:test_llm_provider:Module] -# @RELATION: VERIFIES -> [src.services.llm_provider:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, llm-provider, encryption, contract -# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager -# [/DEF:test_llm_provider:Module] +# #region test_llm_provider [C:3] [TYPE Module] [SEMANTICS tests, llm-provider, encryption, contract] +# @BRIEF Contract testing for LLMProviderService and EncryptionManager +# @RELATION VERIFIES -> [src.services.llm_provider:Module] +# #endregion test_llm_provider import pytest import os @@ -14,18 +12,18 @@ from src.services.llm_provider import EncryptionManager, LLMProviderService from src.models.llm import LLMProvider from src.plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType -# [DEF:_test_encryption_key_fixture:Global] -# @PURPOSE: Ensure encryption-dependent provider tests run with a valid Fernet key. -# @RELATION: DEPENDS_ON -> [pytest:Module] +# #region _test_encryption_key_fixture [TYPE Global] +# @BRIEF Ensure encryption-dependent provider tests run with a valid Fernet key. +# @RELATION DEPENDS_ON -> [pytest:Module] os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode()) -# [/DEF:_test_encryption_key_fixture:Global] +# #endregion _test_encryption_key_fixture # @TEST_CONTRACT: EncryptionManagerModel -> Invariants # @TEST_INVARIANT: symmetric_encryption -# [DEF:test_encryption_cycle:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets. +# #region test_encryption_cycle [TYPE Function] +# @BRIEF Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_encryption_cycle(): """Verify encrypted data can be decrypted back to original string.""" manager = EncryptionManager() @@ -36,12 +34,12 @@ def test_encryption_cycle(): # @TEST_EDGE: empty_string_encryption -# [/DEF:test_encryption_cycle:Function] +# #endregion test_encryption_cycle -# [DEF:test_empty_string_encryption:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle. +# #region test_empty_string_encryption [TYPE Function] +# @BRIEF Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_empty_string_encryption(): manager = EncryptionManager() original = "" @@ -50,12 +48,12 @@ def test_empty_string_encryption(): # @TEST_EDGE: decrypt_invalid_data -# [/DEF:test_empty_string_encryption:Function] +# #endregion test_empty_string_encryption -# [DEF:test_decrypt_invalid_data:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Ensure decrypt rejects invalid ciphertext input by raising an exception. +# #region test_decrypt_invalid_data [TYPE Function] +# @BRIEF Ensure decrypt rejects invalid ciphertext input by raising an exception. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_decrypt_invalid_data(): manager = EncryptionManager() with pytest.raises(Exception): @@ -63,50 +61,48 @@ def test_decrypt_invalid_data(): # @TEST_FIXTURE: mock_db_session -# [/DEF:test_decrypt_invalid_data:Function] +# #endregion test_decrypt_invalid_data -# [DEF:mock_db:Fixture] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @COMPLEXITY: 1 -# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests. -# @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced. +# #region mock_db [C:1] [TYPE Fixture] +# @BRIEF MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests. +# @INVARIANT Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced. +# @RELATION BINDS_TO -> [test_llm_provider:Module] @pytest.fixture def mock_db(): # @RISK: query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety. return MagicMock(spec=Session) -# [/DEF:mock_db:Fixture] +# #endregion mock_db -# [DEF:service:Fixture] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @COMPLEXITY: 1 -# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests. +# #region service [C:1] [TYPE Fixture] +# @BRIEF LLMProviderService fixture wired to mock_db for provider CRUD tests. +# @RELATION BINDS_TO -> [test_llm_provider:Module] @pytest.fixture def service(mock_db): return LLMProviderService(db=mock_db) -# [/DEF:service:Fixture] +# #endregion service -# [DEF:test_get_all_providers:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Verify provider list retrieval issues query/all calls on the backing DB session. +# #region test_get_all_providers [TYPE Function] +# @BRIEF Verify provider list retrieval issues query/all calls on the backing DB session. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_get_all_providers(service, mock_db): service.get_all_providers() mock_db.query.assert_called() mock_db.query().all.assert_called() -# [/DEF:test_get_all_providers:Function] +# #endregion test_get_all_providers -# [DEF:test_create_provider:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Ensure provider creation persists entity and stores API key in encrypted form. +# #region test_create_provider [TYPE Function] +# @BRIEF Ensure provider creation persists entity and stores API key in encrypted form. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_create_provider(service, mock_db): config = LLMProviderConfig( provider_type=LLMProviderType.OPENAI, @@ -127,12 +123,12 @@ def test_create_provider(service, mock_db): assert EncryptionManager().decrypt(provider.api_key) == "sk-test" -# [/DEF:test_create_provider:Function] +# #endregion test_create_provider -# [DEF:test_get_decrypted_api_key:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Verify service decrypts stored provider API key for an existing provider record. +# #region test_get_decrypted_api_key [TYPE Function] +# @BRIEF Verify service decrypts stored provider API key for an existing provider record. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_get_decrypted_api_key(service, mock_db): # Setup mock provider encrypted_key = EncryptionManager().encrypt("secret-value") @@ -143,23 +139,23 @@ def test_get_decrypted_api_key(service, mock_db): assert key == "secret-value" -# [/DEF:test_get_decrypted_api_key:Function] +# #endregion test_get_decrypted_api_key -# [DEF:test_get_decrypted_api_key_not_found:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Verify missing provider lookup returns None instead of attempting decryption. +# #region test_get_decrypted_api_key_not_found [TYPE Function] +# @BRIEF Verify missing provider lookup returns None instead of attempting decryption. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_get_decrypted_api_key_not_found(service, mock_db): mock_db.query().filter().first.return_value = None assert service.get_decrypted_api_key("missing") is None -# [/DEF:test_get_decrypted_api_key_not_found:Function] +# #endregion test_get_decrypted_api_key_not_found -# [DEF:test_update_provider_ignores_masked_placeholder_api_key:Function] -# @RELATION: BINDS_TO -> [test_llm_provider:Module] -# @PURPOSE: Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets. +# #region test_update_provider_ignores_masked_placeholder_api_key [TYPE Function] +# @BRIEF Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets. +# @RELATION BINDS_TO -> [test_llm_provider:Module] def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db): existing_encrypted = EncryptionManager().encrypt("secret-value") mock_provider = LLMProvider( @@ -190,4 +186,4 @@ def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db): assert updated.is_active is False -# [/DEF:test_update_provider_ignores_masked_placeholder_api_key:Function] +# #endregion test_update_provider_ignores_masked_placeholder_api_key diff --git a/backend/src/services/__tests__/test_rbac_permission_catalog.py b/backend/src/services/__tests__/test_rbac_permission_catalog.py index 7f2e2c92..a524717f 100644 --- a/backend/src/services/__tests__/test_rbac_permission_catalog.py +++ b/backend/src/services/__tests__/test_rbac_permission_catalog.py @@ -1,10 +1,8 @@ -# [DEF:test_rbac_permission_catalog:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 3 -# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync -# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior. -# @LAYER: Service Tests -# @INVARIANT: Synchronization adds only missing normalized permission pairs. +# #region test_rbac_permission_catalog [C:3] [TYPE Module] [SEMANTICS tests, rbac, permissions, catalog, discovery, sync] +# @BRIEF Verifies RBAC permission catalog discovery and idempotent synchronization behavior. +# @LAYER Service Tests +# @INVARIANT Synchronization adds only missing normalized permission pairs. +# @RELATION BELONGS_TO -> [SrcRoot] # [SECTION: IMPORTS] from types import SimpleNamespace @@ -14,11 +12,11 @@ import src.services.rbac_permission_catalog as catalog # [/SECTION: IMPORTS] -# [DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function] -# @RELATION: BINDS_TO -> test_rbac_permission_catalog -# @PURPOSE: Ensures route-scanner extracts has_permission pairs from route files and skips __tests__. -# @PRE: Temporary route directory contains route and test files. -# @POST: Returned set includes production route permissions and excludes test-only declarations. +# #region test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests [TYPE Function] +# @BRIEF Ensures route-scanner extracts has_permission pairs from route files and skips __tests__. +# @PRE Temporary route directory contains route and test files. +# @POST Returned set includes production route permissions and excludes test-only declarations. +# @RELATION BINDS_TO -> [test_rbac_permission_catalog] def test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tmp_path, monkeypatch): routes_dir = tmp_path / "routes" routes_dir.mkdir(parents=True, exist_ok=True) @@ -49,14 +47,14 @@ def test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tm assert ("plugin:migration", "EXECUTE") in discovered assert ("tasks", "WRITE") in discovered assert ("plugin:ignored", "READ") not in discovered -# [/DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function] +# #endregion test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests -# [DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function] -# @RELATION: BINDS_TO -> test_rbac_permission_catalog -# @PURPOSE: Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights. -# @PRE: Route discovery and plugin loader both return permission sources. -# @POST: Result set contains union of both sources. +# #region test_discover_declared_permissions_unions_route_and_plugin_permissions [TYPE Function] +# @BRIEF Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights. +# @PRE Route discovery and plugin loader both return permission sources. +# @POST Result set contains union of both sources. +# @RELATION BINDS_TO -> [test_rbac_permission_catalog] def test_discover_declared_permissions_unions_route_and_plugin_permissions(monkeypatch): monkeypatch.setattr( catalog, @@ -76,14 +74,14 @@ def test_discover_declared_permissions_unions_route_and_plugin_permissions(monke assert ("plugin:migration", "READ") in discovered assert ("plugin:superset-backup", "EXECUTE") in discovered assert ("plugin:llm_dashboard_validation", "EXECUTE") in discovered -# [/DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function] +# #endregion test_discover_declared_permissions_unions_route_and_plugin_permissions -# [DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function] -# @RELATION: BINDS_TO -> test_rbac_permission_catalog -# @PURPOSE: Ensures synchronization inserts only missing pairs and normalizes action/resource tokens. -# @PRE: DB already contains subset of permissions. -# @POST: Only missing normalized pairs are inserted and commit is executed once. +# #region test_sync_permission_catalog_inserts_only_missing_normalized_pairs [TYPE Function] +# @BRIEF Ensures synchronization inserts only missing pairs and normalizes action/resource tokens. +# @PRE DB already contains subset of permissions. +# @POST Only missing normalized pairs are inserted and commit is executed once. +# @RELATION BINDS_TO -> [test_rbac_permission_catalog] def test_sync_permission_catalog_inserts_only_missing_normalized_pairs(): db = MagicMock() db.query.return_value.all.return_value = [ @@ -110,14 +108,14 @@ def test_sync_permission_catalog_inserts_only_missing_normalized_pairs(): assert inserted_permission.resource == "plugin:migration" assert inserted_permission.action == "READ" db.commit.assert_called_once() -# [/DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function] +# #endregion test_sync_permission_catalog_inserts_only_missing_normalized_pairs -# [DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function] -# @RELATION: BINDS_TO -> test_rbac_permission_catalog -# @PURPOSE: Ensures synchronization is idempotent when all declared pairs already exist. -# @PRE: DB contains full declared permission set. -# @POST: No inserts are added and commit is not called. +# #region test_sync_permission_catalog_is_noop_when_all_permissions_exist [TYPE Function] +# @BRIEF Ensures synchronization is idempotent when all declared pairs already exist. +# @PRE DB contains full declared permission set. +# @POST No inserts are added and commit is not called. +# @RELATION BINDS_TO -> [test_rbac_permission_catalog] def test_sync_permission_catalog_is_noop_when_all_permissions_exist(): db = MagicMock() db.query.return_value.all.return_value = [ @@ -138,7 +136,7 @@ def test_sync_permission_catalog_is_noop_when_all_permissions_exist(): assert inserted_count == 0 db.add.assert_not_called() db.commit.assert_not_called() -# [/DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function] +# #endregion test_sync_permission_catalog_is_noop_when_all_permissions_exist -# [/DEF:test_rbac_permission_catalog:Module] \ No newline at end of file +# #endregion test_rbac_permission_catalog diff --git a/backend/src/services/__tests__/test_resource_service.py b/backend/src/services/__tests__/test_resource_service.py index da65e9a5..bb4dc106 100644 --- a/backend/src/services/__tests__/test_resource_service.py +++ b/backend/src/services/__tests__/test_resource_service.py @@ -1,19 +1,17 @@ -# [DEF:TestResourceService:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: resource-service, tests, dashboards, datasets, activity -# @PURPOSE: Unit tests for ResourceService -# @LAYER: Service -# @RELATION: VERIFIES ->[src.services.resource_service.ResourceService] -# @INVARIANT: Resource summaries preserve task linkage and status projection behavior. +# #region TestResourceService [C:3] [TYPE Module] [SEMANTICS resource-service, tests, dashboards, datasets, activity] +# @BRIEF Unit tests for ResourceService +# @LAYER Service +# @INVARIANT Resource summaries preserve task linkage and status projection behavior. +# @RELATION VERIFIES -> [src.services.resource_service.ResourceService] import pytest from unittest.mock import MagicMock, patch, AsyncMock from datetime import datetime, timezone -# [DEF:test_get_dashboards_with_status:Function] -# @RELATION: BINDS_TO ->[TestResourceService] -# @PURPOSE: Validate dashboard enrichment includes git/task status projections. +# #region test_get_dashboards_with_status [TYPE Function] +# @BRIEF Validate dashboard enrichment includes git/task status projections. +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_dashboards_with_status returns dashboards with git and task status # @PRE: SupersetClient returns dashboard list # @POST: Each dashboard has git_status and last_task fields @@ -73,11 +71,11 @@ async def test_get_dashboards_with_status(): assert result[0]["last_task"]["validation_status"] == "FAIL" -# [/DEF:test_get_dashboards_with_status:Function] +# #endregion test_get_dashboards_with_status -# [DEF:test_get_datasets_with_status:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_datasets_with_status [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_datasets_with_status returns datasets with task status # @PRE: SupersetClient returns dataset list # @POST: Each dataset has last_task field @@ -114,11 +112,11 @@ async def test_get_datasets_with_status(): assert result[0]["last_task"]["status"] == "RUNNING" -# [/DEF:test_get_datasets_with_status:Function] +# #endregion test_get_datasets_with_status -# [DEF:test_get_activity_summary:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_activity_summary [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_activity_summary returns active count and recent tasks # @PRE: tasks list provided # @POST: Returns dict with active_count and recent_tasks @@ -153,11 +151,11 @@ def test_get_activity_summary(): assert len(result["recent_tasks"]) == 3 -# [/DEF:test_get_activity_summary:Function] +# #endregion test_get_activity_summary -# [DEF:test_get_git_status_for_dashboard_no_repo:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_git_status_for_dashboard_no_repo [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _get_git_status_for_dashboard returns None when no repo exists # @PRE: GitService returns None for repo # @POST: Returns None @@ -176,11 +174,11 @@ def test_get_git_status_for_dashboard_no_repo(): assert result["has_repo"] is False -# [/DEF:test_get_git_status_for_dashboard_no_repo:Function] +# #endregion test_get_git_status_for_dashboard_no_repo -# [DEF:test_get_last_task_for_resource:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_last_task_for_resource [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _get_last_task_for_resource returns most recent task for resource # @PRE: tasks list with matching resource_id # @POST: Returns task summary with task_id and status @@ -210,11 +208,11 @@ def test_get_last_task_for_resource(): assert result["status"] == "RUNNING" -# [/DEF:test_get_last_task_for_resource:Function] +# #endregion test_get_last_task_for_resource -# [DEF:test_extract_resource_name_from_task:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_extract_resource_name_from_task [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _extract_resource_name_from_task extracts name from params # @PRE: task has resource_name in params # @POST: Returns resource name or fallback @@ -241,11 +239,11 @@ def test_extract_resource_name_from_task(): assert "task-456" in result2 -# [/DEF:test_extract_resource_name_from_task:Function] +# #endregion test_extract_resource_name_from_task -# [DEF:test_get_last_task_for_resource_empty_tasks:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_last_task_for_resource_empty_tasks [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _get_last_task_for_resource returns None for empty tasks list # @PRE: tasks is empty list # @POST: Returns None @@ -259,11 +257,11 @@ def test_get_last_task_for_resource_empty_tasks(): assert result is None -# [/DEF:test_get_last_task_for_resource_empty_tasks:Function] +# #endregion test_get_last_task_for_resource_empty_tasks -# [DEF:test_get_last_task_for_resource_no_match:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_last_task_for_resource_no_match [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _get_last_task_for_resource returns None when no tasks match resource_id # @PRE: tasks list has no matching resource_id # @POST: Returns None @@ -283,11 +281,11 @@ def test_get_last_task_for_resource_no_match(): assert result is None -# [/DEF:test_get_last_task_for_resource_no_match:Function] +# #endregion test_get_last_task_for_resource_no_match -# [DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors. # @PRE: Task list includes both timezone-aware and timezone-naive timestamps. # @POST: Latest task is selected deterministically and no exception is raised. @@ -327,11 +325,11 @@ async def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_dat assert result[0]["last_task"]["task_id"] == "task-aware" -# [/DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function] +# #endregion test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes -# [DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status. # @PRE: Same dashboard has older WARN and newer UNKNOWN validation tasks. # @POST: Returned last_task points to newest task but preserves WARN as last meaningful validation state. @@ -377,11 +375,11 @@ async def test_get_dashboards_with_status_prefers_latest_decisive_validation_sta assert result[0]["last_task"]["validation_status"] == "WARN" -# [/DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function] +# #endregion test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown -# [DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists. # @PRE: Same dashboard has only UNKNOWN validation tasks. # @POST: Returned last_task keeps newest UNKNOWN task. @@ -426,11 +424,11 @@ async def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_d assert result[0]["last_task"]["validation_status"] == "UNKNOWN" -# [/DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function] +# #endregion test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history -# [DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function] -# @RELATION: BINDS_TO ->[TestResourceService] +# #region test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at [TYPE Function] +# @RELATION BINDS_TO -> [TestResourceService] # @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values. # @PRE: Matching tasks include naive and aware created_at timestamps. # @POST: Latest task is returned without raising datetime comparison errors. @@ -460,7 +458,7 @@ def test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at(): assert result["task_id"] == "task-new" -# [/DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function] +# #endregion test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at -# [/DEF:TestResourceService:Module] +# #endregion TestResourceService diff --git a/backend/src/services/auth_service.py b/backend/src/services/auth_service.py index 22567b64..505c74d1 100644 --- a/backend/src/services/auth_service.py +++ b/backend/src/services/auth_service.py @@ -1,18 +1,16 @@ -# [DEF:auth_service:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: auth, service, business-logic, login, jwt, adfs, jit-provisioning -# @PURPOSE: Orchestrates credential authentication and ADFS JIT user provisioning. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[AuthRepository] -# @RELATION: [DEPENDS_ON] ->[verify_password] -# @RELATION: [DEPENDS_ON] ->[create_access_token] -# @RELATION: [DEPENDS_ON] ->[User] -# @RELATION: [DEPENDS_ON] ->[Role] -# @INVARIANT: Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles. -# @PRE: Core auth models and security utilities available. -# @POST: User identity verified and session tokens issued according to role scopes. -# @SIDE_EFFECT: Writes last login timestamps and JIT-provisions external users. -# @DATA_CONTRACT: [Credentials | ADFSClaims] -> [UserEntity | SessionToken] +# #region auth_service [C:5] [TYPE Module] [SEMANTICS auth, service, business-logic, login, jwt, adfs, jit-provisioning] +# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning. +# @LAYER Domain +# @PRE Core auth models and security utilities available. +# @POST User identity verified and session tokens issued according to role scopes. +# @SIDE_EFFECT Writes last login timestamps and JIT-provisions external users. +# @DATA_CONTRACT [Credentials | ADFSClaims] -> [UserEntity | SessionToken] +# @INVARIANT Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles. +# @RELATION DEPENDS_ON -> [AuthRepository] +# @RELATION DEPENDS_ON -> [verify_password] +# @RELATION DEPENDS_ON -> [create_access_token] +# @RELATION DEPENDS_ON -> [User] +# @RELATION DEPENDS_ON -> [Role] from typing import Dict, Any, Optional, List from datetime import datetime @@ -26,37 +24,34 @@ from ..models.auth import User, Role from ..core.logger import belief_scope -# [DEF:AuthService:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Provides high-level authentication services. -# @RELATION: [DEPENDS_ON] ->[AuthRepository] -# @RELATION: [DEPENDS_ON] ->[User] -# @RELATION: [DEPENDS_ON] ->[Role] +# #region AuthService [C:3] [TYPE Class] +# @BRIEF Provides high-level authentication services. +# @RELATION DEPENDS_ON -> [AuthRepository] +# @RELATION DEPENDS_ON -> [User] +# @RELATION DEPENDS_ON -> [Role] class AuthService: - # [DEF:AuthService_init:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Initializes the authentication service with repository access over an active DB session. - # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context. - # @POST: self.repo is initialized and ready for auth user/role CRUD operations. - # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session. - # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository) + # #region AuthService_init [C:1] [TYPE Function] + # @BRIEF Initializes the authentication service with repository access over an active DB session. + # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context. + # @POST self.repo is initialized and ready for auth user/role CRUD operations. + # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session. + # @DATA_CONTRACT Input(Session) -> Model(AuthRepository) # @PARAM: db (Session) - SQLAlchemy session. def __init__(self, db: Session): self.db = db self.repo = AuthRepository(db) - # [/DEF:AuthService_init:Function] + # #endregion AuthService_init - # [DEF:AuthService.authenticate_user:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Validates credentials and account state for local username/password authentication. - # @PRE: username and password are non-empty credential inputs. - # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None. - # @SIDE_EFFECT: Persists last_login update for successful authentications via repository. - # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None) - # @RELATION: [DEPENDS_ON] ->[AuthRepository] - # @RELATION: [CALLS] ->[verify_password] - # @RELATION: [DEPENDS_ON] ->[User] + # #region AuthService.authenticate_user [C:3] [TYPE Function] + # @BRIEF Validates credentials and account state for local username/password authentication. + # @PRE username and password are non-empty credential inputs. + # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None. + # @SIDE_EFFECT Persists last_login update for successful authentications via repository. + # @DATA_CONTRACT Input(str username, str password) -> Output(User | None) + # @RELATION DEPENDS_ON -> [AuthRepository] + # @RELATION CALLS -> [verify_password] + # @RELATION DEPENDS_ON -> [User] # @PARAM: username (str) - The username. # @PARAM: password (str) - The plain password. # @RETURN: Optional[User] - The authenticated user or None. @@ -76,18 +71,17 @@ class AuthService: return user - # [/DEF:AuthService.authenticate_user:Function] + # #endregion AuthService.authenticate_user - # [DEF:AuthService.create_session:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Issues an access token payload for an already authenticated user. - # @PRE: user is a valid User entity containing username and iterable roles with role.name values. - # @POST: Returns session dict with non-empty access_token and token_type='bearer'. - # @SIDE_EFFECT: Generates signed JWT via auth JWT provider. - # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type}) - # @RELATION: [CALLS] ->[create_access_token] - # @RELATION: [DEPENDS_ON] ->[User] - # @RELATION: [DEPENDS_ON] ->[Role] + # #region AuthService.create_session [C:3] [TYPE Function] + # @BRIEF Issues an access token payload for an already authenticated user. + # @PRE user is a valid User entity containing username and iterable roles with role.name values. + # @POST Returns session dict with non-empty access_token and token_type='bearer'. + # @SIDE_EFFECT Generates signed JWT via auth JWT provider. + # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type}) + # @RELATION CALLS -> [create_access_token] + # @RELATION DEPENDS_ON -> [User] + # @RELATION DEPENDS_ON -> [Role] # @PARAM: user (User) - The authenticated user. # @RETURN: Dict[str, str] - Session data. def create_session(self, user: User) -> Dict[str, str]: @@ -98,18 +92,17 @@ class AuthService: ) return {"access_token": access_token, "token_type": "bearer"} - # [/DEF:AuthService.create_session:Function] + # #endregion AuthService.create_session - # [DEF:AuthService.provision_adfs_user:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings. - # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent. - # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state. - # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state. - # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted) - # @RELATION: [DEPENDS_ON] ->[AuthRepository] - # @RELATION: [DEPENDS_ON] ->[User] - # @RELATION: [DEPENDS_ON] ->[Role] + # #region AuthService.provision_adfs_user [C:3] [TYPE Function] + # @BRIEF Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings. + # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent. + # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state. + # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state. + # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted) + # @RELATION DEPENDS_ON -> [AuthRepository] + # @RELATION DEPENDS_ON -> [User] + # @RELATION DEPENDS_ON -> [Role] # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token. # @RETURN: User - The provisioned user. def provision_adfs_user(self, user_info: Dict[str, Any]) -> User: @@ -141,9 +134,7 @@ class AuthService: return user - # [/DEF:AuthService.provision_adfs_user:Function] + # #endregion AuthService.provision_adfs_user -# [/DEF:AuthService:Class] - -# [/DEF:auth_service:Module] +# #endregion AuthService diff --git a/backend/src/services/clean_release/__init__.py b/backend/src/services/clean_release/__init__.py index 70d02cfb..b16639cd 100644 --- a/backend/src/services/clean_release/__init__.py +++ b/backend/src/services/clean_release/__init__.py @@ -1,11 +1,10 @@ -# [DEF:CleanReleaseContracts:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Publish the canonical semantic root for the clean-release backend service cluster. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator] -# @RELATION: [DEPENDS_ON] ->[ManifestBuilder] -# @RELATION: [DEPENDS_ON] ->[PolicyEngine] -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] +# #region CleanReleaseContracts [C:3] [TYPE Module] +# @BRIEF Publish the canonical semantic root for the clean-release backend service cluster. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [ComplianceOrchestrator] +# @RELATION DEPENDS_ON -> [ManifestBuilder] +# @RELATION DEPENDS_ON -> [PolicyEngine] +# @RELATION DEPENDS_ON -> [RepositoryRelations] from ...core.logger import logger @@ -17,4 +16,4 @@ __all__ = [ "logger", ] -# [/DEF:CleanReleaseContracts:Module] +# #endregion CleanReleaseContracts diff --git a/backend/src/services/clean_release/__tests__/test_audit_service.py b/backend/src/services/clean_release/__tests__/test_audit_service.py index 55b38df4..91b6545c 100644 --- a/backend/src/services/clean_release/__tests__/test_audit_service.py +++ b/backend/src/services/clean_release/__tests__/test_audit_service.py @@ -1,9 +1,7 @@ -# [DEF:TestAuditService:Module] -# @RELATION: [DEPENDS_ON] ->[AuditService] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, audit, logging -# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle. -# @LAYER: Infra +# #region TestAuditService [C:3] [TYPE Module] [SEMANTICS tests, clean-release, audit, logging] +# @BRIEF Validate audit hooks emit expected log patterns for clean release lifecycle. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [AuditService] from unittest.mock import patch from src.services.clean_release.audit_service import ( @@ -14,9 +12,9 @@ from src.services.clean_release.audit_service import ( @patch("src.services.clean_release.audit_service.logger") -# [DEF:test_audit_preparation:Function] -# @RELATION: BINDS_TO -> TestAuditService -# @PURPOSE: Verify audit preparation stage correctly initializes and validates candidate state. +# #region test_audit_preparation [TYPE Function] +# @BRIEF Verify audit preparation stage correctly initializes and validates candidate state. +# @RELATION BINDS_TO -> [TestAuditService] def test_audit_preparation(mock_logger): audit_preparation("cand-1", "PREPARED") mock_logger.info.assert_called_with( @@ -24,13 +22,13 @@ def test_audit_preparation(mock_logger): ) -# [/DEF:test_audit_preparation:Function] +# #endregion test_audit_preparation @patch("src.services.clean_release.audit_service.logger") -# [DEF:test_audit_check_run:Function] -# @RELATION: BINDS_TO -> TestAuditService -# @PURPOSE: Verify audit check run executes all checks and collects results. +# #region test_audit_check_run [TYPE Function] +# @BRIEF Verify audit check run executes all checks and collects results. +# @RELATION BINDS_TO -> [TestAuditService] def test_audit_check_run(mock_logger): audit_check_run("check-1", "COMPLIANT") mock_logger.info.assert_called_with( @@ -38,13 +36,13 @@ def test_audit_check_run(mock_logger): ) -# [/DEF:test_audit_check_run:Function] +# #endregion test_audit_check_run @patch("src.services.clean_release.audit_service.logger") -# [DEF:test_audit_report:Function] -# @RELATION: BINDS_TO -> TestAuditService -# @PURPOSE: Verify audit report generation aggregates check results into a structured report. +# #region test_audit_report [TYPE Function] +# @BRIEF Verify audit report generation aggregates check results into a structured report. +# @RELATION BINDS_TO -> [TestAuditService] def test_audit_report(mock_logger): audit_report("rep-1", "cand-1") mock_logger.info.assert_called_with( @@ -52,5 +50,5 @@ def test_audit_report(mock_logger): ) -# [/DEF:test_audit_report:Function] -# [/DEF:TestAuditService:Module] +# #endregion test_audit_report +# #endregion TestAuditService diff --git a/backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py b/backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py index 180d369b..1e3a32f3 100644 --- a/backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py +++ b/backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py @@ -1,10 +1,8 @@ -# [DEF:TestComplianceOrchestrator:Module] -# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine -# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation. -# @LAYER: Domain -# @INVARIANT: Failed mandatory stage forces BLOCKED terminal status. +# #region TestComplianceOrchestrator [C:3] [TYPE Module] [SEMANTICS tests, clean-release, orchestrator, stage-state-machine] +# @BRIEF Validate compliance orchestrator stage transitions and final status derivation. +# @LAYER Domain +# @INVARIANT Failed mandatory stage forces BLOCKED terminal status. +# @RELATION DEPENDS_ON -> [ComplianceOrchestrator] from unittest.mock import patch @@ -23,9 +21,9 @@ from src.services.clean_release.report_builder import ComplianceReportBuilder from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:test_orchestrator_stage_failure_blocks_release:Function] -# @RELATION: BINDS_TO -> TestComplianceOrchestrator -# @PURPOSE: Verify mandatory stage failure forces BLOCKED final status. +# #region test_orchestrator_stage_failure_blocks_release [TYPE Function] +# @BRIEF Verify mandatory stage failure forces BLOCKED final status. +# @RELATION BINDS_TO -> [TestComplianceOrchestrator] def test_orchestrator_stage_failure_blocks_release(): repository = CleanReleaseRepository() orchestrator = CleanComplianceOrchestrator(repository) @@ -66,12 +64,12 @@ def test_orchestrator_stage_failure_blocks_release(): assert run.final_status == CheckFinalStatus.BLOCKED -# [/DEF:test_orchestrator_stage_failure_blocks_release:Function] +# #endregion test_orchestrator_stage_failure_blocks_release -# [DEF:test_orchestrator_compliant_candidate:Function] -# @RELATION: BINDS_TO -> TestComplianceOrchestrator -# @PURPOSE: Verify happy path where all mandatory stages pass yields COMPLIANT. +# #region test_orchestrator_compliant_candidate [TYPE Function] +# @BRIEF Verify happy path where all mandatory stages pass yields COMPLIANT. +# @RELATION BINDS_TO -> [TestComplianceOrchestrator] def test_orchestrator_compliant_candidate(): repository = CleanReleaseRepository() orchestrator = CleanComplianceOrchestrator(repository) @@ -112,12 +110,12 @@ def test_orchestrator_compliant_candidate(): assert run.final_status == CheckFinalStatus.COMPLIANT -# [/DEF:test_orchestrator_compliant_candidate:Function] +# #endregion test_orchestrator_compliant_candidate -# [DEF:test_orchestrator_missing_stage_result:Function] -# @RELATION: BINDS_TO -> TestComplianceOrchestrator -# @PURPOSE: Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED. +# #region test_orchestrator_missing_stage_result [TYPE Function] +# @BRIEF Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED. +# @RELATION BINDS_TO -> [TestComplianceOrchestrator] def test_orchestrator_missing_stage_result(): repository = CleanReleaseRepository() orchestrator = CleanComplianceOrchestrator(repository) @@ -138,12 +136,12 @@ def test_orchestrator_missing_stage_result(): assert run.final_status == CheckFinalStatus.FAILED -# [/DEF:test_orchestrator_missing_stage_result:Function] +# #endregion test_orchestrator_missing_stage_result -# [DEF:test_orchestrator_report_generation_error:Function] -# @RELATION: BINDS_TO -> TestComplianceOrchestrator -# @PURPOSE: Verify downstream report errors do not mutate orchestrator final status. +# #region test_orchestrator_report_generation_error [TYPE Function] +# @BRIEF Verify downstream report errors do not mutate orchestrator final status. +# @RELATION BINDS_TO -> [TestComplianceOrchestrator] def test_orchestrator_report_generation_error(): repository = CleanReleaseRepository() orchestrator = CleanComplianceOrchestrator(repository) @@ -164,5 +162,5 @@ def test_orchestrator_report_generation_error(): assert run.final_status == CheckFinalStatus.FAILED -# [/DEF:test_orchestrator_report_generation_error:Function] -# [/DEF:TestComplianceOrchestrator:Module] +# #endregion test_orchestrator_report_generation_error +# #endregion TestComplianceOrchestrator diff --git a/backend/src/services/clean_release/__tests__/test_manifest_builder.py b/backend/src/services/clean_release/__tests__/test_manifest_builder.py index 46bb2c68..2c559b68 100644 --- a/backend/src/services/clean_release/__tests__/test_manifest_builder.py +++ b/backend/src/services/clean_release/__tests__/test_manifest_builder.py @@ -1,23 +1,21 @@ -# [DEF:TestManifestBuilder:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: tests, clean-release, manifest, deterministic -# @PURPOSE: Validate deterministic manifest generation behavior for US1. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[ManifestBuilder] -# @INVARIANT: Same input artifacts produce identical deterministic hash. -# @PRE: Test fixtures are properly initialized -# @POST: All test assertions pass -# @SIDE_EFFECT: None - test isolation -# @DATA_CONTRACT: TestInput -> TestOutput +# #region TestManifestBuilder [C:5] [TYPE Module] [SEMANTICS tests, clean-release, manifest, deterministic] +# @BRIEF Validate deterministic manifest generation behavior for US1. +# @LAYER Domain +# @PRE Test fixtures are properly initialized +# @POST All test assertions pass +# @SIDE_EFFECT None - test isolation +# @DATA_CONTRACT TestInput -> TestOutput +# @INVARIANT Same input artifacts produce identical deterministic hash. +# @RELATION DEPENDS_ON -> [ManifestBuilder] from src.services.clean_release.manifest_builder import build_distribution_manifest -# [DEF:test_manifest_deterministic_hash_for_same_input:Function] -# @RELATION: BINDS_TO -> TestManifestBuilder -# @PURPOSE: Ensure hash is stable for same candidate/policy/artifact input. -# @PRE: Same input lists are passed twice. -# @POST: Hash and summary remain identical. +# #region test_manifest_deterministic_hash_for_same_input [TYPE Function] +# @BRIEF Ensure hash is stable for same candidate/policy/artifact input. +# @PRE Same input lists are passed twice. +# @POST Hash and summary remain identical. +# @RELATION BINDS_TO -> [TestManifestBuilder] def test_manifest_deterministic_hash_for_same_input(): artifacts = [ { @@ -54,5 +52,5 @@ def test_manifest_deterministic_hash_for_same_input(): assert manifest1.summary.excluded_count == manifest2.summary.excluded_count -# [/DEF:test_manifest_deterministic_hash_for_same_input:Function] -# [/DEF:TestManifestBuilder:Module] +# #endregion test_manifest_deterministic_hash_for_same_input +# #endregion TestManifestBuilder diff --git a/backend/src/services/clean_release/__tests__/test_policy_engine.py b/backend/src/services/clean_release/__tests__/test_policy_engine.py index b53fff43..11777e37 100644 --- a/backend/src/services/clean_release/__tests__/test_policy_engine.py +++ b/backend/src/services/clean_release/__tests__/test_policy_engine.py @@ -1,7 +1,7 @@ -# [DEF:TestPolicyEngine:Module] -# @RELATION: [DEPENDS_ON] ->[PolicyEngine] -# @PURPOSE: Contract testing for CleanPolicyEngine -# [/DEF:TestPolicyEngine:Module] +# #region TestPolicyEngine [TYPE Module] +# @BRIEF Contract testing for CleanPolicyEngine +# @RELATION DEPENDS_ON -> [PolicyEngine] +# #endregion TestPolicyEngine import pytest from datetime import datetime @@ -48,9 +48,9 @@ def enterprise_clean_setup(): # @TEST_SCENARIO: policy_valid -# [DEF:test_policy_valid:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify policy validation passes when all required fields are present and valid. +# #region test_policy_valid [TYPE Function] +# @BRIEF Verify policy validation passes when all required fields are present and valid. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_policy_valid(enterprise_clean_setup): policy, registry = enterprise_clean_setup engine = CleanPolicyEngine(policy, registry) @@ -60,12 +60,12 @@ def test_policy_valid(enterprise_clean_setup): # @TEST_EDGE: missing_registry_ref -# [/DEF:test_policy_valid:Function] +# #endregion test_policy_valid -# [DEF:test_missing_registry_ref:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify policy validation fails when registry_ref is missing. +# #region test_missing_registry_ref [TYPE Function] +# @BRIEF Verify policy validation fails when registry_ref is missing. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_missing_registry_ref(enterprise_clean_setup): policy, registry = enterprise_clean_setup policy.internal_source_registry_ref = " " @@ -76,12 +76,12 @@ def test_missing_registry_ref(enterprise_clean_setup): # @TEST_EDGE: conflicting_registry -# [/DEF:test_missing_registry_ref:Function] +# #endregion test_missing_registry_ref -# [DEF:test_conflicting_registry:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify policy engine rejects conflicting registry references. +# #region test_conflicting_registry [TYPE Function] +# @BRIEF Verify policy engine rejects conflicting registry references. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_conflicting_registry(enterprise_clean_setup): policy, registry = enterprise_clean_setup registry.registry_id = "WRONG-REG" @@ -95,12 +95,12 @@ def test_conflicting_registry(enterprise_clean_setup): # @TEST_INVARIANT: deterministic_classification -# [/DEF:test_conflicting_registry:Function] +# #endregion test_conflicting_registry -# [DEF:test_classify_artifact:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify policy engine correctly classifies artifacts based on source and type. +# #region test_classify_artifact [TYPE Function] +# @BRIEF Verify policy engine correctly classifies artifacts based on source and type. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_classify_artifact(enterprise_clean_setup): policy, registry = enterprise_clean_setup engine = CleanPolicyEngine(policy, registry) @@ -120,12 +120,12 @@ def test_classify_artifact(enterprise_clean_setup): # @TEST_EDGE: external_endpoint -# [/DEF:test_classify_artifact:Function] +# #endregion test_classify_artifact -# [DEF:test_validate_resource_source:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify validate_resource_source correctly validates or rejects resource source identifiers. +# #region test_validate_resource_source [TYPE Function] +# @BRIEF Verify validate_resource_source correctly validates or rejects resource source identifiers. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_validate_resource_source(enterprise_clean_setup): policy, registry = enterprise_clean_setup engine = CleanPolicyEngine(policy, registry) @@ -141,12 +141,12 @@ def test_validate_resource_source(enterprise_clean_setup): assert res_fail.violation["blocked_release"] is True -# [/DEF:test_validate_resource_source:Function] +# #endregion test_validate_resource_source -# [DEF:test_evaluate_candidate:Function] -# @RELATION: BINDS_TO -> TestPolicyEngine -# @PURPOSE: Verify policy engine evaluates release candidates against configured policies. +# #region test_evaluate_candidate [TYPE Function] +# @BRIEF Verify policy engine evaluates release candidates against configured policies. +# @RELATION BINDS_TO -> [TestPolicyEngine] def test_evaluate_candidate(enterprise_clean_setup): policy, registry = enterprise_clean_setup engine = CleanPolicyEngine(policy, registry) @@ -169,4 +169,4 @@ def test_evaluate_candidate(enterprise_clean_setup): assert violations[1]["category"] == "external-source" -# [/DEF:test_evaluate_candidate:Function] +# #endregion test_evaluate_candidate diff --git a/backend/src/services/clean_release/__tests__/test_preparation_service.py b/backend/src/services/clean_release/__tests__/test_preparation_service.py index 8b7423ac..e58c3362 100644 --- a/backend/src/services/clean_release/__tests__/test_preparation_service.py +++ b/backend/src/services/clean_release/__tests__/test_preparation_service.py @@ -1,10 +1,8 @@ -# [DEF:TestPreparationService:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, preparation, flow -# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[PreparationService] -# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically. +# #region TestPreparationService [C:3] [TYPE Module] [SEMANTICS tests, clean-release, preparation, flow] +# @BRIEF Validate release candidate preparation flow, including policy evaluation and manifest persisting. +# @LAYER Domain +# @INVARIANT Candidate preparation always persists manifest and candidate status deterministically. +# @RELATION DEPENDS_ON -> [PreparationService] import pytest from unittest.mock import MagicMock, patch @@ -22,9 +20,9 @@ from src.models.clean_release import ( from src.services.clean_release.preparation_service import prepare_candidate -# [DEF:_mock_policy:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Build a valid clean profile policy fixture for preparation tests. +# #region _mock_policy [TYPE Function] +# @BRIEF Build a valid clean profile policy fixture for preparation tests. +# @RELATION BINDS_TO -> [TestPreparationService] def _mock_policy() -> CleanProfilePolicy: return CleanProfilePolicy( policy_id="pol-1", @@ -39,12 +37,12 @@ def _mock_policy() -> CleanProfilePolicy: ) -# [/DEF:_mock_policy:Function] +# #endregion _mock_policy -# [DEF:_mock_registry:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Build an internal-only source registry fixture for preparation tests. +# #region _mock_registry [TYPE Function] +# @BRIEF Build an internal-only source registry fixture for preparation tests. +# @RELATION BINDS_TO -> [TestPreparationService] def _mock_registry() -> ResourceSourceRegistry: return ResourceSourceRegistry( registry_id="reg-1", @@ -63,12 +61,12 @@ def _mock_registry() -> ResourceSourceRegistry: ) -# [/DEF:_mock_registry:Function] +# #endregion _mock_registry -# [DEF:_mock_candidate:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Build a draft release candidate fixture with provided identifier. +# #region _mock_candidate [TYPE Function] +# @BRIEF Build a draft release candidate fixture with provided identifier. +# @RELATION BINDS_TO -> [TestPreparationService] def _mock_candidate(candidate_id: str) -> ReleaseCandidate: return ReleaseCandidate( candidate_id=candidate_id, @@ -81,12 +79,12 @@ def _mock_candidate(candidate_id: str) -> ReleaseCandidate: ) -# [/DEF:_mock_candidate:Function] +# #endregion _mock_candidate -# [DEF:test_prepare_candidate_success:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Verify candidate transitions to PREPARED when evaluation returns no violations. +# #region test_prepare_candidate_success [TYPE Function] +# @BRIEF Verify candidate transitions to PREPARED when evaluation returns no violations. +# @RELATION BINDS_TO -> [TestPreparationService] # @TEST_CONTRACT: [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved] # @TEST_SCENARIO: [prepare_success] -> [prepared status and persistence side effects are produced] # @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON @@ -133,12 +131,12 @@ def test_prepare_candidate_success(): repository.save_candidate.assert_called_with(candidate) -# [/DEF:test_prepare_candidate_success:Function] +# #endregion test_prepare_candidate_success -# [DEF:test_prepare_candidate_with_violations:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Verify candidate transitions to BLOCKED when evaluation returns blocking violations. +# #region test_prepare_candidate_with_violations [TYPE Function] +# @BRIEF Verify candidate transitions to BLOCKED when evaluation returns blocking violations. +# @RELATION BINDS_TO -> [TestPreparationService] # @TEST_CONTRACT: [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed] # @TEST_SCENARIO: [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced] # @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON @@ -184,12 +182,12 @@ def test_prepare_candidate_with_violations(): assert len(result["violations"]) == 1 -# [/DEF:test_prepare_candidate_with_violations:Function] +# #endregion test_prepare_candidate_with_violations -# [DEF:test_prepare_candidate_not_found:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Verify preparation raises ValueError when candidate does not exist. +# #region test_prepare_candidate_not_found [TYPE Function] +# @BRIEF Verify preparation raises ValueError when candidate does not exist. +# @RELATION BINDS_TO -> [TestPreparationService] # @TEST_CONTRACT: [missing_candidate] -> [ValueError('Candidate not found')] # @TEST_SCENARIO: [prepare_missing_candidate] -> [raises candidate not found error] # @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON @@ -203,12 +201,12 @@ def test_prepare_candidate_not_found(): prepare_candidate(repository, "non-existent", [], [], "op") -# [/DEF:test_prepare_candidate_not_found:Function] +# #endregion test_prepare_candidate_not_found -# [DEF:test_prepare_candidate_no_active_policy:Function] -# @RELATION: BINDS_TO -> TestPreparationService -# @PURPOSE: Verify preparation raises ValueError when no active policy is available. +# #region test_prepare_candidate_no_active_policy [TYPE Function] +# @BRIEF Verify preparation raises ValueError when no active policy is available. +# @RELATION BINDS_TO -> [TestPreparationService] # @TEST_CONTRACT: [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')] # @TEST_SCENARIO: [prepare_missing_policy] -> [raises active policy missing error] # @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON @@ -223,7 +221,7 @@ def test_prepare_candidate_no_active_policy(): prepare_candidate(repository, "cand-1", [], [], "op") -# [/DEF:test_prepare_candidate_no_active_policy:Function] +# #endregion test_prepare_candidate_no_active_policy -# [/DEF:TestPreparationService:Module] +# #endregion TestPreparationService diff --git a/backend/src/services/clean_release/__tests__/test_report_builder.py b/backend/src/services/clean_release/__tests__/test_report_builder.py index a0c4b10f..65075339 100644 --- a/backend/src/services/clean_release/__tests__/test_report_builder.py +++ b/backend/src/services/clean_release/__tests__/test_report_builder.py @@ -1,10 +1,8 @@ -# [DEF:TestReportBuilder:Module] -# @RELATION: [DEPENDS_ON] ->[ReportBuilder] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, report-builder, counters -# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints. -# @LAYER: Domain -# @INVARIANT: blocked run requires at least one blocking violation. +# #region TestReportBuilder [C:3] [TYPE Module] [SEMANTICS tests, clean-release, report-builder, counters] +# @BRIEF Validate compliance report builder counter integrity and blocked-run constraints. +# @LAYER Domain +# @INVARIANT blocked run requires at least one blocking violation. +# @RELATION DEPENDS_ON -> [ReportBuilder] from datetime import datetime, timezone @@ -22,9 +20,9 @@ from src.services.clean_release.report_builder import ComplianceReportBuilder from src.services.clean_release.repository import CleanReleaseRepository -# [DEF:_terminal_run:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Build terminal/non-terminal run fixtures for report builder tests. +# #region _terminal_run [TYPE Function] +# @BRIEF Build terminal/non-terminal run fixtures for report builder tests. +# @RELATION BINDS_TO -> [TestReportBuilder] def _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun: return ComplianceCheckRun( check_run_id="check-1", @@ -39,12 +37,12 @@ def _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun: ) -# [/DEF:_terminal_run:Function] +# #endregion _terminal_run -# [DEF:_blocking_violation:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Build a blocking violation fixture for blocked report scenarios. +# #region _blocking_violation [TYPE Function] +# @BRIEF Build a blocking violation fixture for blocked report scenarios. +# @RELATION BINDS_TO -> [TestReportBuilder] def _blocking_violation() -> ComplianceViolation: return ComplianceViolation( violation_id="viol-1", @@ -58,12 +56,12 @@ def _blocking_violation() -> ComplianceViolation: ) -# [/DEF:_blocking_violation:Function] +# #endregion _blocking_violation -# [DEF:test_report_builder_blocked_requires_blocking_violations:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Verify BLOCKED run requires at least one blocking violation. +# #region test_report_builder_blocked_requires_blocking_violations [TYPE Function] +# @BRIEF Verify BLOCKED run requires at least one blocking violation. +# @RELATION BINDS_TO -> [TestReportBuilder] def test_report_builder_blocked_requires_blocking_violations(): builder = ComplianceReportBuilder(CleanReleaseRepository()) run = _terminal_run(CheckFinalStatus.BLOCKED) @@ -72,12 +70,12 @@ def test_report_builder_blocked_requires_blocking_violations(): builder.build_report_payload(run, []) -# [/DEF:test_report_builder_blocked_requires_blocking_violations:Function] +# #endregion test_report_builder_blocked_requires_blocking_violations -# [DEF:test_report_builder_blocked_with_two_violations:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Verify report builder generates conformant payload for a BLOCKED run with violations. +# #region test_report_builder_blocked_with_two_violations [TYPE Function] +# @BRIEF Verify report builder generates conformant payload for a BLOCKED run with violations. +# @RELATION BINDS_TO -> [TestReportBuilder] def test_report_builder_blocked_with_two_violations(): builder = ComplianceReportBuilder(CleanReleaseRepository()) run = _terminal_run(CheckFinalStatus.BLOCKED) @@ -95,12 +93,12 @@ def test_report_builder_blocked_with_two_violations(): assert report.blocking_violations_count == 2 -# [/DEF:test_report_builder_blocked_with_two_violations:Function] +# #endregion test_report_builder_blocked_with_two_violations -# [DEF:test_report_builder_counter_consistency:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Verify violations counters remain consistent for blocking payload. +# #region test_report_builder_counter_consistency [TYPE Function] +# @BRIEF Verify violations counters remain consistent for blocking payload. +# @RELATION BINDS_TO -> [TestReportBuilder] def test_report_builder_counter_consistency(): builder = ComplianceReportBuilder(CleanReleaseRepository()) run = _terminal_run(CheckFinalStatus.BLOCKED) @@ -110,12 +108,12 @@ def test_report_builder_counter_consistency(): assert report.blocking_violations_count == 1 -# [/DEF:test_report_builder_counter_consistency:Function] +# #endregion test_report_builder_counter_consistency -# [DEF:test_missing_operator_summary:Function] -# @RELATION: BINDS_TO -> TestReportBuilder -# @PURPOSE: Validate non-terminal run prevents operator summary/report generation. +# #region test_missing_operator_summary [TYPE Function] +# @BRIEF Validate non-terminal run prevents operator summary/report generation. +# @RELATION BINDS_TO -> [TestReportBuilder] def test_missing_operator_summary(): builder = ComplianceReportBuilder(CleanReleaseRepository()) run = _terminal_run(CheckFinalStatus.RUNNING) @@ -126,5 +124,5 @@ def test_missing_operator_summary(): assert "Cannot build report for non-terminal run" in str(exc.value) -# [/DEF:test_missing_operator_summary:Function] -# [/DEF:TestReportBuilder:Module] +# #endregion test_missing_operator_summary +# #endregion TestReportBuilder diff --git a/backend/src/services/clean_release/__tests__/test_source_isolation.py b/backend/src/services/clean_release/__tests__/test_source_isolation.py index 12c9aa9b..380c0833 100644 --- a/backend/src/services/clean_release/__tests__/test_source_isolation.py +++ b/backend/src/services/clean_release/__tests__/test_source_isolation.py @@ -1,10 +1,8 @@ -# [DEF:TestSourceIsolation:Module] -# @RELATION: [DEPENDS_ON] ->[SourceIsolation] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, source-isolation, internal-only -# @PURPOSE: Verify internal source registry validation behavior. -# @LAYER: Domain -# @INVARIANT: External endpoints always produce blocking violations. +# #region TestSourceIsolation [C:3] [TYPE Module] [SEMANTICS tests, clean-release, source-isolation, internal-only] +# @BRIEF Verify internal source registry validation behavior. +# @LAYER Domain +# @INVARIANT External endpoints always produce blocking violations. +# @RELATION DEPENDS_ON -> [SourceIsolation] from datetime import datetime, timezone @@ -12,8 +10,8 @@ from src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry from src.services.clean_release.source_isolation import validate_internal_sources -# [DEF:_registry:Function] -# @RELATION: BINDS_TO -> TestSourceIsolation +# #region _registry [TYPE Function] +# @RELATION BINDS_TO -> [TestSourceIsolation] def _registry() -> ResourceSourceRegistry: return ResourceSourceRegistry( registry_id="registry-internal-v1", @@ -40,12 +38,12 @@ def _registry() -> ResourceSourceRegistry: ) -# [/DEF:_registry:Function] +# #endregion _registry -# [DEF:test_validate_internal_sources_all_internal_ok:Function] -# @RELATION: BINDS_TO -> TestSourceIsolation -# @PURPOSE: Verify validate_internal_sources passes when all sources are internal and allowed. +# #region test_validate_internal_sources_all_internal_ok [TYPE Function] +# @BRIEF Verify validate_internal_sources passes when all sources are internal and allowed. +# @RELATION BINDS_TO -> [TestSourceIsolation] def test_validate_internal_sources_all_internal_ok(): result = validate_internal_sources( registry=_registry(), @@ -55,12 +53,12 @@ def test_validate_internal_sources_all_internal_ok(): assert result["violations"] == [] -# [/DEF:test_validate_internal_sources_all_internal_ok:Function] +# #endregion test_validate_internal_sources_all_internal_ok -# [DEF:test_validate_internal_sources_external_blocked:Function] -# @RELATION: BINDS_TO -> TestSourceIsolation -# @PURPOSE: Verify validate_internal_sources blocks external sources when policy requires internal-only. +# #region test_validate_internal_sources_external_blocked [TYPE Function] +# @BRIEF Verify validate_internal_sources blocks external sources when policy requires internal-only. +# @RELATION BINDS_TO -> [TestSourceIsolation] def test_validate_internal_sources_external_blocked(): result = validate_internal_sources( registry=_registry(), @@ -72,5 +70,5 @@ def test_validate_internal_sources_external_blocked(): assert result["violations"][0]["blocked_release"] is True -# [/DEF:test_validate_internal_sources_external_blocked:Function] -# [/DEF:TestSourceIsolation:Module] +# #endregion test_validate_internal_sources_external_blocked +# #endregion TestSourceIsolation diff --git a/backend/src/services/clean_release/__tests__/test_stages.py b/backend/src/services/clean_release/__tests__/test_stages.py index 8cc69a63..4cb1581e 100644 --- a/backend/src/services/clean_release/__tests__/test_stages.py +++ b/backend/src/services/clean_release/__tests__/test_stages.py @@ -1,9 +1,7 @@ -# [DEF:TestStages:Module] -# @RELATION: [DEPENDS_ON] ->[ComplianceStages] -# @COMPLEXITY: 3 -# @SEMANTICS: tests, clean-release, compliance, stages -# @PURPOSE: Validate final status derivation logic from stage results. -# @LAYER: Domain +# #region TestStages [C:3] [TYPE Module] [SEMANTICS tests, clean-release, compliance, stages] +# @BRIEF Validate final status derivation logic from stage results. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [ComplianceStages] from src.models.clean_release import ( CheckFinalStatus, @@ -14,9 +12,9 @@ from src.models.clean_release import ( from src.services.clean_release.stages import derive_final_status, MANDATORY_STAGE_ORDER -# [DEF:test_derive_final_status_compliant:Function] -# @RELATION: BINDS_TO -> TestStages -# @PURPOSE: Verify derive_final_status returns compliant when all stages pass. +# #region test_derive_final_status_compliant [TYPE Function] +# @BRIEF Verify derive_final_status returns compliant when all stages pass. +# @RELATION BINDS_TO -> [TestStages] def test_derive_final_status_compliant(): results = [ CheckStageResult(stage=s, status=CheckStageStatus.PASS, details="ok") @@ -25,12 +23,12 @@ def test_derive_final_status_compliant(): assert derive_final_status(results) == CheckFinalStatus.COMPLIANT -# [/DEF:test_derive_final_status_compliant:Function] +# #endregion test_derive_final_status_compliant -# [DEF:test_derive_final_status_blocked:Function] -# @RELATION: BINDS_TO -> TestStages -# @PURPOSE: Verify derive_final_status returns blocked when any stage fails. +# #region test_derive_final_status_blocked [TYPE Function] +# @BRIEF Verify derive_final_status returns blocked when any stage fails. +# @RELATION BINDS_TO -> [TestStages] def test_derive_final_status_blocked(): results = [ CheckStageResult(stage=s, status=CheckStageStatus.PASS, details="ok") @@ -40,12 +38,12 @@ def test_derive_final_status_blocked(): assert derive_final_status(results) == CheckFinalStatus.BLOCKED -# [/DEF:test_derive_final_status_blocked:Function] +# #endregion test_derive_final_status_blocked -# [DEF:test_derive_final_status_failed_missing:Function] -# @RELATION: BINDS_TO -> TestStages -# @PURPOSE: Verify derive_final_status returns failed when required stages are missing. +# #region test_derive_final_status_failed_missing [TYPE Function] +# @BRIEF Verify derive_final_status returns failed when required stages are missing. +# @RELATION BINDS_TO -> [TestStages] def test_derive_final_status_failed_missing(): results = [ CheckStageResult( @@ -55,12 +53,12 @@ def test_derive_final_status_failed_missing(): assert derive_final_status(results) == CheckFinalStatus.FAILED -# [/DEF:test_derive_final_status_failed_missing:Function] +# #endregion test_derive_final_status_failed_missing -# [DEF:test_derive_final_status_failed_skipped:Function] -# @RELATION: BINDS_TO -> TestStages -# @PURPOSE: Verify derive_final_status returns failed when critical stages are skipped. +# #region test_derive_final_status_failed_skipped [TYPE Function] +# @BRIEF Verify derive_final_status returns failed when critical stages are skipped. +# @RELATION BINDS_TO -> [TestStages] def test_derive_final_status_failed_skipped(): results = [ CheckStageResult(stage=s, status=CheckStageStatus.PASS, details="ok") @@ -70,5 +68,5 @@ def test_derive_final_status_failed_skipped(): assert derive_final_status(results) == CheckFinalStatus.FAILED -# [/DEF:test_derive_final_status_failed_skipped:Function] -# [/DEF:TestStages:Module] +# #endregion test_derive_final_status_failed_skipped +# #endregion TestStages diff --git a/backend/src/services/clean_release/approval_service.py b/backend/src/services/clean_release/approval_service.py index 68c2ab45..c439bf41 100644 --- a/backend/src/services/clean_release/approval_service.py +++ b/backend/src/services/clean_release/approval_service.py @@ -22,10 +22,10 @@ from .exceptions import ApprovalGateError from .repository import CleanReleaseRepository -# [DEF:_get_or_init_decisions_store:Function] -# @PURPOSE: Provide append-only in-memory storage for approval decisions. -# @PRE: repository is initialized. -# @POST: Returns mutable decision list attached to repository. +# #region _get_or_init_decisions_store [TYPE Function] +# @BRIEF Provide append-only in-memory storage for approval decisions. +# @PRE repository is initialized. +# @POST Returns mutable decision list attached to repository. def _get_or_init_decisions_store( repository: CleanReleaseRepository, ) -> List[ApprovalDecision]: @@ -36,13 +36,13 @@ def _get_or_init_decisions_store( return decisions -# [/DEF:_get_or_init_decisions_store:Function] +# #endregion _get_or_init_decisions_store -# [DEF:_latest_decision_for_candidate:Function] -# @PURPOSE: Resolve latest approval decision for candidate from append-only store. -# @PRE: candidate_id is non-empty. -# @POST: Returns latest ApprovalDecision or None. +# #region _latest_decision_for_candidate [TYPE Function] +# @BRIEF Resolve latest approval decision for candidate from append-only store. +# @PRE candidate_id is non-empty. +# @POST Returns latest ApprovalDecision or None. def _latest_decision_for_candidate( repository: CleanReleaseRepository, candidate_id: str ) -> ApprovalDecision | None: @@ -57,13 +57,13 @@ def _latest_decision_for_candidate( )[0] -# [/DEF:_latest_decision_for_candidate:Function] +# #endregion _latest_decision_for_candidate -# [DEF:_resolve_candidate_and_report:Function] -# @PURPOSE: Validate candidate/report existence and ownership prior to decision persistence. -# @PRE: candidate_id and report_id are non-empty. -# @POST: Returns tuple(candidate, report); raises ApprovalGateError on contract violation. +# #region _resolve_candidate_and_report [TYPE Function] +# @BRIEF Validate candidate/report existence and ownership prior to decision persistence. +# @PRE candidate_id and report_id are non-empty. +# @POST Returns tuple(candidate, report); raises ApprovalGateError on contract violation. def _resolve_candidate_and_report( repository: CleanReleaseRepository, *, @@ -84,13 +84,13 @@ def _resolve_candidate_and_report( return candidate, report -# [/DEF:_resolve_candidate_and_report:Function] +# #endregion _resolve_candidate_and_report -# [DEF:approve_candidate:Function] -# @PURPOSE: Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED. -# @PRE: Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED. -# @POST: Approval decision is appended and candidate transitions to APPROVED. +# #region approve_candidate [TYPE Function] +# @BRIEF Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED. +# @PRE Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED. +# @POST Approval decision is appended and candidate transitions to APPROVED. def approve_candidate( *, repository: CleanReleaseRepository, @@ -160,13 +160,13 @@ def approve_candidate( return decision -# [/DEF:approve_candidate:Function] +# #endregion approve_candidate -# [DEF:reject_candidate:Function] -# @PURPOSE: Persist immutable REJECTED decision without promoting candidate lifecycle. -# @PRE: Candidate exists and report belongs to candidate. -# @POST: Rejected decision is appended; candidate lifecycle is unchanged. +# #region reject_candidate [TYPE Function] +# @BRIEF Persist immutable REJECTED decision without promoting candidate lifecycle. +# @PRE Candidate exists and report belongs to candidate. +# @POST Rejected decision is appended; candidate lifecycle is unchanged. def reject_candidate( *, repository: CleanReleaseRepository, @@ -208,6 +208,6 @@ def reject_candidate( return decision -# [/DEF:reject_candidate:Function] +# #endregion reject_candidate # [/DEF:backend.src.services.clean_release.approval_service:Module] diff --git a/backend/src/services/clean_release/artifact_catalog_loader.py b/backend/src/services/clean_release/artifact_catalog_loader.py index e6eddc64..3540dae6 100644 --- a/backend/src/services/clean_release/artifact_catalog_loader.py +++ b/backend/src/services/clean_release/artifact_catalog_loader.py @@ -15,10 +15,10 @@ from typing import Any, Dict, List from ...models.clean_release import CandidateArtifact -# [DEF:load_bootstrap_artifacts:Function] -# @PURPOSE: Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows. -# @PRE: path points to readable JSON file; payload is list[artifact] or {"artifacts": list[artifact]}. -# @POST: Returns non-mutated CandidateArtifact models with required fields populated. +# #region load_bootstrap_artifacts [TYPE Function] +# @BRIEF Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows. +# @PRE path points to readable JSON file; payload is list[artifact] or {"artifacts": list[artifact]}. +# @POST Returns non-mutated CandidateArtifact models with required fields populated. def load_bootstrap_artifacts(path: str, candidate_id: str) -> List[CandidateArtifact]: if not path or not path.strip(): return [] @@ -102,6 +102,6 @@ def load_bootstrap_artifacts(path: str, candidate_id: str) -> List[CandidateArti return artifacts -# [/DEF:load_bootstrap_artifacts:Function] +# #endregion load_bootstrap_artifacts # [/DEF:backend.src.services.clean_release.artifact_catalog_loader:Module] diff --git a/backend/src/services/clean_release/candidate_service.py b/backend/src/services/clean_release/candidate_service.py index 3d4b883b..1b83d8dd 100644 --- a/backend/src/services/clean_release/candidate_service.py +++ b/backend/src/services/clean_release/candidate_service.py @@ -1,13 +1,11 @@ -# [DEF:candidate_service:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, candidate, artifacts, lifecycle, validation -# @PURPOSE: Register release candidates with validated artifacts and advance lifecycle through legal transitions. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.repository -# @RELATION: DEPENDS_ON -> backend.src.models.clean_release -# @PRE: candidate_id must be unique; artifacts input must be non-empty and valid. -# @POST: candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only. -# @INVARIANT: Candidate lifecycle transitions are delegated to domain guard logic. +# #region candidate_service [C:5] [TYPE Module] [SEMANTICS clean-release, candidate, artifacts, lifecycle, validation] +# @BRIEF Register release candidates with validated artifacts and advance lifecycle through legal transitions. +# @LAYER Domain +# @PRE candidate_id must be unique; artifacts input must be non-empty and valid. +# @POST candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only. +# @INVARIANT Candidate lifecycle transitions are delegated to domain guard logic. +# @RELATION DEPENDS_ON -> [backend.src.services.clean_release.repository] +# @RELATION DEPENDS_ON -> [backend.src.models.clean_release] from __future__ import annotations @@ -19,10 +17,10 @@ from .enums import CandidateStatus from .repository import CleanReleaseRepository -# [DEF:_validate_artifacts:Function] -# @PURPOSE: Validate raw artifact payload list for required fields and shape. -# @PRE: artifacts payload is provided by caller. -# @POST: Returns normalized artifact list or raises ValueError. +# #region _validate_artifacts [TYPE Function] +# @BRIEF Validate raw artifact payload list for required fields and shape. +# @PRE artifacts payload is provided by caller. +# @POST Returns normalized artifact list or raises ValueError. def _validate_artifacts(artifacts: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]: normalized = list(artifacts) if not normalized: @@ -44,13 +42,13 @@ def _validate_artifacts(artifacts: Iterable[Dict[str, Any]]) -> List[Dict[str, A if not isinstance(artifact["size"], int) or artifact["size"] <= 0: raise ValueError(f"artifact[{index}] field 'size' must be a positive integer") return normalized -# [/DEF:_validate_artifacts:Function] +# #endregion _validate_artifacts -# [DEF:register_candidate:Function] -# @PURPOSE: Register a candidate and persist its artifacts with legal lifecycle transition. -# @PRE: candidate_id must be unique and artifacts must pass validation. -# @POST: Candidate exists in repository with PREPARED status and artifacts persisted. +# #region register_candidate [TYPE Function] +# @BRIEF Register a candidate and persist its artifacts with legal lifecycle transition. +# @PRE candidate_id must be unique and artifacts must pass validation. +# @POST Candidate exists in repository with PREPARED status and artifacts persisted. def register_candidate( repository: CleanReleaseRepository, candidate_id: str, @@ -102,6 +100,6 @@ def register_candidate( candidate.transition_to(CandidateStatus.PREPARED) repository.save_candidate(candidate) return candidate -# [/DEF:register_candidate:Function] +# #endregion register_candidate -# [/DEF:backend.src.services.clean_release.candidate_service:Module] \ No newline at end of file +# #endregion candidate_service diff --git a/backend/src/services/clean_release/compliance_execution_service.py b/backend/src/services/clean_release/compliance_execution_service.py index 33eb2ef4..02dab513 100644 --- a/backend/src/services/clean_release/compliance_execution_service.py +++ b/backend/src/services/clean_release/compliance_execution_service.py @@ -1,17 +1,15 @@ -# [DEF:ComplianceExecutionService:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, compliance, execution, stages, immutable-evidence -# @PURPOSE: Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> RepositoryRelations -# @RELATION: DEPENDS_ON -> PolicyResolutionService -# @RELATION: DEPENDS_ON -> ComplianceStages -# @RELATION: DEPENDS_ON -> ReportBuilder -# @PRE: Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request. -# @POST: Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds. -# @SIDE_EFFECT: Persists runs, stage results, violations, and reports through repository adapters and audit helpers. -# @DATA_CONTRACT: Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult] -# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set. +# #region ComplianceExecutionService [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, execution, stages, immutable-evidence] +# @BRIEF Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence. +# @LAYER Domain +# @PRE Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request. +# @POST Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds. +# @SIDE_EFFECT Persists runs, stage results, violations, and reports through repository adapters and audit helpers. +# @DATA_CONTRACT Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult] +# @INVARIANT A run binds to exactly one candidate/manifest/policy/registry snapshot set. +# @RELATION DEPENDS_ON -> [RepositoryRelations] +# @RELATION DEPENDS_ON -> [PolicyResolutionService] +# @RELATION DEPENDS_ON -> [ComplianceStages] +# @RELATION DEPENDS_ON -> [ReportBuilder] from __future__ import annotations @@ -41,8 +39,8 @@ from .stages.base import ComplianceStage, ComplianceStageContext, build_stage_ru belief_logger = cast(Any, logger) -# [DEF:ComplianceExecutionResult:Class] -# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts. +# #region ComplianceExecutionResult [TYPE Class] +# @BRIEF Return envelope for compliance execution with run/report and persisted stage artifacts. @dataclass class ComplianceExecutionResult: run: ComplianceRun @@ -51,15 +49,15 @@ class ComplianceExecutionResult: violations: List[ComplianceViolation] -# [/DEF:ComplianceExecutionResult:Class] +# #endregion ComplianceExecutionResult -# [DEF:ComplianceExecutionService:Class] -# @PURPOSE: Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence. -# @PRE: Database session active, candidate registered -# @POST: Returns ComplianceReport with pass/fail status and violation details -# @SIDE_EFFECT: Updates compliance status in database, logs violations -# @DATA_CONTRACT: ComplianceCheckResult, ComplianceReport, Violation +# #region ComplianceExecutionService [TYPE Class] +# @BRIEF Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence. +# @PRE Database session active, candidate registered +# @POST Returns ComplianceReport with pass/fail status and violation details +# @SIDE_EFFECT Updates compliance status in database, logs violations +# @DATA_CONTRACT ComplianceCheckResult, ComplianceReport, Violation class ComplianceExecutionService: TASK_PLUGIN_ID = "clean-release-compliance" @@ -75,10 +73,10 @@ class ComplianceExecutionService: self.stages = list(stages) if stages is not None else build_default_stages() self.report_builder = ComplianceReportBuilder(repository) - # [DEF:_resolve_manifest:Function] - # @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest. - # @PRE: candidate exists. - # @POST: Returns manifest snapshot or raises ComplianceRunError. + # #region _resolve_manifest [TYPE Function] + # @BRIEF Resolve explicit manifest or fallback to latest candidate manifest. + # @PRE candidate exists. + # @POST Returns manifest snapshot or raises ComplianceRunError. def _resolve_manifest( self, candidate_id: str, manifest_id: Optional[str] ) -> DistributionManifest: @@ -101,29 +99,29 @@ class ComplianceExecutionService: manifests, key=lambda item: item.manifest_version, reverse=True )[0] - # [/DEF:_resolve_manifest:Function] + # #endregion _resolve_manifest - # [DEF:_persist_stage_run:Function] - # @PURPOSE: Persist stage run if repository supports stage records. - # @POST: Stage run is persisted when adapter is available, otherwise no-op. + # #region _persist_stage_run [TYPE Function] + # @BRIEF Persist stage run if repository supports stage records. + # @POST Stage run is persisted when adapter is available, otherwise no-op. def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None: self.repository.save_stage_run(stage_run) - # [/DEF:_persist_stage_run:Function] + # #endregion _persist_stage_run - # [DEF:_persist_violations:Function] - # @PURPOSE: Persist stage violations via repository adapters. - # @POST: Violations are appended to repository evidence store. + # #region _persist_violations [TYPE Function] + # @BRIEF Persist stage violations via repository adapters. + # @POST Violations are appended to repository evidence store. def _persist_violations(self, violations: List[ComplianceViolation]) -> None: for violation in violations: self.repository.save_violation(violation) - # [/DEF:_persist_violations:Function] + # #endregion _persist_violations - # [DEF:execute_run:Function] - # @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success. - # @PRE: candidate exists and trusted policy/registry snapshots are resolvable. - # @POST: Run and evidence are persisted; report exists for SUCCEEDED runs. + # #region execute_run [TYPE Function] + # @BRIEF Execute compliance run stages and finalize immutable report on terminal success. + # @PRE candidate exists and trusted policy/registry snapshots are resolvable. + # @POST Run and evidence are persisted; report exists for SUCCEEDED runs. def execute_run( self, *, @@ -225,9 +223,9 @@ class ComplianceExecutionService: violations=violations, ) - # [/DEF:execute_run:Function] + # #endregion execute_run -# [/DEF:ComplianceExecutionService:Class] +# #endregion ComplianceExecutionService -# [/DEF:ComplianceExecutionService:Module] +# #endregion ComplianceExecutionService diff --git a/backend/src/services/clean_release/compliance_orchestrator.py b/backend/src/services/clean_release/compliance_orchestrator.py index 5190f265..414a3910 100644 --- a/backend/src/services/clean_release/compliance_orchestrator.py +++ b/backend/src/services/clean_release/compliance_orchestrator.py @@ -1,12 +1,10 @@ -# [DEF:ComplianceOrchestrator:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, orchestrator, compliance-gate, stages -# @PURPOSE: Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[ComplianceStages] -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @INVARIANT: COMPLIANT is impossible when any mandatory stage fails. +# #region ComplianceOrchestrator [C:5] [TYPE Module] [SEMANTICS clean-release, orchestrator, compliance-gate, stages] +# @BRIEF Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome. +# @LAYER Domain +# @INVARIANT COMPLIANT is impossible when any mandatory stage fails. +# @RELATION DEPENDS_ON -> [ComplianceStages] +# @RELATION DEPENDS_ON -> [RepositoryRelations] +# @RELATION DEPENDS_ON -> [CleanReleaseModels] # @TEST_CONTRACT: ComplianceCheckRun -> ComplianceCheckRun # @TEST_FIXTURE: compliant_candidate -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json # @TEST_EDGE: stage_failure_blocks_release -> Mandatory stage returns FAIL and final status becomes BLOCKED @@ -43,27 +41,27 @@ from .stages import derive_final_status from ...core.logger import belief_scope, logger -# [DEF:CleanComplianceOrchestrator:Class] -# @PURPOSE: Coordinate clean-release compliance verification stages. +# #region CleanComplianceOrchestrator [TYPE Class] +# @BRIEF Coordinate clean-release compliance verification stages. class CleanComplianceOrchestrator: - # [DEF:__init__:Function] - # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups. - # @PRE: repository is a valid CleanReleaseRepository instance with required methods. - # @POST: self.repository is assigned and used by all orchestration steps. - # @SIDE_EFFECT: Stores repository reference on orchestrator instance. - # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None + # #region __init__ [TYPE Function] + # @BRIEF Bind repository dependency used for orchestrator persistence and lookups. + # @PRE repository is a valid CleanReleaseRepository instance with required methods. + # @POST self.repository is assigned and used by all orchestration steps. + # @SIDE_EFFECT Stores repository reference on orchestrator instance. + # @DATA_CONTRACT Input -> CleanReleaseRepository, Output -> None def __init__(self, repository: CleanReleaseRepository): with belief_scope("CleanComplianceOrchestrator.__init__"): self.repository = repository - # [/DEF:__init__:Function] + # #endregion __init__ - # [DEF:start_check_run:Function] - # @PURPOSE: Initiate a new compliance run session. - # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records. - # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository. - # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run. - # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun + # #region start_check_run [TYPE Function] + # @BRIEF Initiate a new compliance run session. + # @PRE candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records. + # @POST Returns initialized ComplianceRun in RUNNING state persisted in repository. + # @SIDE_EFFECT Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run. + # @DATA_CONTRACT Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun def start_check_run( self, candidate_id: str, @@ -151,14 +149,14 @@ class CleanComplianceOrchestrator: ) return self.repository.save_check_run(check_run) - # [/DEF:start_check_run:Function] + # #endregion start_check_run - # [DEF:execute_stages:Function] - # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields. - # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository. - # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set. - # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run. - # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun + # #region execute_stages [TYPE Function] + # @BRIEF Execute or accept compliance stage outcomes and set intermediate/final check-run status fields. + # @PRE check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository. + # @POST Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set. + # @SIDE_EFFECT Reads candidate/policy/registry/manifest and persists updated check_run. + # @DATA_CONTRACT Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun def execute_stages( self, check_run: ComplianceRun, @@ -213,14 +211,14 @@ class CleanComplianceOrchestrator: return self.repository.save_check_run(check_run) - # [/DEF:execute_stages:Function] + # #endregion execute_stages - # [DEF:finalize_run:Function] - # @PURPOSE: Finalize run status based on cumulative stage results. - # @PRE: check_run was started and may already contain a derived final_status from stage execution. - # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty. - # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run. - # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun + # #region finalize_run [TYPE Function] + # @BRIEF Finalize run status based on cumulative stage results. + # @PRE check_run was started and may already contain a derived final_status from stage execution. + # @POST Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty. + # @SIDE_EFFECT Mutates check_run terminal fields and persists via repository.save_check_run. + # @DATA_CONTRACT Input -> ComplianceRun, Output -> ComplianceRun def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun: with belief_scope("finalize_run"): if check_run.status == RunStatus.FAILED: @@ -240,18 +238,18 @@ class CleanComplianceOrchestrator: check_run.finished_at = datetime.now(timezone.utc) return self.repository.save_check_run(check_run) - # [/DEF:finalize_run:Function] + # #endregion finalize_run -# [/DEF:CleanComplianceOrchestrator:Class] +# #endregion CleanComplianceOrchestrator -# [DEF:run_check_legacy:Function] -# @PURPOSE: Legacy wrapper for compatibility with previous orchestrator call style. -# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies. -# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence. -# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls. -# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun +# #region run_check_legacy [TYPE Function] +# @BRIEF Legacy wrapper for compatibility with previous orchestrator call style. +# @PRE repository and identifiers are valid and resolvable by orchestrator dependencies. +# @POST Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence. +# @SIDE_EFFECT Reads/writes compliance entities through repository during orchestrator calls. +# @DATA_CONTRACT Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun def run_check_legacy( repository: CleanReleaseRepository, candidate_id: str, @@ -271,5 +269,5 @@ def run_check_legacy( return orchestrator.finalize_run(run) -# [/DEF:run_check_legacy:Function] -# [/DEF:ComplianceOrchestrator:Module] +# #endregion run_check_legacy +# #endregion ComplianceOrchestrator diff --git a/backend/src/services/clean_release/demo_data_service.py b/backend/src/services/clean_release/demo_data_service.py index 245832b5..dcf90201 100644 --- a/backend/src/services/clean_release/demo_data_service.py +++ b/backend/src/services/clean_release/demo_data_service.py @@ -1,20 +1,18 @@ -# [DEF:DemoDataService:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, demo-mode, namespace, isolation, repository -# @PURPOSE: Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers. +# #region DemoDataService [C:3] [TYPE Module] [SEMANTICS clean-release, demo-mode, namespace, isolation, repository] +# @BRIEF Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes. +# @LAYER Domain +# @INVARIANT Demo and real namespaces must never collide for generated physical identifiers. +# @RELATION DEPENDS_ON -> [RepositoryRelations] from __future__ import annotations from .repository import CleanReleaseRepository -# [DEF:resolve_namespace:Function] -# @PURPOSE: Resolve canonical clean-release namespace for requested mode. -# @PRE: mode is a non-empty string identifying runtime mode. -# @POST: Returns deterministic namespace key for demo/real separation. +# #region resolve_namespace [TYPE Function] +# @BRIEF Resolve canonical clean-release namespace for requested mode. +# @PRE mode is a non-empty string identifying runtime mode. +# @POST Returns deterministic namespace key for demo/real separation. def resolve_namespace(mode: str) -> str: normalized = (mode or "").strip().lower() if normalized == "demo": @@ -22,13 +20,13 @@ def resolve_namespace(mode: str) -> str: return "clean-release:real" -# [/DEF:resolve_namespace:Function] +# #endregion resolve_namespace -# [DEF:build_namespaced_id:Function] -# @PURPOSE: Build storage-safe physical identifier under mode namespace. -# @PRE: namespace and logical_id are non-empty strings. -# @POST: Returns deterministic "{namespace}::{logical_id}" identifier. +# #region build_namespaced_id [TYPE Function] +# @BRIEF Build storage-safe physical identifier under mode namespace. +# @PRE namespace and logical_id are non-empty strings. +# @POST Returns deterministic "{namespace}::{logical_id}" identifier. def build_namespaced_id(namespace: str, logical_id: str) -> str: if not namespace or not namespace.strip(): raise ValueError("namespace must be non-empty") @@ -37,13 +35,13 @@ def build_namespaced_id(namespace: str, logical_id: str) -> str: return f"{namespace}::{logical_id}" -# [/DEF:build_namespaced_id:Function] +# #endregion build_namespaced_id -# [DEF:create_isolated_repository:Function] -# @PURPOSE: Create isolated in-memory repository instance for selected mode namespace. -# @PRE: mode is a valid runtime mode marker. -# @POST: Returns repository instance tagged with namespace metadata. +# #region create_isolated_repository [TYPE Function] +# @BRIEF Create isolated in-memory repository instance for selected mode namespace. +# @PRE mode is a valid runtime mode marker. +# @POST Returns repository instance tagged with namespace metadata. def create_isolated_repository(mode: str) -> CleanReleaseRepository: namespace = resolve_namespace(mode) repository = CleanReleaseRepository() @@ -51,6 +49,6 @@ def create_isolated_repository(mode: str) -> CleanReleaseRepository: return repository -# [/DEF:create_isolated_repository:Function] +# #endregion create_isolated_repository -# [/DEF:DemoDataService:Module] +# #endregion DemoDataService diff --git a/backend/src/services/clean_release/dto.py b/backend/src/services/clean_release/dto.py index 29da0d4e..7cd095d1 100644 --- a/backend/src/services/clean_release/dto.py +++ b/backend/src/services/clean_release/dto.py @@ -1,8 +1,7 @@ -# [DEF:clean_release_dto:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Data Transfer Objects for clean release compliance subsystem. -# @LAYER: Application -# @RELATION: DEPENDS_ON -> pydantic +# #region clean_release_dto [C:3] [TYPE Module] +# @BRIEF Data Transfer Objects for clean release compliance subsystem. +# @LAYER Application +# @RELATION DEPENDS_ON -> [pydantic] from datetime import datetime from typing import List, Optional, Dict, Any @@ -83,4 +82,4 @@ class CandidateOverviewDTO(BaseModel): latest_publication_id: Optional[str] = None latest_publication_status: Optional[str] = None -# [/DEF:clean_release_dto:Module] \ No newline at end of file +# #endregion clean_release_dto diff --git a/backend/src/services/clean_release/enums.py b/backend/src/services/clean_release/enums.py index 57401695..bc1607d4 100644 --- a/backend/src/services/clean_release/enums.py +++ b/backend/src/services/clean_release/enums.py @@ -1,8 +1,7 @@ -# [DEF:clean_release_enums:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Canonical enums for clean release lifecycle and compliance. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> enum +# #region clean_release_enums [C:3] [TYPE Module] +# @BRIEF Canonical enums for clean release lifecycle and compliance. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [enum] from enum import Enum @@ -70,4 +69,4 @@ class ViolationCategory(str, Enum): MANIFEST_CONSISTENCY = "MANIFEST_CONSISTENCY" EXTERNAL_ENDPOINT = "EXTERNAL_ENDPOINT" -# [/DEF:clean_release_enums:Module] \ No newline at end of file +# #endregion clean_release_enums diff --git a/backend/src/services/clean_release/exceptions.py b/backend/src/services/clean_release/exceptions.py index 2229c7d3..30d7a2f5 100644 --- a/backend/src/services/clean_release/exceptions.py +++ b/backend/src/services/clean_release/exceptions.py @@ -1,8 +1,7 @@ -# [DEF:clean_release_exceptions:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Domain exceptions for clean release compliance subsystem. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> Exception +# #region clean_release_exceptions [C:3] [TYPE Module] +# @BRIEF Domain exceptions for clean release compliance subsystem. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [Exception] class CleanReleaseError(Exception): """Base exception for clean release subsystem.""" @@ -36,4 +35,4 @@ class PublicationGateError(CleanReleaseError): """Raised when publication requirements are not met.""" pass -# [/DEF:clean_release_exceptions:Module] \ No newline at end of file +# #endregion clean_release_exceptions diff --git a/backend/src/services/clean_release/facade.py b/backend/src/services/clean_release/facade.py index edc3a927..202aa9dc 100644 --- a/backend/src/services/clean_release/facade.py +++ b/backend/src/services/clean_release/facade.py @@ -1,8 +1,7 @@ -# [DEF:clean_release_facade:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Unified entry point for clean release operations. -# @LAYER: Application -# @RELATION: DEPENDS_ON -> ComplianceOrchestrator +# #region clean_release_facade [C:3] [TYPE Module] +# @BRIEF Unified entry point for clean release operations. +# @LAYER Application +# @RELATION DEPENDS_ON -> [ComplianceOrchestrator] from typing import List, Optional from src.services.clean_release.repositories import ( @@ -120,4 +119,4 @@ class CleanReleaseFacade: candidates = self.candidate_repo.list_all() return [self.get_candidate_overview(c.id) for c in candidates] -# [/DEF:clean_release_facade:Module] \ No newline at end of file +# #endregion clean_release_facade diff --git a/backend/src/services/clean_release/manifest_builder.py b/backend/src/services/clean_release/manifest_builder.py index b7d58ed0..918bb2f7 100644 --- a/backend/src/services/clean_release/manifest_builder.py +++ b/backend/src/services/clean_release/manifest_builder.py @@ -1,10 +1,8 @@ -# [DEF:ManifestBuilder:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, manifest, deterministic-hash, summary -# @PURPOSE: Build deterministic distribution manifest from classified artifact input. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @INVARIANT: Equal semantic artifact sets produce identical deterministic hash values. +# #region ManifestBuilder [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, deterministic-hash, summary] +# @BRIEF Build deterministic distribution manifest from classified artifact input. +# @LAYER Domain +# @INVARIANT Equal semantic artifact sets produce identical deterministic hash values. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] from __future__ import annotations @@ -54,10 +52,10 @@ def _stable_hash_payload( return digest -# [DEF:build_distribution_manifest:Function] -# @PURPOSE: Build DistributionManifest with deterministic hash and validated counters. -# @PRE: artifacts list contains normalized classification values. -# @POST: Returns DistributionManifest with summary counts matching items cardinality. +# #region build_distribution_manifest [TYPE Function] +# @BRIEF Build DistributionManifest with deterministic hash and validated counters. +# @PRE artifacts list contains normalized classification values. +# @POST Returns DistributionManifest with summary counts matching items cardinality. def build_distribution_manifest( manifest_id: str, candidate_id: str, @@ -108,13 +106,13 @@ def build_distribution_manifest( ) -# [/DEF:build_distribution_manifest:Function] +# #endregion build_distribution_manifest -# [DEF:build_manifest:Function] -# @PURPOSE: Legacy compatibility wrapper for old manifest builder import paths. -# @PRE: Same as build_distribution_manifest. -# @POST: Returns DistributionManifest produced by canonical builder. +# #region build_manifest [TYPE Function] +# @BRIEF Legacy compatibility wrapper for old manifest builder import paths. +# @PRE Same as build_distribution_manifest. +# @POST Returns DistributionManifest produced by canonical builder. def build_manifest( manifest_id: str, candidate_id: str, @@ -131,5 +129,5 @@ def build_manifest( ) -# [/DEF:build_manifest:Function] -# [/DEF:ManifestBuilder:Module] +# #endregion build_manifest +# #endregion ManifestBuilder diff --git a/backend/src/services/clean_release/manifest_service.py b/backend/src/services/clean_release/manifest_service.py index b534f5d4..42228ec0 100644 --- a/backend/src/services/clean_release/manifest_service.py +++ b/backend/src/services/clean_release/manifest_service.py @@ -1,16 +1,14 @@ -# [DEF:ManifestService:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, manifest, versioning, immutability, lifecycle -# @PURPOSE: Build immutable distribution manifests with deterministic digest and version increment. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @RELATION: [DEPENDS_ON] ->[ManifestBuilder] -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @PRE: Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present. -# @POST: New immutable manifest is persisted with incremented version and deterministic digest. -# @INVARIANT: Existing manifests are never mutated. -# @SIDE_EFFECT: May modify manifest state during processing -# @DATA_CONTRACT: Manifest -> ManifestRecord; Candidate -> ManifestRecord +# #region ManifestService [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, versioning, immutability, lifecycle] +# @BRIEF Build immutable distribution manifests with deterministic digest and version increment. +# @LAYER Domain +# @PRE Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present. +# @POST New immutable manifest is persisted with incremented version and deterministic digest. +# @SIDE_EFFECT May modify manifest state during processing +# @DATA_CONTRACT Manifest -> ManifestRecord; Candidate -> ManifestRecord +# @INVARIANT Existing manifests are never mutated. +# @RELATION DEPENDS_ON -> [RepositoryRelations] +# @RELATION DEPENDS_ON -> [ManifestBuilder] +# @RELATION DEPENDS_ON -> [CleanReleaseModels] from __future__ import annotations @@ -22,10 +20,10 @@ from .manifest_builder import build_distribution_manifest from .repository import CleanReleaseRepository -# [DEF:build_manifest_snapshot:Function] -# @PURPOSE: Create a new immutable manifest version for a candidate. -# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid. -# @POST: Returns persisted DistributionManifest with monotonically incremented version. +# #region build_manifest_snapshot [TYPE Function] +# @BRIEF Create a new immutable manifest version for a candidate. +# @PRE Candidate is prepared, artifacts are available, candidate_id is valid. +# @POST Returns persisted DistributionManifest with monotonically incremented version. def build_manifest_snapshot( repository: CleanReleaseRepository, candidate_id: str, @@ -92,6 +90,6 @@ def build_manifest_snapshot( return manifest -# [/DEF:build_manifest_snapshot:Function] +# #endregion build_manifest_snapshot -# [/DEF:ManifestService:Module] +# #endregion ManifestService diff --git a/backend/src/services/clean_release/mappers.py b/backend/src/services/clean_release/mappers.py index d049e863..e1f72685 100644 --- a/backend/src/services/clean_release/mappers.py +++ b/backend/src/services/clean_release/mappers.py @@ -1,8 +1,7 @@ -# [DEF:clean_release_mappers:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Map between domain entities (SQLAlchemy models) and DTOs. -# @LAYER: Application -# @RELATION: DEPENDS_ON -> clean_release_dto +# #region clean_release_mappers [C:3] [TYPE Module] +# @BRIEF Map between domain entities (SQLAlchemy models) and DTOs. +# @LAYER Application +# @RELATION DEPENDS_ON -> [clean_release_dto] from typing import List from src.models.clean_release import ( @@ -65,4 +64,4 @@ def map_report_to_dto(report: ComplianceReport) -> ReportDTO: generated_at=report.generated_at ) -# [/DEF:clean_release_mappers:Module] \ No newline at end of file +# #endregion clean_release_mappers diff --git a/backend/src/services/clean_release/policy_engine.py b/backend/src/services/clean_release/policy_engine.py index 626fd4dd..ff84166c 100644 --- a/backend/src/services/clean_release/policy_engine.py +++ b/backend/src/services/clean_release/policy_engine.py @@ -1,15 +1,13 @@ -# [DEF:PolicyEngine:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, policy, classification, source-isolation -# @PURPOSE: Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @RELATION: [DEPENDS_ON] ->[LoggerModule] -# @INVARIANT: Enterprise-clean policy always treats non-registry sources as violations. -# @DATA_CONTRACT: Candidate -> PolicyDecision -# @PRE: PolicyRepository is accessible -# @POST: PolicyDecision returned with approval status -# @SIDE_EFFECT: Read-only policy evaluation; no state changes +# #region PolicyEngine [C:5] [TYPE Module] [SEMANTICS clean-release, policy, classification, source-isolation] +# @BRIEF Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes. +# @LAYER Domain +# @PRE PolicyRepository is accessible +# @POST PolicyDecision returned with approval status +# @SIDE_EFFECT Read-only policy evaluation; no state changes +# @DATA_CONTRACT Candidate -> PolicyDecision +# @INVARIANT Enterprise-clean policy always treats non-registry sources as violations. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] +# @RELATION DEPENDS_ON -> [LoggerModule] from __future__ import annotations @@ -37,9 +35,9 @@ class SourceValidationResult: violation: Dict | None -# [DEF:CleanPolicyEngine:Class] -# @PRE: Active policy exists and is internally consistent. -# @POST: Deterministic classification and source validation are available. +# #region CleanPolicyEngine [TYPE Class] +# @PRE Active policy exists and is internally consistent. +# @POST Deterministic classification and source validation are available. # @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult # @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True # @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json @@ -232,5 +230,5 @@ class CleanPolicyEngine: return classified, violations -# [/DEF:CleanPolicyEngine:Class] -# [/DEF:PolicyEngine:Module] +# #endregion CleanPolicyEngine +# #endregion PolicyEngine diff --git a/backend/src/services/clean_release/policy_resolution_service.py b/backend/src/services/clean_release/policy_resolution_service.py index 59762134..753f4602 100644 --- a/backend/src/services/clean_release/policy_resolution_service.py +++ b/backend/src/services/clean_release/policy_resolution_service.py @@ -1,16 +1,14 @@ -# [DEF:PolicyResolutionService:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, policy, registry, trusted-resolution, immutable-snapshots -# @PURPOSE: Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[ConfigManager] -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @RELATION: [DEPENDS_ON] ->[clean_release_exceptions] -# @INVARIANT: Trusted snapshot resolution is based only on ConfigManager active identifiers. -# @DATA_CONTRACT: PolicyRequest -> ResolutionResult -# @PRE: PolicyRepository and Manifest are available -# @POST: ResolutionResult with matched policies -# @SIDE_EFFECT: Read-only policy evaluation; logs resolution decisions +# #region PolicyResolutionService [C:5] [TYPE Module] [SEMANTICS clean-release, policy, registry, trusted-resolution, immutable-snapshots] +# @BRIEF Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides. +# @LAYER Domain +# @PRE PolicyRepository and Manifest are available +# @POST ResolutionResult with matched policies +# @SIDE_EFFECT Read-only policy evaluation; logs resolution decisions +# @DATA_CONTRACT PolicyRequest -> ResolutionResult +# @INVARIANT Trusted snapshot resolution is based only on ConfigManager active identifiers. +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [RepositoryRelations] +# @RELATION DEPENDS_ON -> [clean_release_exceptions] from __future__ import annotations @@ -21,11 +19,11 @@ from .exceptions import PolicyResolutionError from .repository import CleanReleaseRepository -# [DEF:resolve_trusted_policy_snapshots:Function] -# @PURPOSE: Resolve immutable trusted policy and registry snapshots using active config IDs only. -# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots. -# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected. -# @SIDE_EFFECT: None. +# #region resolve_trusted_policy_snapshots [TYPE Function] +# @BRIEF Resolve immutable trusted policy and registry snapshots using active config IDs only. +# @PRE ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots. +# @POST Returns immutable policy and registry snapshots; runtime override attempts are rejected. +# @SIDE_EFFECT None. def resolve_trusted_policy_snapshots( *, config_manager, @@ -77,6 +75,6 @@ def resolve_trusted_policy_snapshots( return policy_snapshot, registry_snapshot -# [/DEF:resolve_trusted_policy_snapshots:Function] +# #endregion resolve_trusted_policy_snapshots -# [/DEF:PolicyResolutionService:Module] +# #endregion PolicyResolutionService diff --git a/backend/src/services/clean_release/preparation_service.py b/backend/src/services/clean_release/preparation_service.py index 49575c0f..3e54da36 100644 --- a/backend/src/services/clean_release/preparation_service.py +++ b/backend/src/services/clean_release/preparation_service.py @@ -1,12 +1,10 @@ -# [DEF:PreparationService:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, preparation, manifest, policy-evaluation -# @PURPOSE: Prepare release candidate by policy evaluation and deterministic manifest creation. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[PolicyEngine] -# @RELATION: [DEPENDS_ON] ->[ManifestBuilder] -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically. +# #region PreparationService [C:3] [TYPE Module] [SEMANTICS clean-release, preparation, manifest, policy-evaluation] +# @BRIEF Prepare release candidate by policy evaluation and deterministic manifest creation. +# @LAYER Domain +# @INVARIANT Candidate preparation always persists manifest and candidate status deterministically. +# @RELATION DEPENDS_ON -> [PolicyEngine] +# @RELATION DEPENDS_ON -> [ManifestBuilder] +# @RELATION DEPENDS_ON -> [RepositoryRelations] from __future__ import annotations @@ -89,10 +87,10 @@ def prepare_candidate( } -# [DEF:prepare_candidate_legacy:Function] -# @PURPOSE: Legacy compatibility wrapper kept for migration period. -# @PRE: Same as prepare_candidate. -# @POST: Delegates to canonical prepare_candidate and preserves response shape. +# #region prepare_candidate_legacy [TYPE Function] +# @BRIEF Legacy compatibility wrapper kept for migration period. +# @PRE Same as prepare_candidate. +# @POST Delegates to canonical prepare_candidate and preserves response shape. def prepare_candidate_legacy( repository: CleanReleaseRepository, candidate_id: str, @@ -109,5 +107,5 @@ def prepare_candidate_legacy( ) -# [/DEF:prepare_candidate_legacy:Function] -# [/DEF:PreparationService:Module] +# #endregion prepare_candidate_legacy +# #endregion PreparationService diff --git a/backend/src/services/clean_release/publication_service.py b/backend/src/services/clean_release/publication_service.py index d37badb5..f151a7c2 100644 --- a/backend/src/services/clean_release/publication_service.py +++ b/backend/src/services/clean_release/publication_service.py @@ -23,10 +23,10 @@ from .exceptions import PublicationGateError from .repository import CleanReleaseRepository -# [DEF:_get_or_init_publications_store:Function] -# @PURPOSE: Provide in-memory append-only publication storage. -# @PRE: repository is initialized. -# @POST: Returns publication list attached to repository. +# #region _get_or_init_publications_store [TYPE Function] +# @BRIEF Provide in-memory append-only publication storage. +# @PRE repository is initialized. +# @POST Returns publication list attached to repository. def _get_or_init_publications_store( repository: CleanReleaseRepository, ) -> List[PublicationRecord]: @@ -37,13 +37,13 @@ def _get_or_init_publications_store( return publications -# [/DEF:_get_or_init_publications_store:Function] +# #endregion _get_or_init_publications_store -# [DEF:_latest_publication_for_candidate:Function] -# @PURPOSE: Resolve latest publication record for candidate. -# @PRE: candidate_id is non-empty. -# @POST: Returns latest record or None. +# #region _latest_publication_for_candidate [TYPE Function] +# @BRIEF Resolve latest publication record for candidate. +# @PRE candidate_id is non-empty. +# @POST Returns latest record or None. def _latest_publication_for_candidate( repository: CleanReleaseRepository, candidate_id: str, @@ -62,13 +62,13 @@ def _latest_publication_for_candidate( )[0] -# [/DEF:_latest_publication_for_candidate:Function] +# #endregion _latest_publication_for_candidate -# [DEF:_latest_approval_for_candidate:Function] -# @PURPOSE: Resolve latest approval decision from repository decision store. -# @PRE: candidate_id is non-empty. -# @POST: Returns latest decision object or None. +# #region _latest_approval_for_candidate [TYPE Function] +# @BRIEF Resolve latest approval decision from repository decision store. +# @PRE candidate_id is non-empty. +# @POST Returns latest decision object or None. def _latest_approval_for_candidate( repository: CleanReleaseRepository, candidate_id: str ): @@ -83,13 +83,13 @@ def _latest_approval_for_candidate( )[0] -# [/DEF:_latest_approval_for_candidate:Function] +# #endregion _latest_approval_for_candidate -# [DEF:publish_candidate:Function] -# @PURPOSE: Create immutable publication record for approved candidate. -# @PRE: Candidate exists, report belongs to candidate, latest approval is APPROVED. -# @POST: New ACTIVE publication record is appended. +# #region publish_candidate [TYPE Function] +# @BRIEF Create immutable publication record for approved candidate. +# @PRE Candidate exists, report belongs to candidate, latest approval is APPROVED. +# @POST New ACTIVE publication record is appended. def publish_candidate( *, repository: CleanReleaseRepository, @@ -163,13 +163,13 @@ def publish_candidate( return record -# [/DEF:publish_candidate:Function] +# #endregion publish_candidate -# [DEF:revoke_publication:Function] -# @PURPOSE: Revoke existing publication record without deleting history. -# @PRE: publication_id exists in repository publication store. -# @POST: Target publication status becomes REVOKED and updated record is returned. +# #region revoke_publication [TYPE Function] +# @BRIEF Revoke existing publication record without deleting history. +# @PRE publication_id exists in repository publication store. +# @POST Target publication status becomes REVOKED and updated record is returned. def revoke_publication( *, repository: CleanReleaseRepository, @@ -210,6 +210,6 @@ def revoke_publication( return record -# [/DEF:revoke_publication:Function] +# #endregion revoke_publication # [/DEF:backend.src.services.clean_release.publication_service:Module] diff --git a/backend/src/services/clean_release/report_builder.py b/backend/src/services/clean_release/report_builder.py index 88b9ddb9..7e09bd30 100644 --- a/backend/src/services/clean_release/report_builder.py +++ b/backend/src/services/clean_release/report_builder.py @@ -1,11 +1,9 @@ -# [DEF:ReportBuilder:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: clean-release, report, audit, counters, violations -# @PURPOSE: Build and persist compliance reports with consistent counter invariants. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @RELATION: [DEPENDS_ON] ->[RepositoryRelations] -# @INVARIANT: blocking_violations_count never exceeds violations_count. +# #region ReportBuilder [C:5] [TYPE Module] [SEMANTICS clean-release, report, audit, counters, violations] +# @BRIEF Build and persist compliance reports with consistent counter invariants. +# @LAYER Domain +# @INVARIANT blocking_violations_count never exceeds violations_count. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] +# @RELATION DEPENDS_ON -> [RepositoryRelations] # @TEST_CONTRACT: ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport # @TEST_FIXTURE: blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json # @TEST_EDGE: empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError @@ -76,4 +74,4 @@ class ComplianceReportBuilder: return self.repository.save_report(report) -# [/DEF:ReportBuilder:Module] +# #endregion ReportBuilder diff --git a/backend/src/services/clean_release/repositories/__init__.py b/backend/src/services/clean_release/repositories/__init__.py index 05b78bc3..c79f11d8 100644 --- a/backend/src/services/clean_release/repositories/__init__.py +++ b/backend/src/services/clean_release/repositories/__init__.py @@ -1,7 +1,6 @@ -# [DEF:clean_release_repositories:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Export all clean release repositories. -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region clean_release_repositories [C:3] [TYPE Module] +# @BRIEF Export all clean release repositories. +# @RELATION DEPENDS_ON -> [sqlalchemy] from .candidate_repository import CandidateRepository from .artifact_repository import ArtifactRepository @@ -26,4 +25,4 @@ __all__ = [ "CleanReleaseAuditLog" ] -# [/DEF:clean_release_repositories:Module] \ No newline at end of file +# #endregion clean_release_repositories diff --git a/backend/src/services/clean_release/repositories/approval_repository.py b/backend/src/services/clean_release/repositories/approval_repository.py index 2cbe095b..d44f016a 100644 --- a/backend/src/services/clean_release/repositories/approval_repository.py +++ b/backend/src/services/clean_release/repositories/approval_repository.py @@ -1,8 +1,7 @@ -# [DEF:approval_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query approval decisions. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region approval_repository [C:3] [TYPE Module] +# @BRIEF Persist and query approval decisions. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -51,4 +50,4 @@ class ApprovalRepository: with belief_scope("ApprovalRepository.list_by_candidate"): return self.db.query(ApprovalDecision).filter(ApprovalDecision.candidate_id == candidate_id).all() -# [/DEF:approval_repository:Module] \ No newline at end of file +# #endregion approval_repository diff --git a/backend/src/services/clean_release/repositories/artifact_repository.py b/backend/src/services/clean_release/repositories/artifact_repository.py index 53b95a69..1e371855 100644 --- a/backend/src/services/clean_release/repositories/artifact_repository.py +++ b/backend/src/services/clean_release/repositories/artifact_repository.py @@ -1,8 +1,7 @@ -# [DEF:artifact_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query candidate artifacts. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region artifact_repository [C:3] [TYPE Module] +# @BRIEF Persist and query candidate artifacts. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -52,4 +51,4 @@ class ArtifactRepository: with belief_scope("ArtifactRepository.list_by_candidate"): return self.db.query(CandidateArtifact).filter(CandidateArtifact.candidate_id == candidate_id).all() -# [/DEF:artifact_repository:Module] \ No newline at end of file +# #endregion artifact_repository diff --git a/backend/src/services/clean_release/repositories/audit_repository.py b/backend/src/services/clean_release/repositories/audit_repository.py index 9bd83119..8113a25b 100644 --- a/backend/src/services/clean_release/repositories/audit_repository.py +++ b/backend/src/services/clean_release/repositories/audit_repository.py @@ -1,8 +1,7 @@ -# [DEF:audit_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query audit logs for clean release operations. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region audit_repository [C:3] [TYPE Module] +# @BRIEF Persist and query audit logs for clean release operations. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -44,4 +43,4 @@ class AuditRepository: with belief_scope("AuditRepository.list_by_candidate"): return self.db.query(CleanReleaseAuditLog).filter(CleanReleaseAuditLog.candidate_id == candidate_id).all() -# [/DEF:audit_repository:Module] \ No newline at end of file +# #endregion audit_repository diff --git a/backend/src/services/clean_release/repositories/candidate_repository.py b/backend/src/services/clean_release/repositories/candidate_repository.py index 39650684..1689f772 100644 --- a/backend/src/services/clean_release/repositories/candidate_repository.py +++ b/backend/src/services/clean_release/repositories/candidate_repository.py @@ -1,8 +1,7 @@ -# [DEF:candidate_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query release candidates. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region candidate_repository [C:3] [TYPE Module] +# @BRIEF Persist and query release candidates. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -45,4 +44,4 @@ class CandidateRepository: with belief_scope("CandidateRepository.list_all"): return self.db.query(ReleaseCandidate).all() -# [/DEF:candidate_repository:Module] \ No newline at end of file +# #endregion candidate_repository diff --git a/backend/src/services/clean_release/repositories/compliance_repository.py b/backend/src/services/clean_release/repositories/compliance_repository.py index 9e03f324..e9ad9444 100644 --- a/backend/src/services/clean_release/repositories/compliance_repository.py +++ b/backend/src/services/clean_release/repositories/compliance_repository.py @@ -1,8 +1,7 @@ -# [DEF:compliance_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query compliance runs, stage runs, and violations. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region compliance_repository [C:3] [TYPE Module] +# @BRIEF Persist and query compliance runs, stage runs, and violations. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -85,4 +84,4 @@ class ComplianceRepository: with belief_scope("ComplianceRepository.list_violations_by_run"): return self.db.query(ComplianceViolation).filter(ComplianceViolation.run_id == run_id).all() -# [/DEF:compliance_repository:Module] \ No newline at end of file +# #endregion compliance_repository diff --git a/backend/src/services/clean_release/repositories/manifest_repository.py b/backend/src/services/clean_release/repositories/manifest_repository.py index 42f18db7..2b8145ae 100644 --- a/backend/src/services/clean_release/repositories/manifest_repository.py +++ b/backend/src/services/clean_release/repositories/manifest_repository.py @@ -1,10 +1,9 @@ -# [DEF:ManifestRepositoryModule:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query distribution manifests. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> DistributionManifest -# @RELATION: DEPENDS_ON -> sqlalchemy -# @RELATION: DEPENDS_ON -> belief_scope +# #region ManifestRepositoryModule [C:3] [TYPE Module] +# @BRIEF Persist and query distribution manifests. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [DistributionManifest] +# @RELATION DEPENDS_ON -> [sqlalchemy] +# @RELATION DEPENDS_ON -> [belief_scope] from typing import Optional, List from sqlalchemy.orm import Session @@ -12,57 +11,52 @@ from src.models.clean_release import DistributionManifest from src.core.logger import belief_scope -# [DEF:ManifestRepository:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Encapsulates database CRUD operations for DistributionManifest entities. -# @RELATION: DEPENDS_ON -> DistributionManifest -# @RELATION: DEPENDS_ON -> sqlalchemy.Session +# #region ManifestRepository [C:3] [TYPE Class] +# @BRIEF Encapsulates database CRUD operations for DistributionManifest entities. +# @RELATION DEPENDS_ON -> [DistributionManifest] +# @RELATION DEPENDS_ON -> [sqlalchemy.Session] class ManifestRepository: """Repository for distribution manifest persistence.""" - # [DEF:ManifestRepository.__init__:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Initialize repository with an active SQLAlchemy session. - # @PRE: db is a valid SQLAlchemy Session instance. - # @POST: Repository is ready for database operations. + # #region ManifestRepository.__init__ [C:1] [TYPE Function] + # @BRIEF Initialize repository with an active SQLAlchemy session. + # @PRE db is a valid SQLAlchemy Session instance. + # @POST Repository is ready for database operations. def __init__(self, db: Session): self.db = db - # [/DEF:ManifestRepository.__init__:Function] + # #endregion ManifestRepository.__init__ - # [DEF:ManifestRepository.save:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persist a DistributionManifest to the database. - # @PRE: manifest is a valid DistributionManifest instance with required fields populated. - # @POST: Manifest is committed to database and refreshed with generated ID. - # @SIDE_EFFECT: Database commit via session.commit(). - # @RELATION: DEPENDS_ON -> DistributionManifest + # #region ManifestRepository.save [C:3] [TYPE Function] + # @BRIEF Persist a DistributionManifest to the database. + # @PRE manifest is a valid DistributionManifest instance with required fields populated. + # @POST Manifest is committed to database and refreshed with generated ID. + # @SIDE_EFFECT Database commit via session.commit(). + # @RELATION DEPENDS_ON -> [DistributionManifest] def save(self, manifest: DistributionManifest) -> DistributionManifest: with belief_scope("ManifestRepository.save"): self.db.add(manifest) self.db.commit() self.db.refresh(manifest) return manifest - # [/DEF:ManifestRepository.save:Function] + # #endregion ManifestRepository.save - # [DEF:ManifestRepository.get_by_id:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Retrieve a single DistributionManifest by its primary key. - # @PRE: manifest_id is a valid string identifier. - # @POST: Returns DistributionManifest if found, None otherwise. - # @RELATION: DEPENDS_ON -> DistributionManifest + # #region ManifestRepository.get_by_id [C:2] [TYPE Function] + # @BRIEF Retrieve a single DistributionManifest by its primary key. + # @PRE manifest_id is a valid string identifier. + # @POST Returns DistributionManifest if found, None otherwise. + # @RELATION DEPENDS_ON -> [DistributionManifest] def get_by_id(self, manifest_id: str) -> Optional[DistributionManifest]: with belief_scope("ManifestRepository.get_by_id"): return self.db.query(DistributionManifest).filter( DistributionManifest.id == manifest_id ).first() - # [/DEF:ManifestRepository.get_by_id:Function] + # #endregion ManifestRepository.get_by_id - # [DEF:ManifestRepository.get_latest_for_candidate:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Retrieve the most recent manifest version for a given candidate. - # @PRE: candidate_id is a valid string identifier. - # @POST: Returns the highest manifest_version manifest for the candidate, or None. - # @RELATION: DEPENDS_ON -> DistributionManifest + # #region ManifestRepository.get_latest_for_candidate [C:3] [TYPE Function] + # @BRIEF Retrieve the most recent manifest version for a given candidate. + # @PRE candidate_id is a valid string identifier. + # @POST Returns the highest manifest_version manifest for the candidate, or None. + # @RELATION DEPENDS_ON -> [DistributionManifest] def get_latest_for_candidate(self, candidate_id: str) -> Optional[DistributionManifest]: with belief_scope("ManifestRepository.get_latest_for_candidate"): return ( @@ -71,14 +65,13 @@ class ManifestRepository: .order_by(DistributionManifest.manifest_version.desc()) .first() ) - # [/DEF:ManifestRepository.get_latest_for_candidate:Function] + # #endregion ManifestRepository.get_latest_for_candidate - # [DEF:ManifestRepository.list_by_candidate:Function] - # @COMPLEXITY: 2 - # @PURPOSE: List all manifests for a specific candidate, ordered by version. - # @PRE: candidate_id is a valid string identifier. - # @POST: Returns a list of DistributionManifest instances (may be empty). - # @RELATION: DEPENDS_ON -> DistributionManifest + # #region ManifestRepository.list_by_candidate [C:2] [TYPE Function] + # @BRIEF List all manifests for a specific candidate, ordered by version. + # @PRE candidate_id is a valid string identifier. + # @POST Returns a list of DistributionManifest instances (may be empty). + # @RELATION DEPENDS_ON -> [DistributionManifest] def list_by_candidate(self, candidate_id: str) -> List[DistributionManifest]: with belief_scope("ManifestRepository.list_by_candidate"): return ( @@ -86,8 +79,6 @@ class ManifestRepository: .filter(DistributionManifest.candidate_id == candidate_id) .all() ) - # [/DEF:ManifestRepository.list_by_candidate:Function] + # #endregion ManifestRepository.list_by_candidate -# [/DEF:ManifestRepository:Class] - -# [/DEF:ManifestRepositoryModule:Module] \ No newline at end of file +# #endregion ManifestRepository diff --git a/backend/src/services/clean_release/repositories/policy_repository.py b/backend/src/services/clean_release/repositories/policy_repository.py index 7f84d846..2faadc71 100644 --- a/backend/src/services/clean_release/repositories/policy_repository.py +++ b/backend/src/services/clean_release/repositories/policy_repository.py @@ -1,8 +1,7 @@ -# [DEF:policy_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query policy and registry snapshots. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region policy_repository [C:3] [TYPE Module] +# @BRIEF Persist and query policy and registry snapshots. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -50,4 +49,4 @@ class PolicyRepository: with belief_scope("PolicyRepository.get_registry_snapshot"): return self.db.query(SourceRegistrySnapshot).filter(SourceRegistrySnapshot.id == snapshot_id).first() -# [/DEF:policy_repository:Module] \ No newline at end of file +# #endregion policy_repository diff --git a/backend/src/services/clean_release/repositories/publication_repository.py b/backend/src/services/clean_release/repositories/publication_repository.py index 91f23da2..da98a4d9 100644 --- a/backend/src/services/clean_release/repositories/publication_repository.py +++ b/backend/src/services/clean_release/repositories/publication_repository.py @@ -1,8 +1,7 @@ -# [DEF:publication_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query publication records. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region publication_repository [C:3] [TYPE Module] +# @BRIEF Persist and query publication records. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -51,4 +50,4 @@ class PublicationRepository: with belief_scope("PublicationRepository.list_by_candidate"): return self.db.query(PublicationRecord).filter(PublicationRecord.candidate_id == candidate_id).all() -# [/DEF:publication_repository:Module] \ No newline at end of file +# #endregion publication_repository diff --git a/backend/src/services/clean_release/repositories/report_repository.py b/backend/src/services/clean_release/repositories/report_repository.py index 657aec45..b41f3ed9 100644 --- a/backend/src/services/clean_release/repositories/report_repository.py +++ b/backend/src/services/clean_release/repositories/report_repository.py @@ -1,8 +1,7 @@ -# [DEF:report_repository:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Persist and query compliance reports. -# @LAYER: Infra -# @RELATION: DEPENDS_ON -> sqlalchemy +# #region report_repository [C:3] [TYPE Module] +# @BRIEF Persist and query compliance reports. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [sqlalchemy] from typing import Optional, List from sqlalchemy.orm import Session @@ -48,4 +47,4 @@ class ReportRepository: with belief_scope("ReportRepository.list_by_candidate"): return self.db.query(ComplianceReport).filter(ComplianceReport.candidate_id == candidate_id).all() -# [/DEF:report_repository:Module] \ No newline at end of file +# #endregion report_repository diff --git a/backend/src/services/clean_release/repository.py b/backend/src/services/clean_release/repository.py index 4a773531..b256bb5f 100644 --- a/backend/src/services/clean_release/repository.py +++ b/backend/src/services/clean_release/repository.py @@ -1,10 +1,8 @@ -# [DEF:RepositoryRelations:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, repository, persistence, in-memory -# @PURPOSE: Provide repository adapter for clean release entities with deterministic access methods. -# @LAYER: Infra -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @INVARIANT: Repository operations are side-effect free outside explicit save/update calls. +# #region RepositoryRelations [C:3] [TYPE Module] [SEMANTICS clean-release, repository, persistence, in-memory] +# @BRIEF Provide repository adapter for clean release entities with deterministic access methods. +# @LAYER Infra +# @INVARIANT Repository operations are side-effect free outside explicit save/update calls. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] from __future__ import annotations @@ -23,8 +21,8 @@ from ...models.clean_release import ( ) -# [DEF:CleanReleaseRepository:Class] -# @PURPOSE: Data access object for clean release lifecycle. +# #region CleanReleaseRepository [TYPE Class] +# @BRIEF Data access object for clean release lifecycle. @dataclass class CleanReleaseRepository: candidates: Dict[str, ReleaseCandidate] = field(default_factory=dict) @@ -176,5 +174,5 @@ class CleanReleaseRepository: self.violations.clear() -# [/DEF:CleanReleaseRepository:Class] -# [/DEF:RepositoryRelations:Module] +# #endregion CleanReleaseRepository +# #endregion RepositoryRelations diff --git a/backend/src/services/clean_release/source_isolation.py b/backend/src/services/clean_release/source_isolation.py index 87141e1b..5787ddcb 100644 --- a/backend/src/services/clean_release/source_isolation.py +++ b/backend/src/services/clean_release/source_isolation.py @@ -1,10 +1,8 @@ -# [DEF:SourceIsolation:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, source-isolation, internal-only, validation -# @PURPOSE: Validate that all resource endpoints belong to the approved internal source registry. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @INVARIANT: Any endpoint outside enabled registry entries is treated as external-source violation. +# #region SourceIsolation [C:3] [TYPE Module] [SEMANTICS clean-release, source-isolation, internal-only, validation] +# @BRIEF Validate that all resource endpoints belong to the approved internal source registry. +# @LAYER Domain +# @INVARIANT Any endpoint outside enabled registry entries is treated as external-source violation. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] from __future__ import annotations @@ -36,4 +34,4 @@ def validate_internal_sources( return {"ok": len(violations) == 0, "violations": violations} -# [/DEF:SourceIsolation:Module] +# #endregion SourceIsolation diff --git a/backend/src/services/clean_release/stages/__init__.py b/backend/src/services/clean_release/stages/__init__.py index 8fc91ddc..0e111954 100644 --- a/backend/src/services/clean_release/stages/__init__.py +++ b/backend/src/services/clean_release/stages/__init__.py @@ -1,11 +1,9 @@ -# [DEF:ComplianceStages:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance, stages, state-machine -# @PURPOSE: Define compliance stage order and helper functions for deterministic run-state evaluation. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @RELATION: [DEPENDS_ON] ->[ComplianceStageBase] -# @INVARIANT: Stage order remains deterministic for all compliance runs. +# #region ComplianceStages [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, stages, state-machine] +# @BRIEF Define compliance stage order and helper functions for deterministic run-state evaluation. +# @LAYER Domain +# @INVARIANT Stage order remains deterministic for all compliance runs. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] +# @RELATION DEPENDS_ON -> [ComplianceStageBase] from __future__ import annotations @@ -32,10 +30,10 @@ MANDATORY_STAGE_ORDER: List[ComplianceStageName] = [ ] -# [DEF:build_default_stages:Function] -# @PURPOSE: Build default deterministic stage pipeline implementation order. -# @PRE: None. -# @POST: Returns stage instances in mandatory execution order. +# #region build_default_stages [TYPE Function] +# @BRIEF Build default deterministic stage pipeline implementation order. +# @PRE None. +# @POST Returns stage instances in mandatory execution order. def build_default_stages() -> List[ComplianceStage]: return [ DataPurityStage(), @@ -45,13 +43,13 @@ def build_default_stages() -> List[ComplianceStage]: ] -# [/DEF:build_default_stages:Function] +# #endregion build_default_stages -# [DEF:stage_result_map:Function] -# @PURPOSE: Convert stage result list to dictionary by stage name. -# @PRE: stage_results may be empty or contain unique stage names. -# @POST: Returns stage->status dictionary for downstream evaluation. +# #region stage_result_map [TYPE Function] +# @BRIEF Convert stage result list to dictionary by stage name. +# @PRE stage_results may be empty or contain unique stage names. +# @POST Returns stage->status dictionary for downstream evaluation. def stage_result_map( stage_results: Iterable[ComplianceStageRun | CheckStageResult], ) -> Dict[ComplianceStageName, CheckStageStatus]: @@ -84,26 +82,26 @@ def stage_result_map( return normalized -# [/DEF:stage_result_map:Function] +# #endregion stage_result_map -# [DEF:missing_mandatory_stages:Function] -# @PURPOSE: Identify mandatory stages that are absent from run results. -# @PRE: stage_status_map contains zero or more known stage statuses. -# @POST: Returns ordered list of missing mandatory stages. +# #region missing_mandatory_stages [TYPE Function] +# @BRIEF Identify mandatory stages that are absent from run results. +# @PRE stage_status_map contains zero or more known stage statuses. +# @POST Returns ordered list of missing mandatory stages. def missing_mandatory_stages( stage_status_map: Dict[ComplianceStageName, CheckStageStatus], ) -> List[ComplianceStageName]: return [stage for stage in MANDATORY_STAGE_ORDER if stage not in stage_status_map] -# [/DEF:missing_mandatory_stages:Function] +# #endregion missing_mandatory_stages -# [DEF:derive_final_status:Function] -# @PURPOSE: Derive final run status from stage results with deterministic blocking behavior. -# @PRE: Stage statuses correspond to compliance checks. -# @POST: Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes. +# #region derive_final_status [TYPE Function] +# @BRIEF Derive final run status from stage results with deterministic blocking behavior. +# @PRE Stage statuses correspond to compliance checks. +# @POST Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes. def derive_final_status( stage_results: Iterable[ComplianceStageRun | CheckStageResult], ) -> CheckFinalStatus: @@ -122,5 +120,5 @@ def derive_final_status( return CheckFinalStatus.COMPLIANT -# [/DEF:derive_final_status:Function] -# [/DEF:ComplianceStages:Module] +# #endregion derive_final_status +# #endregion ComplianceStages diff --git a/backend/src/services/clean_release/stages/base.py b/backend/src/services/clean_release/stages/base.py index ca3f4bf2..2cc97ad3 100644 --- a/backend/src/services/clean_release/stages/base.py +++ b/backend/src/services/clean_release/stages/base.py @@ -1,11 +1,9 @@ -# [DEF:ComplianceStageBase:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance, stages, contracts, base -# @PURPOSE: Define shared contracts and helpers for pluggable clean-release compliance stages. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels] -# @RELATION: [DEPENDS_ON] ->[LoggerModule] -# @INVARIANT: Stage execution is deterministic for equal input context. +# #region ComplianceStageBase [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, stages, contracts, base] +# @BRIEF Define shared contracts and helpers for pluggable clean-release compliance stages. +# @LAYER Domain +# @INVARIANT Stage execution is deterministic for equal input context. +# @RELATION DEPENDS_ON -> [CleanReleaseModels] +# @RELATION DEPENDS_ON -> [LoggerModule] from __future__ import annotations @@ -28,8 +26,8 @@ from ....models.clean_release import ( from ..enums import ComplianceStageName, ViolationSeverity -# [DEF:ComplianceStageContext:Class] -# @PURPOSE: Immutable input envelope passed to each compliance stage. +# #region ComplianceStageContext [TYPE Class] +# @BRIEF Immutable input envelope passed to each compliance stage. @dataclass(frozen=True) class ComplianceStageContext: run: ComplianceRun @@ -39,11 +37,11 @@ class ComplianceStageContext: registry: SourceRegistrySnapshot -# [/DEF:ComplianceStageContext:Class] +# #endregion ComplianceStageContext -# [DEF:StageExecutionResult:Class] -# @PURPOSE: Structured stage output containing decision, details and violations. +# #region StageExecutionResult [TYPE Class] +# @BRIEF Structured stage output containing decision, details and violations. @dataclass class StageExecutionResult: decision: ComplianceDecision @@ -51,24 +49,24 @@ class StageExecutionResult: violations: List[ComplianceViolation] = field(default_factory=list) -# [/DEF:StageExecutionResult:Class] +# #endregion StageExecutionResult -# [DEF:ComplianceStage:Class] -# @PURPOSE: Protocol for pluggable stage implementations. +# #region ComplianceStage [TYPE Class] +# @BRIEF Protocol for pluggable stage implementations. class ComplianceStage(Protocol): stage_name: ComplianceStageName def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ... -# [/DEF:ComplianceStage:Class] +# #endregion ComplianceStage -# [DEF:build_stage_run_record:Function] -# @PURPOSE: Build persisted stage run record from stage result. -# @PRE: run_id and stage_name are non-empty. -# @POST: Returns ComplianceStageRun with deterministic identifiers and timestamps. +# #region build_stage_run_record [TYPE Function] +# @BRIEF Build persisted stage run record from stage result. +# @PRE run_id and stage_name are non-empty. +# @POST Returns ComplianceStageRun with deterministic identifiers and timestamps. def build_stage_run_record( *, run_id: str, @@ -93,13 +91,13 @@ def build_stage_run_record( ) -# [/DEF:build_stage_run_record:Function] +# #endregion build_stage_run_record -# [DEF:build_violation:Function] -# @PURPOSE: Construct a compliance violation with normalized defaults. -# @PRE: run_id, stage_name, code and message are non-empty. -# @POST: Returns immutable-style violation payload ready for persistence. +# #region build_violation [TYPE Function] +# @BRIEF Construct a compliance violation with normalized defaults. +# @PRE run_id, stage_name, code and message are non-empty. +# @POST Returns immutable-style violation payload ready for persistence. def build_violation( *, run_id: str, @@ -129,6 +127,6 @@ def build_violation( ) -# [/DEF:build_violation:Function] +# #endregion build_violation -# [/DEF:ComplianceStageBase:Module] +# #endregion ComplianceStageBase diff --git a/backend/src/services/clean_release/stages/data_purity.py b/backend/src/services/clean_release/stages/data_purity.py index c40d6d88..4939de7f 100644 --- a/backend/src/services/clean_release/stages/data_purity.py +++ b/backend/src/services/clean_release/stages/data_purity.py @@ -1,11 +1,9 @@ -# [DEF:data_purity:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance-stage, data-purity -# @PURPOSE: Evaluate manifest purity counters and emit blocking violations for prohibited artifacts. -# @LAYER: Domain -# @RELATION: [IMPLEMENTS] ->[ComplianceStage] -# @RELATION: [DEPENDS_ON] ->[ComplianceStageBase] -# @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision. +# #region data_purity [C:3] [TYPE Module] [SEMANTICS clean-release, compliance-stage, data-purity] +# @BRIEF Evaluate manifest purity counters and emit blocking violations for prohibited artifacts. +# @LAYER Domain +# @INVARIANT prohibited_detected_count > 0 always yields BLOCKED stage decision. +# @RELATION IMPLEMENTS -> [ComplianceStage] +# @RELATION DEPENDS_ON -> [ComplianceStageBase] from __future__ import annotations @@ -14,10 +12,10 @@ from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity from .base import ComplianceStageContext, StageExecutionResult, build_violation -# [DEF:DataPurityStage:Class] -# @PURPOSE: Validate manifest summary for prohibited artifacts. -# @PRE: context.manifest.content_json contains summary block or defaults to safe counters. -# @POST: Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations. +# #region DataPurityStage [TYPE Class] +# @BRIEF Validate manifest summary for prohibited artifacts. +# @PRE context.manifest.content_json contains summary block or defaults to safe counters. +# @POST Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations. class DataPurityStage: stage_name = ComplianceStageName.DATA_PURITY @@ -63,6 +61,6 @@ class DataPurityStage: ) -# [/DEF:DataPurityStage:Class] +# #endregion DataPurityStage -# [/DEF:data_purity:Module] +# #endregion data_purity diff --git a/backend/src/services/clean_release/stages/internal_sources_only.py b/backend/src/services/clean_release/stages/internal_sources_only.py index 04474316..2bcf8ed6 100644 --- a/backend/src/services/clean_release/stages/internal_sources_only.py +++ b/backend/src/services/clean_release/stages/internal_sources_only.py @@ -1,11 +1,9 @@ -# [DEF:internal_sources_only:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance-stage, source-isolation, registry -# @PURPOSE: Verify manifest-declared sources belong to trusted internal registry allowlist. -# @LAYER: Domain -# @RELATION: [IMPLEMENTS] ->[ComplianceStage] -# @RELATION: [DEPENDS_ON] ->[ComplianceStageBase] -# @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation. +# #region internal_sources_only [C:3] [TYPE Module] [SEMANTICS clean-release, compliance-stage, source-isolation, registry] +# @BRIEF Verify manifest-declared sources belong to trusted internal registry allowlist. +# @LAYER Domain +# @INVARIANT Any source host outside allowed_hosts yields BLOCKED decision with at least one violation. +# @RELATION IMPLEMENTS -> [ComplianceStage] +# @RELATION DEPENDS_ON -> [ComplianceStageBase] from __future__ import annotations @@ -14,10 +12,10 @@ from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity from .base import ComplianceStageContext, StageExecutionResult, build_violation -# [DEF:InternalSourcesOnlyStage:Class] -# @PURPOSE: Enforce internal-source-only policy from trusted registry snapshot. -# @PRE: context.registry.allowed_hosts is available. -# @POST: Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured. +# #region InternalSourcesOnlyStage [TYPE Class] +# @BRIEF Enforce internal-source-only policy from trusted registry snapshot. +# @PRE context.registry.allowed_hosts is available. +# @POST Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured. class InternalSourcesOnlyStage: stage_name = ComplianceStageName.INTERNAL_SOURCES_ONLY @@ -82,6 +80,6 @@ class InternalSourcesOnlyStage: ) -# [/DEF:InternalSourcesOnlyStage:Class] +# #endregion InternalSourcesOnlyStage -# [/DEF:internal_sources_only:Module] +# #endregion internal_sources_only diff --git a/backend/src/services/clean_release/stages/manifest_consistency.py b/backend/src/services/clean_release/stages/manifest_consistency.py index a11f805c..7b865731 100644 --- a/backend/src/services/clean_release/stages/manifest_consistency.py +++ b/backend/src/services/clean_release/stages/manifest_consistency.py @@ -1,11 +1,9 @@ -# [DEF:manifest_consistency:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance-stage, manifest, consistency, digest -# @PURPOSE: Ensure run is bound to the exact manifest snapshot and digest used at run creation time. -# @LAYER: Domain -# @RELATION: [IMPLEMENTS] ->[ComplianceStage] -# @RELATION: [DEPENDS_ON] ->[ComplianceStageBase] -# @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence. +# #region manifest_consistency [C:3] [TYPE Module] [SEMANTICS clean-release, compliance-stage, manifest, consistency, digest] +# @BRIEF Ensure run is bound to the exact manifest snapshot and digest used at run creation time. +# @LAYER Domain +# @INVARIANT Digest mismatch between run and manifest yields ERROR with blocking violation evidence. +# @RELATION IMPLEMENTS -> [ComplianceStage] +# @RELATION DEPENDS_ON -> [ComplianceStageBase] from __future__ import annotations @@ -14,10 +12,10 @@ from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity from .base import ComplianceStageContext, StageExecutionResult, build_violation -# [DEF:ManifestConsistencyStage:Class] -# @PURPOSE: Validate run/manifest linkage consistency. -# @PRE: context.run and context.manifest are loaded from repository for same run. -# @POST: Returns PASSED when digests match, otherwise ERROR with one violation. +# #region ManifestConsistencyStage [TYPE Class] +# @BRIEF Validate run/manifest linkage consistency. +# @PRE context.run and context.manifest are loaded from repository for same run. +# @POST Returns PASSED when digests match, otherwise ERROR with one violation. class ManifestConsistencyStage: stage_name = ComplianceStageName.MANIFEST_CONSISTENCY @@ -67,6 +65,6 @@ class ManifestConsistencyStage: ) -# [/DEF:ManifestConsistencyStage:Class] +# #endregion ManifestConsistencyStage -# [/DEF:manifest_consistency:Module] +# #endregion manifest_consistency diff --git a/backend/src/services/clean_release/stages/no_external_endpoints.py b/backend/src/services/clean_release/stages/no_external_endpoints.py index d1cfe88c..5e308741 100644 --- a/backend/src/services/clean_release/stages/no_external_endpoints.py +++ b/backend/src/services/clean_release/stages/no_external_endpoints.py @@ -1,11 +1,9 @@ -# [DEF:no_external_endpoints:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: clean-release, compliance-stage, endpoints, network -# @PURPOSE: Block manifest payloads that expose external endpoints outside trusted schemes and hosts. -# @LAYER: Domain -# @RELATION: [IMPLEMENTS] ->[ComplianceStage] -# @RELATION: [DEPENDS_ON] ->[ComplianceStageBase] -# @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision. +# #region no_external_endpoints [C:3] [TYPE Module] [SEMANTICS clean-release, compliance-stage, endpoints, network] +# @BRIEF Block manifest payloads that expose external endpoints outside trusted schemes and hosts. +# @LAYER Domain +# @INVARIANT Endpoint outside allowed scheme/host always yields BLOCKED stage decision. +# @RELATION IMPLEMENTS -> [ComplianceStage] +# @RELATION DEPENDS_ON -> [ComplianceStageBase] from __future__ import annotations @@ -16,10 +14,10 @@ from ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity from .base import ComplianceStageContext, StageExecutionResult, build_violation -# [DEF:NoExternalEndpointsStage:Class] -# @PURPOSE: Validate endpoint references from manifest against trusted registry. -# @PRE: context.registry includes allowed hosts and schemes. -# @POST: Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations. +# #region NoExternalEndpointsStage [TYPE Class] +# @BRIEF Validate endpoint references from manifest against trusted registry. +# @PRE context.registry includes allowed hosts and schemes. +# @POST Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations. class NoExternalEndpointsStage: stage_name = ComplianceStageName.NO_EXTERNAL_ENDPOINTS @@ -88,6 +86,6 @@ class NoExternalEndpointsStage: ) -# [/DEF:NoExternalEndpointsStage:Class] +# #endregion NoExternalEndpointsStage -# [/DEF:no_external_endpoints:Module] +# #endregion no_external_endpoints diff --git a/backend/src/services/dataset_review/__init__.py b/backend/src/services/dataset_review/__init__.py index 3228395c..65215a09 100644 --- a/backend/src/services/dataset_review/__init__.py +++ b/backend/src/services/dataset_review/__init__.py @@ -1,8 +1,7 @@ -# [DEF:dataset_review:Module] +# #region dataset_review [TYPE Module] [SEMANTICS dataset, review, orchestration] +# @BRIEF Provides services for dataset-centered orchestration flow. +# @LAYER Services +# @RELATION EXPORTS -> [DatasetReviewOrchestrator:Class] # -# @SEMANTICS: dataset, review, orchestration -# @PURPOSE: Provides services for dataset-centered orchestration flow. -# @RELATION: EXPORTS ->[DatasetReviewOrchestrator:Class] -# @LAYER: Services # -# [/DEF:dataset_review:Module] \ No newline at end of file +# #endregion dataset_review diff --git a/backend/src/services/dataset_review/clarification_engine.py b/backend/src/services/dataset_review/clarification_engine.py index 44a62cec..842848d4 100644 --- a/backend/src/services/dataset_review/clarification_engine.py +++ b/backend/src/services/dataset_review/clarification_engine.py @@ -1,21 +1,19 @@ -# [DEF:ClarificationEngine:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: dataset_review, clarification, question_payload, answer_persistence, readiness, findings -# @PURPOSE: Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository] -# @RELATION: DEPENDS_ON -> [ClarificationSession] -# @RELATION: DEPENDS_ON -> [ClarificationQuestion] -# @RELATION: DEPENDS_ON -> [ClarificationAnswer] -# @RELATION: DEPENDS_ON -> [ValidationFinding] -# @RELATION: DISPATCHES -> [ClarificationHelpers:Module] -# @PRE: Target session contains a persisted clarification aggregate in the current ownership scope. -# @POST: Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation. -# @SIDE_EFFECT: Persists clarification answers, question/session states, and related readiness/finding changes. -# @DATA_CONTRACT: Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult] -# @INVARIANT: Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible. -# @RATIONALE: Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module. -# @REJECTED: Keeping all clarification logic in one file because it exceeded the fractal limit. +# #region ClarificationEngine [C:4] [TYPE Module] [SEMANTICS dataset_review, clarification, question_payload, answer_persistence, readiness, findings] +# @BRIEF Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates. +# @LAYER Domain +# @PRE Target session contains a persisted clarification aggregate in the current ownership scope. +# @POST Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation. +# @SIDE_EFFECT Persists clarification answers, question/session states, and related readiness/finding changes. +# @DATA_CONTRACT Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult] +# @INVARIANT Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible. +# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository] +# @RELATION DEPENDS_ON -> [ClarificationSession] +# @RELATION DEPENDS_ON -> [ClarificationQuestion] +# @RELATION DEPENDS_ON -> [ClarificationAnswer] +# @RELATION DEPENDS_ON -> [ValidationFinding] +# @RELATION DISPATCHES -> [ClarificationHelpers:Module] +# @RATIONALE Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module. +# @REJECTED Keeping all clarification logic in one file because it exceeded the fractal limit. from __future__ import annotations @@ -56,9 +54,8 @@ from src.services.dataset_review.clarification_pkg._helpers import ( log = MarkerLogger("ClarificationEngine") -# [DEF:ClarificationQuestionPayload:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed active-question payload returned to the API layer. +# #region ClarificationQuestionPayload [C:2] [TYPE Class] +# @BRIEF Typed active-question payload returned to the API layer. @dataclass class ClarificationQuestionPayload: question_id: str @@ -72,12 +69,11 @@ class ClarificationQuestionPayload: options: list[dict[str, object]] = field(default_factory=list) -# [/DEF:ClarificationQuestionPayload:Class] +# #endregion ClarificationQuestionPayload -# [DEF:ClarificationStateResult:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Clarification state result carrying the current session, active payload, and changed findings. +# #region ClarificationStateResult [C:2] [TYPE Class] +# @BRIEF Clarification state result carrying the current session, active payload, and changed findings. @dataclass class ClarificationStateResult: clarification_session: ClarificationSession @@ -86,12 +82,11 @@ class ClarificationStateResult: changed_findings: List[ValidationFinding] = field(default_factory=list) -# [/DEF:ClarificationStateResult:Class] +# #endregion ClarificationStateResult -# [DEF:ClarificationAnswerCommand:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed answer command for clarification state mutation. +# #region ClarificationAnswerCommand [C:2] [TYPE Class] +# @BRIEF Typed answer command for clarification state mutation. @dataclass class ClarificationAnswerCommand: session: DatasetReviewSession @@ -101,32 +96,29 @@ class ClarificationAnswerCommand: user: User -# [/DEF:ClarificationAnswerCommand:Class] +# #endregion ClarificationAnswerCommand -# [DEF:ClarificationEngine:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Provide deterministic one-question-at-a-time clarification selection and answer persistence. -# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository] -# @RELATION: CALLS -> [ClarificationHelpers:Module] -# @PRE: Repository is bound to the current request transaction scope. -# @POST: Returned clarification state is persistence-backed and aligned with session readiness/recommended action. -# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings. +# #region ClarificationEngine [C:4] [TYPE Class] +# @BRIEF Provide deterministic one-question-at-a-time clarification selection and answer persistence. +# @PRE Repository is bound to the current request transaction scope. +# @POST Returned clarification state is persistence-backed and aligned with session readiness/recommended action. +# @SIDE_EFFECT Mutates clarification answers, session flags, and related clarification findings. +# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository] +# @RELATION CALLS -> [ClarificationHelpers:Module] class ClarificationEngine: - # [DEF:ClarificationEngine_init:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Bind repository dependency for clarification persistence operations. + # #region ClarificationEngine_init [C:2] [TYPE Function] + # @BRIEF Bind repository dependency for clarification persistence operations. def __init__(self, repository: DatasetReviewSessionRepository) -> None: self.repository = repository - # [/DEF:ClarificationEngine_init:Function] + # #endregion ClarificationEngine_init - # [DEF:build_question_payload:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Return the one active highest-priority clarification question payload. - # @PRE: Session contains unresolved clarification state or a resumable clarification session. - # @POST: Returns exactly one active/open question payload or None when no unresolved question remains. - # @SIDE_EFFECT: Normalizes the active-question pointer and clarification status in persistence. + # #region build_question_payload [C:4] [TYPE Function] + # @BRIEF Return the one active highest-priority clarification question payload. + # @PRE Session contains unresolved clarification state or a resumable clarification session. + # @POST Returns exactly one active/open question payload or None when no unresolved question remains. + # @SIDE_EFFECT Normalizes the active-question pointer and clarification status in persistence. def build_question_payload( self, session: DatasetReviewSession, ) -> Optional[ClarificationQuestionPayload]: @@ -179,14 +171,13 @@ class ClarificationEngine: log.reflect("Clarification payload built", payload={"session_id": session.session_id, "question_id": payload.question_id, "option_count": len(payload.options)}) return payload - # [/DEF:build_question_payload:Function] + # #endregion build_question_payload - # [DEF:record_answer:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation. - # @PRE: Target question belongs to the session's active clarification session and is still open. - # @POST: Answer row is persisted before current-question pointer advances. - # @SIDE_EFFECT: Inserts answer row, mutates question/session states, updates clarification findings, and commits. + # #region record_answer [C:4] [TYPE Function] + # @BRIEF Persist one clarification answer before any pointer/readiness mutation. + # @PRE Target question belongs to the session's active clarification session and is still open. + # @POST Answer row is persisted before current-question pointer advances. + # @SIDE_EFFECT Inserts answer row, mutates question/session states, updates clarification findings, and commits. def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult: with belief_scope("ClarificationEngine.record_answer"): session = command.session @@ -266,41 +257,36 @@ class ClarificationEngine: changed_findings=[changed_finding] if changed_finding else [], ) - # [/DEF:record_answer:Function] + # #endregion record_answer - # [DEF:summarize_progress:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX. + # #region summarize_progress [C:1] [TYPE Function] + # @BRIEF Produce a compact progress summary for pause/resume and completion UX. def summarize_progress(self, clarification_session: ClarificationSession) -> str: resolved = count_resolved_questions(clarification_session) remaining = count_remaining_questions(clarification_session) return f"{resolved} resolved, {remaining} unresolved" - # [/DEF:summarize_progress:Function] + # #endregion summarize_progress - # [DEF:_get_latest_clarification_session:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Select the latest clarification session for the current dataset review aggregate. + # #region _get_latest_clarification_session [C:2] [TYPE Function] + # @BRIEF Select the latest clarification session for the current dataset review aggregate. def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]: if not session.clarification_sessions: return None ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True) return ordered[0] - # [/DEF:_get_latest_clarification_session:Function] + # #endregion _get_latest_clarification_session - # [DEF:_find_question:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Resolve a clarification question from the active clarification aggregate. + # #region _find_question [C:1] [TYPE Function] + # @BRIEF Resolve a clarification question from the active clarification aggregate. def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]: for q in clarification_session.questions: if q.question_id == question_id: return q return None - # [/DEF:_find_question:Function] + # #endregion _find_question -# [/DEF:ClarificationEngine:Class] - -# [/DEF:ClarificationEngine:Module] +# #endregion ClarificationEngine diff --git a/backend/src/services/dataset_review/clarification_pkg/_helpers.py b/backend/src/services/dataset_review/clarification_pkg/_helpers.py index 829baee8..466baa33 100644 --- a/backend/src/services/dataset_review/clarification_pkg/_helpers.py +++ b/backend/src/services/dataset_review/clarification_pkg/_helpers.py @@ -1,8 +1,7 @@ -# [DEF:ClarificationHelpers:Module] -# @COMPLEXITY: 3 -# @PURPOSE: Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewModels] +# #region ClarificationHelpers [C:3] [TYPE Module] +# @BRIEF Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewModels] from __future__ import annotations @@ -26,9 +25,8 @@ from src.models.dataset_review import ( ) -# [DEF:select_next_open_question:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Select the next unresolved question in deterministic priority order. +# #region select_next_open_question [C:2] [TYPE Function] +# @BRIEF Select the next unresolved question in deterministic priority order. def select_next_open_question( clarification_session: ClarificationSession, ) -> Optional[ClarificationQuestion]: @@ -41,22 +39,20 @@ def select_next_open_question( return open_questions[0] -# [/DEF:select_next_open_question:Function] +# #endregion select_next_open_question -# [DEF:count_resolved_questions:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Count questions whose answers fully resolved the ambiguity. +# #region count_resolved_questions [C:1] [TYPE Function] +# @BRIEF Count questions whose answers fully resolved the ambiguity. def count_resolved_questions(clarification_session: ClarificationSession) -> int: return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED) -# [/DEF:count_resolved_questions:Function] +# #endregion count_resolved_questions -# [DEF:count_remaining_questions:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Count questions still unresolved or deferred after clarification interaction. +# #region count_remaining_questions [C:1] [TYPE Function] +# @BRIEF Count questions still unresolved or deferred after clarification interaction. def count_remaining_questions(clarification_session: ClarificationSession) -> int: return sum( 1 @@ -65,12 +61,11 @@ def count_remaining_questions(clarification_session: ClarificationSession) -> in ) -# [/DEF:count_remaining_questions:Function] +# #endregion count_remaining_questions -# [DEF:normalize_answer_value:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Validate and normalize answer payload based on answer kind and active question options. +# #region normalize_answer_value [C:2] [TYPE Function] +# @BRIEF Validate and normalize answer payload based on answer kind and active question options. def normalize_answer_value( answer_kind: AnswerKind, answer_value: Optional[str], @@ -90,12 +85,11 @@ def normalize_answer_value( return normalized -# [/DEF:normalize_answer_value:Function] +# #endregion normalize_answer_value -# [DEF:build_impact_summary:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Build a compact audit note describing how the clarification answer affects session state. +# #region build_impact_summary [C:1] [TYPE Function] +# @BRIEF Build a compact audit note describing how the clarification answer affects session state. def build_impact_summary( question: ClarificationQuestion, answer_kind: AnswerKind, @@ -108,13 +102,12 @@ def build_impact_summary( return f"Clarification for {question.topic_ref} recorded as '{answer_value}'." -# [/DEF:build_impact_summary:Function] +# #endregion build_impact_summary -# [DEF:upsert_clarification_finding:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules. -# @RELATION: DEPENDS_ON -> [ValidationFinding] +# #region upsert_clarification_finding [C:3] [TYPE Function] +# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules. +# @RELATION DEPENDS_ON -> [ValidationFinding] def upsert_clarification_finding( session: DatasetReviewSession, question: ClarificationQuestion, @@ -176,12 +169,11 @@ def upsert_clarification_finding( return existing -# [/DEF:upsert_clarification_finding:Function] +# #endregion upsert_clarification_finding -# [DEF:derive_readiness_state:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Recompute readiness after clarification mutation while preserving unresolved visibility semantics. +# #region derive_readiness_state [C:2] [TYPE Function] +# @BRIEF Recompute readiness after clarification mutation while preserving unresolved visibility semantics. def derive_readiness_state( session: DatasetReviewSession, clarification_session: Optional[ClarificationSession], @@ -195,12 +187,11 @@ def derive_readiness_state( return ReadinessState.REVIEW_READY -# [/DEF:derive_readiness_state:Function] +# #endregion derive_readiness_state -# [DEF:derive_recommended_action:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Recompute next-action guidance after clarification mutations. +# #region derive_recommended_action [C:2] [TYPE Function] +# @BRIEF Recompute next-action guidance after clarification mutations. def derive_recommended_action( session: DatasetReviewSession, clarification_session: Optional[ClarificationSession], @@ -214,7 +205,7 @@ def derive_recommended_action( return RecommendedAction.REVIEW_DOCUMENTATION -# [/DEF:derive_recommended_action:Function] +# #endregion derive_recommended_action -# [/DEF:ClarificationHelpers:Module] +# #endregion ClarificationHelpers diff --git a/backend/src/services/dataset_review/event_logger.py b/backend/src/services/dataset_review/event_logger.py index 3af54c83..c16ae9fd 100644 --- a/backend/src/services/dataset_review/event_logger.py +++ b/backend/src/services/dataset_review/event_logger.py @@ -1,18 +1,16 @@ -# [DEF:SessionEventLoggerModule:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: dataset_review, audit, session_events, persistence, observability -# @PURPOSE: Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[SessionEvent] -# @RELATION: [DEPENDS_ON] ->[DatasetReviewSession] -# @PRE: Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event. -# @POST: Every logged event is committed as an explicit, queryable audit record with deterministic event metadata. -# @SIDE_EFFECT: Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations. -# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent] +# #region SessionEventLoggerModule [C:4] [TYPE Module] [SEMANTICS dataset_review, audit, session_events, persistence, observability] +# @BRIEF Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants. +# @LAYER Domain +# @PRE Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event. +# @POST Every logged event is committed as an explicit, queryable audit record with deterministic event metadata. +# @SIDE_EFFECT Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations. +# @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent] +# @RELATION DEPENDS_ON -> [SessionEvent] +# @RELATION DEPENDS_ON -> [DatasetReviewSession] from __future__ import annotations -# [DEF:SessionEventLoggerImports:Block] +# #region SessionEventLoggerImports [TYPE Block] from dataclasses import dataclass, field from typing import Any, Dict, Optional @@ -21,14 +19,13 @@ from sqlalchemy.orm import Session from src.core.cot_logger import MarkerLogger from src.core.logger import belief_scope from src.models.dataset_review import DatasetReviewSession, SessionEvent -# [/DEF:SessionEventLoggerImports:Block] +# #endregion SessionEventLoggerImports log = MarkerLogger("SessionEventLogger") -# [DEF:SessionEventPayload:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed input contract for one persisted dataset-review session audit event. +# #region SessionEventPayload [C:2] [TYPE Class] +# @BRIEF Typed input contract for one persisted dataset-review session audit event. @dataclass(frozen=True) class SessionEventPayload: session_id: str @@ -38,34 +35,31 @@ class SessionEventPayload: current_phase: Optional[str] = None readiness_state: Optional[str] = None event_details: Dict[str, Any] = field(default_factory=dict) -# [/DEF:SessionEventPayload:Class] +# #endregion SessionEventPayload -# [DEF:SessionEventLogger:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Persist explicit dataset-review session audit events with meaningful runtime reasoning logs. -# @RELATION: [DEPENDS_ON] ->[SessionEvent] -# @RELATION: [DEPENDS_ON] ->[SessionEventPayload] -# @PRE: The database session is live and payload identifiers are non-empty. -# @POST: Returns the committed session event row with a stable identifier and stored detail payload. -# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces. -# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent] +# #region SessionEventLogger [C:4] [TYPE Class] +# @BRIEF Persist explicit dataset-review session audit events with meaningful runtime reasoning logs. +# @PRE The database session is live and payload identifiers are non-empty. +# @POST Returns the committed session event row with a stable identifier and stored detail payload. +# @SIDE_EFFECT Writes one audit row to persistence and emits logger.reason/logger.reflect traces. +# @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent] +# @RELATION DEPENDS_ON -> [SessionEvent] +# @RELATION DEPENDS_ON -> [SessionEventPayload] class SessionEventLogger: - # [DEF:SessionEventLogger_init:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger. + # #region SessionEventLogger_init [C:2] [TYPE Function] + # @BRIEF Bind a live SQLAlchemy session to the session-event logger. def __init__(self, db: Session) -> None: self.db = db - # [/DEF:SessionEventLogger_init:Function] + # #endregion SessionEventLogger_init - # [DEF:log_event:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation. - # @RELATION: [DEPENDS_ON] ->[SessionEvent] - # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty. - # @POST: Returns the committed SessionEvent record with normalized detail payload. - # @SIDE_EFFECT: Inserts and commits one session_events row. - # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent] + # #region log_event [C:4] [TYPE Function] + # @BRIEF Persist one explicit session event row for an owned dataset-review mutation. + # @PRE session_id, actor_user_id, event_type, and event_summary are non-empty. + # @POST Returns the committed SessionEvent record with normalized detail payload. + # @SIDE_EFFECT Inserts and commits one session_events row. + # @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent] + # @RELATION DEPENDS_ON -> [SessionEvent] def log_event(self, payload: SessionEventPayload) -> SessionEvent: with belief_scope("SessionEventLogger.log_event"): session_id = str(payload.session_id or "").strip() @@ -120,12 +114,11 @@ class SessionEventLogger: }, ) return event - # [/DEF:log_event:Function] + # #endregion log_event - # [DEF:log_for_session:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root. - # @RELATION: [CALLS] ->[SessionEventLogger.log_event] + # #region log_for_session [C:2] [TYPE Function] + # @BRIEF Convenience wrapper for logging an event directly from a session aggregate root. + # @RELATION CALLS -> [SessionEventLogger.log_event] def log_for_session( self, session: DatasetReviewSession, @@ -146,7 +139,5 @@ class SessionEventLogger: event_details=dict(event_details or {}), ) ) - # [/DEF:log_for_session:Function] -# [/DEF:SessionEventLogger:Class] - -# [/DEF:SessionEventLoggerModule:Module] \ No newline at end of file + # #endregion log_for_session +# #endregion SessionEventLogger diff --git a/backend/src/services/dataset_review/orchestrator.py b/backend/src/services/dataset_review/orchestrator.py index 19cdd0eb..4a7c3c49 100644 --- a/backend/src/services/dataset_review/orchestrator.py +++ b/backend/src/services/dataset_review/orchestrator.py @@ -1,22 +1,20 @@ -# [DEF:DatasetReviewOrchestrator:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: dataset_review, orchestration, session_lifecycle, intake, recovery -# @PURPOSE: Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository] -# @RELATION: DEPENDS_ON -> [SemanticSourceResolver] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractor] -# @RELATION: DEPENDS_ON -> [SupersetCompilationAdapter] -# @RELATION: DEPENDS_ON -> [TaskManager] -# @RELATION: DISPATCHES -> [OrchestratorHelpers:Module] -# @RELATION: DISPATCHES -> [OrchestratorCommands:Module] -# @PRE: session mutations must execute inside a persisted session boundary scoped to one authenticated user. -# @POST: state transitions are persisted atomically and emit observable progress for long-running steps. -# @SIDE_EFFECT: creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts. -# @DATA_CONTRACT: Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext] -# @INVARIANT: Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint. -# @RATIONALE: Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point. -# @REJECTED: Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x. +# #region DatasetReviewOrchestrator [C:5] [TYPE Module] [SEMANTICS dataset_review, orchestration, session_lifecycle, intake, recovery] +# @BRIEF Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user. +# @LAYER Domain +# @PRE session mutations must execute inside a persisted session boundary scoped to one authenticated user. +# @POST state transitions are persisted atomically and emit observable progress for long-running steps. +# @SIDE_EFFECT creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts. +# @DATA_CONTRACT Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext] +# @INVARIANT Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint. +# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository] +# @RELATION DEPENDS_ON -> [SemanticSourceResolver] +# @RELATION DEPENDS_ON -> [SupersetContextExtractor] +# @RELATION DEPENDS_ON -> [SupersetCompilationAdapter] +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DISPATCHES -> [OrchestratorHelpers:Module] +# @RELATION DISPATCHES -> [OrchestratorCommands:Module] +# @RATIONALE Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point. +# @REJECTED Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x. from __future__ import annotations @@ -93,26 +91,24 @@ from src.services.dataset_review.orchestrator_pkg._helpers import ( log = MarkerLogger("Orchestrator") -# [DEF:DatasetReviewOrchestrator:Class] -# @COMPLEXITY: 5 -# @PURPOSE: Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery. -# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractor] -# @RELATION: DEPENDS_ON -> [TaskManager] -# @RELATION: DEPENDS_ON -> [ConfigManager] -# @RELATION: DEPENDS_ON -> [SemanticSourceResolver] -# @RELATION: CALLS -> [OrchestratorHelpers:Module] -# @PRE: constructor dependencies are valid and tied to the current request/task scope. -# @POST: orchestrator instance can execute session-scoped mutations for one authenticated user. -# @SIDE_EFFECT: downstream operations may persist session/profile/finding state and enqueue background tasks. -# @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult] -# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial. +# #region DatasetReviewOrchestrator [C:5] [TYPE Class] +# @BRIEF Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery. +# @PRE constructor dependencies are valid and tied to the current request/task scope. +# @POST orchestrator instance can execute session-scoped mutations for one authenticated user. +# @SIDE_EFFECT downstream operations may persist session/profile/finding state and enqueue background tasks. +# @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult] +# @INVARIANT session ownership is preserved on every mutation and recovery remains explicit when partial. +# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository] +# @RELATION DEPENDS_ON -> [SupersetContextExtractor] +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [ConfigManager] +# @RELATION DEPENDS_ON -> [SemanticSourceResolver] +# @RELATION CALLS -> [OrchestratorHelpers:Module] class DatasetReviewOrchestrator: - # [DEF:DatasetReviewOrchestrator_init:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary. - # @PRE: repository/config_manager are valid collaborators for the current request scope. - # @POST: Instance holds collaborator references used by start/preview/launch orchestration methods. + # #region DatasetReviewOrchestrator_init [C:3] [TYPE Function] + # @BRIEF Bind repository, config, and task dependencies required by the orchestration boundary. + # @PRE repository/config_manager are valid collaborators for the current request scope. + # @POST Instance holds collaborator references used by start/preview/launch orchestration methods. def __init__( self, repository: DatasetReviewSessionRepository, @@ -125,18 +121,17 @@ class DatasetReviewOrchestrator: self.task_manager = task_manager self.semantic_resolver = semantic_resolver or SemanticSourceResolver() - # [/DEF:DatasetReviewOrchestrator_init:Function] + # #endregion DatasetReviewOrchestrator_init - # [DEF:start_session:Function] - # @COMPLEXITY: 5 - # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery. - # @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link] - # @RELATION: CALLS -> [TaskManager.create_task] - # @PRE: source input is non-empty and environment is accessible. - # @POST: session exists in persisted storage with intake/recovery state and task linkage when async work is required. - # @SIDE_EFFECT: persists session and may enqueue recovery task. - # @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult] - # @INVARIANT: no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user. + # #region start_session [C:5] [TYPE Function] + # @BRIEF Initialize a new session from a Superset link or dataset selection and trigger context recovery. + # @PRE source input is non-empty and environment is accessible. + # @POST session exists in persisted storage with intake/recovery state and task linkage when async work is required. + # @SIDE_EFFECT persists session and may enqueue recovery task. + # @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult] + # @INVARIANT no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user. + # @RELATION CALLS -> [SupersetContextExtractor.parse_superset_link] + # @RELATION CALLS -> [TaskManager.create_task] def start_session(self, command: StartSessionCommand) -> StartSessionResult: with belief_scope("DatasetReviewOrchestrator.start_session"): normalized_source_kind = str(command.source_kind or "").strip() @@ -280,16 +275,15 @@ class DatasetReviewOrchestrator: findings=findings, ) - # [/DEF:start_session:Function] + # #endregion start_session - # [DEF:prepare_launch_preview:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation. - # @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview] - # @PRE: all required variables have candidate values or explicitly accepted defaults. - # @POST: returns preview artifact in pending, ready, failed, or stale state. - # @SIDE_EFFECT: persists preview attempt and upstream compilation diagnostics. - # @DATA_CONTRACT: Input[PreparePreviewCommand] -> Output[PreparePreviewResult] + # #region prepare_launch_preview [C:4] [TYPE Function] + # @BRIEF Assemble effective execution inputs and trigger Superset-side preview compilation. + # @PRE all required variables have candidate values or explicitly accepted defaults. + # @POST returns preview artifact in pending, ready, failed, or stale state. + # @SIDE_EFFECT persists preview attempt and upstream compilation diagnostics. + # @DATA_CONTRACT Input[PreparePreviewCommand] -> Output[PreparePreviewResult] + # @RELATION CALLS -> [SupersetCompilationAdapter.compile_preview] def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult: with belief_scope("DatasetReviewOrchestrator.prepare_launch_preview"): session = self.repository.load_session_detail(command.session_id, command.user.id) @@ -360,17 +354,16 @@ class DatasetReviewOrchestrator: log.reflect("Superset preview preparation completed", payload={"session_id": session.session_id, "preview_id": persisted_preview.preview_id, "preview_status": persisted_preview.preview_status.value}) return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[]) - # [/DEF:prepare_launch_preview:Function] + # #endregion prepare_launch_preview - # [DEF:launch_dataset:Function] - # @COMPLEXITY: 5 - # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay. - # @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session] - # @PRE: session is run-ready and compiled preview is current. - # @POST: returns persisted run context with SQL Lab session reference and launch outcome. - # @SIDE_EFFECT: creates SQL Lab execution session and audit snapshot. - # @DATA_CONTRACT: Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult] - # @INVARIANT: launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs. + # #region launch_dataset [C:5] [TYPE Function] + # @BRIEF Start the approved dataset execution through SQL Lab and persist run context for audit/replay. + # @PRE session is run-ready and compiled preview is current. + # @POST returns persisted run context with SQL Lab session reference and launch outcome. + # @SIDE_EFFECT creates SQL Lab execution session and audit snapshot. + # @DATA_CONTRACT Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult] + # @INVARIANT launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs. + # @RELATION CALLS -> [SupersetCompilationAdapter.create_sql_lab_session] def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult: with belief_scope("DatasetReviewOrchestrator.launch_dataset"): session = self.repository.load_session_detail(command.session_id, command.user.id) @@ -460,14 +453,13 @@ class DatasetReviewOrchestrator: log.reflect("Dataset launch orchestration completed with audited run context", payload={"session_id": session.session_id, "run_context_id": persisted_run_context.run_context_id, "launch_status": persisted_run_context.launch_status.value}) return LaunchDatasetResult(session=session, run_context=persisted_run_context, blocked_reasons=[]) - # [/DEF:launch_dataset:Function] + # #endregion launch_dataset - # [DEF:_build_recovery_bootstrap:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation. - # @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope. - # @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly. - # @SIDE_EFFECT: Performs Superset reads through the extractor and may append warning findings for incomplete recovery. + # #region _build_recovery_bootstrap [C:4] [TYPE Function] + # @BRIEF Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation. + # @PRE session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope. + # @POST Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly. + # @SIDE_EFFECT Performs Superset reads through the extractor and may append warning findings for incomplete recovery. def _build_recovery_bootstrap( self, environment, @@ -560,14 +552,13 @@ class DatasetReviewOrchestrator: return imported_filters, template_variables, execution_mappings, findings - # [/DEF:_build_recovery_bootstrap:Function] + # #endregion _build_recovery_bootstrap - # [DEF:_enqueue_recovery_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Link session start to observable async recovery when task infrastructure is available. - # @PRE: session is already persisted. - # @POST: returns task identifier when a task could be enqueued, otherwise None. - # @SIDE_EFFECT: may create one background task for progressive recovery. + # #region _enqueue_recovery_task [C:3] [TYPE Function] + # @BRIEF Link session start to observable async recovery when task infrastructure is available. + # @PRE session is already persisted. + # @POST returns task identifier when a task could be enqueued, otherwise None. + # @SIDE_EFFECT may create one background task for progressive recovery. def _enqueue_recovery_task( self, command: StartSessionCommand, @@ -605,9 +596,7 @@ class DatasetReviewOrchestrator: task_id = getattr(task_object, "id", None) return str(task_id) if task_id else None - # [/DEF:_enqueue_recovery_task:Function] + # #endregion _enqueue_recovery_task -# [/DEF:DatasetReviewOrchestrator:Class] - -# [/DEF:DatasetReviewOrchestrator:Module] +# #endregion DatasetReviewOrchestrator diff --git a/backend/src/services/dataset_review/orchestrator_pkg/_commands.py b/backend/src/services/dataset_review/orchestrator_pkg/_commands.py index ecf1c085..8b0e6516 100644 --- a/backend/src/services/dataset_review/orchestrator_pkg/_commands.py +++ b/backend/src/services/dataset_review/orchestrator_pkg/_commands.py @@ -1,9 +1,8 @@ -# [DEF:OrchestratorCommands:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Typed command and result dataclasses for dataset review orchestration boundary. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewModels] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractor] +# #region OrchestratorCommands [C:2] [TYPE Module] +# @BRIEF Typed command and result dataclasses for dataset review orchestration boundary. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [DatasetReviewModels] +# @RELATION DEPENDS_ON -> [SupersetContextExtractor] from __future__ import annotations @@ -20,9 +19,8 @@ from src.models.dataset_review import ( from src.core.utils.superset_context_extractor import SupersetParsedContext -# [DEF:StartSessionCommand:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed input contract for starting a dataset review session. +# #region StartSessionCommand [C:2] [TYPE Class] +# @BRIEF Typed input contract for starting a dataset review session. @dataclass class StartSessionCommand: user: User @@ -31,12 +29,11 @@ class StartSessionCommand: source_input: str -# [/DEF:StartSessionCommand:Class] +# #endregion StartSessionCommand -# [DEF:StartSessionResult:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Session-start result carrying the persisted session and intake recovery metadata. +# #region StartSessionResult [C:2] [TYPE Class] +# @BRIEF Session-start result carrying the persisted session and intake recovery metadata. @dataclass class StartSessionResult: session: DatasetReviewSession @@ -44,12 +41,11 @@ class StartSessionResult: findings: List[ValidationFinding] = field(default_factory=list) -# [/DEF:StartSessionResult:Class] +# #endregion StartSessionResult -# [DEF:PreparePreviewCommand:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed input contract for compiling one Superset-backed session preview. +# #region PreparePreviewCommand [C:2] [TYPE Class] +# @BRIEF Typed input contract for compiling one Superset-backed session preview. @dataclass class PreparePreviewCommand: user: User @@ -57,12 +53,11 @@ class PreparePreviewCommand: expected_version: Optional[int] = None -# [/DEF:PreparePreviewCommand:Class] +# #endregion PreparePreviewCommand -# [DEF:PreparePreviewResult:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Result contract for one persisted compiled preview attempt. +# #region PreparePreviewResult [C:2] [TYPE Class] +# @BRIEF Result contract for one persisted compiled preview attempt. @dataclass class PreparePreviewResult: session: DatasetReviewSession @@ -70,12 +65,11 @@ class PreparePreviewResult: blocked_reasons: List[str] = field(default_factory=list) -# [/DEF:PreparePreviewResult:Class] +# #endregion PreparePreviewResult -# [DEF:LaunchDatasetCommand:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Typed input contract for launching one dataset-review session into SQL Lab. +# #region LaunchDatasetCommand [C:2] [TYPE Class] +# @BRIEF Typed input contract for launching one dataset-review session into SQL Lab. @dataclass class LaunchDatasetCommand: user: User @@ -83,12 +77,11 @@ class LaunchDatasetCommand: expected_version: Optional[int] = None -# [/DEF:LaunchDatasetCommand:Class] +# #endregion LaunchDatasetCommand -# [DEF:LaunchDatasetResult:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Launch result carrying immutable run context and any gate blockers. +# #region LaunchDatasetResult [C:2] [TYPE Class] +# @BRIEF Launch result carrying immutable run context and any gate blockers. @dataclass class LaunchDatasetResult: session: DatasetReviewSession @@ -96,7 +89,7 @@ class LaunchDatasetResult: blocked_reasons: List[str] = field(default_factory=list) -# [/DEF:LaunchDatasetResult:Class] +# #endregion LaunchDatasetResult -# [/DEF:OrchestratorCommands:Module] +# #endregion OrchestratorCommands diff --git a/backend/src/services/dataset_review/orchestrator_pkg/_helpers.py b/backend/src/services/dataset_review/orchestrator_pkg/_helpers.py index 56cfb69b..f53ae775 100644 --- a/backend/src/services/dataset_review/orchestrator_pkg/_helpers.py +++ b/backend/src/services/dataset_review/orchestrator_pkg/_helpers.py @@ -1,11 +1,10 @@ -# [DEF:OrchestratorHelpers:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewModels] -# @RELATION: DEPENDS_ON -> [SupersetContextExtractor] -# @PRE: Caller provides a loaded session aggregate with hydrated child collections. -# @POST: Helper results are deterministic and do not mutate persistence directly. +# #region OrchestratorHelpers [C:4] [TYPE Module] +# @BRIEF Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap. +# @LAYER Domain +# @PRE Caller provides a loaded session aggregate with hydrated child collections. +# @POST Helper results are deterministic and do not mutate persistence directly. +# @RELATION DEPENDS_ON -> [DatasetReviewModels] +# @RELATION DEPENDS_ON -> [SupersetContextExtractor] from __future__ import annotations @@ -40,9 +39,8 @@ from src.models.dataset_review import ( log = MarkerLogger("OrchestratorHelpers") -# [DEF:parse_dataset_selection:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Normalize dataset-selection payload into canonical session references. +# #region parse_dataset_selection [C:2] [TYPE Function] +# @BRIEF Normalize dataset-selection payload into canonical session references. def parse_dataset_selection(source_input: str) -> tuple[str, Optional[int]]: normalized = str(source_input or "").strip() if not normalized: @@ -58,12 +56,11 @@ def parse_dataset_selection(source_input: str) -> tuple[str, Optional[int]]: return normalized, None -# [/DEF:parse_dataset_selection:Function] +# #endregion parse_dataset_selection -# [DEF:build_initial_profile:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Create the first profile snapshot so exports and detail views remain usable immediately after intake. +# #region build_initial_profile [C:2] [TYPE Function] +# @BRIEF Create the first profile snapshot so exports and detail views remain usable immediately after intake. def build_initial_profile( session_id: str, parsed_context: Optional[Any], @@ -101,14 +98,13 @@ def build_initial_profile( ) -# [/DEF:build_initial_profile:Function] +# #endregion build_initial_profile -# [DEF:build_partial_recovery_findings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Project partial Superset intake recovery into explicit findings without blocking session usability. -# @PRE: parsed_context.partial_recovery is true. -# @POST: Returns warning-level findings that preserve usable but incomplete state. +# #region build_partial_recovery_findings [C:3] [TYPE Function] +# @BRIEF Project partial Superset intake recovery into explicit findings without blocking session usability. +# @PRE parsed_context.partial_recovery is true. +# @POST Returns warning-level findings that preserve usable but incomplete state. def build_partial_recovery_findings(parsed_context: Any) -> List[ValidationFinding]: findings: List[ValidationFinding] = [] for unresolved_ref in getattr(parsed_context, "unresolved_references", []): @@ -129,12 +125,11 @@ def build_partial_recovery_findings(parsed_context: Any) -> List[ValidationFindi return findings -# [/DEF:build_partial_recovery_findings:Function] +# #endregion build_partial_recovery_findings -# [DEF:extract_effective_filter_value:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Separate normalized filter payload metadata from the user-facing effective filter value. +# #region extract_effective_filter_value [C:2] [TYPE Function] +# @BRIEF Separate normalized filter payload metadata from the user-facing effective filter value. def extract_effective_filter_value( normalized_value: Any, raw_value: Any ) -> Any: @@ -146,14 +141,13 @@ def extract_effective_filter_value( return normalized_value if normalized_value is not None else raw_value -# [/DEF:extract_effective_filter_value:Function] +# #endregion extract_effective_filter_value -# [DEF:build_execution_snapshot:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Build effective filters, template params, approvals, and fingerprint for preview and launch gating. -# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings. -# @POST: Returns deterministic execution snapshot for current session state without mutating persistence. +# #region build_execution_snapshot [C:4] [TYPE Function] +# @BRIEF Build effective filters, template params, approvals, and fingerprint for preview and launch gating. +# @PRE Session aggregate includes imported filters, template variables, and current execution mappings. +# @POST Returns deterministic execution snapshot for current session state without mutating persistence. def build_execution_snapshot(session: DatasetReviewSession) -> Dict[str, Any]: session_record = cast(Any, session) filter_lookup = { @@ -276,14 +270,13 @@ def build_execution_snapshot(session: DatasetReviewSession) -> Dict[str, Any]: } -# [/DEF:build_execution_snapshot:Function] +# #endregion build_execution_snapshot -# [DEF:build_launch_blockers:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Enforce launch gates from findings, approvals, and current preview truth. -# @PRE: execution_snapshot was computed from current session state. -# @POST: Returns explicit blocker codes for every unmet launch invariant. +# #region build_launch_blockers [C:3] [TYPE Function] +# @BRIEF Enforce launch gates from findings, approvals, and current preview truth. +# @PRE execution_snapshot was computed from current session state. +# @POST Returns explicit blocker codes for every unmet launch invariant. def build_launch_blockers( session: DatasetReviewSession, execution_snapshot: Dict[str, Any], @@ -317,12 +310,11 @@ def build_launch_blockers( return sorted(set(blockers)) -# [/DEF:build_launch_blockers:Function] +# #endregion build_launch_blockers -# [DEF:get_latest_preview:Function] -# @COMPLEXITY: 2 -# @PURPOSE: Resolve the current latest preview snapshot for one session aggregate. +# #region get_latest_preview [C:2] [TYPE Function] +# @BRIEF Resolve the current latest preview snapshot for one session aggregate. def get_latest_preview(session: DatasetReviewSession) -> Optional[CompiledPreview]: session_record = cast(Any, session) if not session_record.previews: @@ -338,18 +330,17 @@ def get_latest_preview(session: DatasetReviewSession) -> Optional[CompiledPrevie )[0] -# [/DEF:get_latest_preview:Function] +# #endregion get_latest_preview -# [DEF:compute_preview_fingerprint:Function] -# @COMPLEXITY: 1 -# @PURPOSE: Produce deterministic execution fingerprint for preview truth and staleness checks. +# #region compute_preview_fingerprint [C:1] [TYPE Function] +# @BRIEF Produce deterministic execution fingerprint for preview truth and staleness checks. def compute_preview_fingerprint(payload: Dict[str, Any]) -> str: serialized = json.dumps(payload, sort_keys=True, default=str) return hashlib.sha256(serialized.encode("utf-8")).hexdigest() -# [/DEF:compute_preview_fingerprint:Function] +# #endregion compute_preview_fingerprint -# [/DEF:OrchestratorHelpers:Module] +# #endregion OrchestratorHelpers diff --git a/backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py b/backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py index de239355..eb12eef4 100644 --- a/backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py +++ b/backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py @@ -26,18 +26,16 @@ from src.services.dataset_review.repositories.session_repository import ( DatasetReviewSessionVersionConflictError, ) -# [DEF:SessionRepositoryTests:Module] -# @RELATION: BELONGS_TO -> SrcRoot -# @COMPLEXITY: 2 -# @PURPOSE: Unit tests for DatasetReviewSessionRepository. +# #region SessionRepositoryTests [C:2] [TYPE Module] +# @BRIEF Unit tests for DatasetReviewSessionRepository. +# @RELATION BELONGS_TO -> [SrcRoot] @pytest.fixture def db_session(): - # [DEF:db_session:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows. - # @RELATION: BINDS_TO -> [SessionRepositoryTests] + # #region db_session [C:2] [TYPE Function] + # @BRIEF Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows. + # @RELATION BINDS_TO -> [SessionRepositoryTests] engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) @@ -57,11 +55,11 @@ def db_session(): session.close() -# [/DEF:db_session:Function] + # #endregion db_session -# [DEF:test_create_session:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_create_session [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_create_session(db_session): # @PURPOSE: Verify session creation and persistence. repo = DatasetReviewSessionRepository(db_session) @@ -84,12 +82,12 @@ def test_create_session(db_session): assert loaded.version == 0 -# [/DEF:test_create_session:Function] +# #endregion test_create_session -# [DEF:test_require_session_version_conflict:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify optimistic-lock conflict is raised when caller version is stale. +# #region test_require_session_version_conflict [TYPE Function] +# @BRIEF Verify optimistic-lock conflict is raised when caller version is stale. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_require_session_version_conflict(db_session): repo = DatasetReviewSessionRepository(db_session) session = DatasetReviewSession( @@ -109,12 +107,12 @@ def test_require_session_version_conflict(db_session): assert exc_info.value.actual_version == 2 -# [/DEF:test_require_session_version_conflict:Function] +# #endregion test_require_session_version_conflict -# [DEF:test_bump_session_version_updates_last_activity:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify repository version bump increments monotonically and refreshes last activity. +# #region test_bump_session_version_updates_last_activity [TYPE Function] +# @BRIEF Verify repository version bump increments monotonically and refreshes last activity. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_bump_session_version_updates_last_activity(db_session): repo = DatasetReviewSessionRepository(db_session) session = DatasetReviewSession( @@ -134,12 +132,12 @@ def test_bump_session_version_updates_last_activity(db_session): assert session.last_activity_at >= before_activity -# [/DEF:test_bump_session_version_updates_last_activity:Function] +# #endregion test_bump_session_version_updates_last_activity -# [DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify imported-filter masking metadata persists with recovery bootstrap state. +# #region test_save_recovery_state_preserves_raw_value_masked_flag [TYPE Function] +# @BRIEF Verify imported-filter masking metadata persists with recovery bootstrap state. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_recovery_state_preserves_raw_value_masked_flag(db_session): repo = DatasetReviewSessionRepository(db_session) session = DatasetReviewSession( @@ -175,11 +173,11 @@ def test_save_recovery_state_preserves_raw_value_masked_flag(db_session): assert updated.version == 1 -# [/DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function] +# #endregion test_save_recovery_state_preserves_raw_value_masked_flag -# [DEF:test_load_session_detail_ownership:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_load_session_detail_ownership [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_load_session_detail_ownership(db_session): # @PURPOSE: Verify ownership enforcement in detail loading. repo = DatasetReviewSessionRepository(db_session) @@ -201,11 +199,11 @@ def test_load_session_detail_ownership(db_session): assert loaded_wrong is None -# [/DEF:test_load_session_detail_ownership:Function] +# #endregion test_load_session_detail_ownership -# [DEF:test_load_session_detail_collaborator:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_load_session_detail_collaborator [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_load_session_detail_collaborator(db_session): # @PURPOSE: Verify collaborator access in detail loading. repo = DatasetReviewSessionRepository(db_session) @@ -238,11 +236,11 @@ def test_load_session_detail_collaborator(db_session): assert loaded.session_id == session.session_id -# [/DEF:test_load_session_detail_collaborator:Function] +# #endregion test_load_session_detail_collaborator -# [DEF:test_save_preview_marks_stale:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_save_preview_marks_stale [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_preview_marks_stale(db_session): # @PURPOSE: Verify that saving a new preview marks old ones as stale. repo = DatasetReviewSessionRepository(db_session) @@ -271,12 +269,12 @@ def test_save_preview_marks_stale(db_session): assert session.last_preview_id == p2.preview_id -# [/DEF:test_save_preview_marks_stale:Function] +# #endregion test_save_preview_marks_stale -# [DEF:test_save_preview_increments_session_version_once_per_call:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation. +# #region test_save_preview_increments_session_version_once_per_call [TYPE Function] +# @BRIEF Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_preview_increments_session_version_once_per_call(db_session): repo = DatasetReviewSessionRepository(db_session) session = DatasetReviewSession( @@ -313,11 +311,11 @@ def test_save_preview_increments_session_version_once_per_call(db_session): assert session.version == 2 -# [/DEF:test_save_preview_increments_session_version_once_per_call:Function] +# #endregion test_save_preview_increments_session_version_once_per_call -# [DEF:test_save_profile_and_findings:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_save_profile_and_findings [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_profile_and_findings(db_session): # @PURPOSE: Verify persistence of profile and findings. repo = DatasetReviewSessionRepository(db_session) @@ -374,12 +372,12 @@ def test_save_profile_and_findings(db_session): assert final_session.version == 2 -# [/DEF:test_save_profile_and_findings:Function] +# #endregion test_save_profile_and_findings -# [DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts. +# #region test_save_profile_and_findings_rejects_stale_concurrent_write [TYPE Function] +# @BRIEF Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path): db_path = tmp_path / "dataset_review_session_repository.sqlite" engine = create_engine(f"sqlite:///{db_path}") @@ -474,11 +472,11 @@ def test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path writer_b.close() -# [/DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function] +# #endregion test_save_profile_and_findings_rejects_stale_concurrent_write -# [DEF:test_save_run_context:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_save_run_context [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_save_run_context(db_session): # @PURPOSE: Verify saving of run context. repo = DatasetReviewSessionRepository(db_session) @@ -509,12 +507,12 @@ def test_save_run_context(db_session): assert session.last_run_context_id == rc.run_context_id -# [/DEF:test_save_run_context:Function] +# #endregion test_save_run_context -# [DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests -# @PURPOSE: Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows. +# #region test_ensure_dataset_review_session_columns_adds_missing_legacy_columns [TYPE Function] +# @BRIEF Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows. +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_ensure_dataset_review_session_columns_adds_missing_legacy_columns(): engine = create_engine("sqlite:///:memory:") with engine.begin() as connection: @@ -676,11 +674,11 @@ def test_ensure_dataset_review_session_columns_adds_missing_legacy_columns(): assert raw_value_masked in (False, 0) -# [/DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function] +# #endregion test_ensure_dataset_review_session_columns_adds_missing_legacy_columns -# [DEF:test_list_sessions_for_user:Function] -# @RELATION: BINDS_TO -> SessionRepositoryTests +# #region test_list_sessions_for_user [TYPE Function] +# @RELATION BINDS_TO -> [SessionRepositoryTests] def test_list_sessions_for_user(db_session): # @PURPOSE: Verify listing of sessions by user. repo = DatasetReviewSessionRepository(db_session) @@ -714,5 +712,5 @@ def test_list_sessions_for_user(db_session): assert all(s.user_id == "user1" for s in sessions) -# [/DEF:test_list_sessions_for_user:Function] -# [/DEF:SessionRepositoryTests:Module] +# #endregion test_list_sessions_for_user +# #endregion SessionRepositoryTests diff --git a/backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py b/backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py index 4f6c9fb9..991ee223 100644 --- a/backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py +++ b/backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py @@ -1,11 +1,10 @@ -# [DEF:SessionRepositoryMutations:Module] -# @COMPLEXITY: 4 -# @PURPOSE: Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewModels] -# @RELATION: DEPENDS_ON -> [SessionEventLogger] -# @PRE: All mutations execute within authenticated request or task scope. -# @POST: Session aggregate writes preserve ownership and version semantics. +# #region SessionRepositoryMutations [C:4] [TYPE Module] +# @BRIEF Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context. +# @LAYER Domain +# @PRE All mutations execute within authenticated request or task scope. +# @POST Session aggregate writes preserve ownership and version semantics. +# @RELATION DEPENDS_ON -> [DatasetReviewModels] +# @RELATION DEPENDS_ON -> [SessionEventLogger] from __future__ import annotations @@ -36,12 +35,11 @@ from src.services.dataset_review.event_logger import SessionEventLogger log = MarkerLogger("SessionMutations") -# [DEF:save_profile_and_findings:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Persist profile state and replace validation findings for an owned session in one transaction. -# @PRE: session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope. -# @POST: stored profile matches the current session and findings are replaced by the supplied collection. -# @SIDE_EFFECT: updates profile rows, deletes stale findings, inserts current findings, and commits the transaction. +# #region save_profile_and_findings [C:4] [TYPE Function] +# @BRIEF Persist profile state and replace validation findings for an owned session in one transaction. +# @PRE session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope. +# @POST stored profile matches the current session and findings are replaced by the supplied collection. +# @SIDE_EFFECT updates profile rows, deletes stale findings, inserts current findings, and commits the transaction. def save_profile_and_findings( db: Session, event_logger: SessionEventLogger, @@ -78,15 +76,14 @@ def save_profile_and_findings( return session -# [/DEF:save_profile_and_findings:Function] +# #endregion save_profile_and_findings -# [DEF:save_recovery_state:Function] -# @COMPLEXITY: 4 -# @PURPOSE: Persist imported filters, template variables, and initial execution mappings for one owned session. -# @PRE: session_id belongs to user_id. -# @POST: Recovery state persisted to database. -# @SIDE_EFFECT: Writes to database. +# #region save_recovery_state [C:4] [TYPE Function] +# @BRIEF Persist imported filters, template variables, and initial execution mappings for one owned session. +# @PRE session_id belongs to user_id. +# @POST Recovery state persisted to database. +# @SIDE_EFFECT Writes to database. def save_recovery_state( db: Session, get_owned_session, @@ -126,15 +123,14 @@ def save_recovery_state( return load_session_detail_fn(session_id, user_id) -# [/DEF:save_recovery_state:Function] +# #endregion save_recovery_state -# [DEF:save_preview:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist a preview snapshot and mark prior session previews stale. -# @PRE: session_id belongs to user_id and preview is prepared for the same session aggregate. -# @POST: preview is persisted and the session points to the latest preview identifier. -# @SIDE_EFFECT: updates prior preview statuses, inserts a preview row, mutates the parent session, and commits. +# #region save_preview [C:3] [TYPE Function] +# @BRIEF Persist a preview snapshot and mark prior session previews stale. +# @PRE session_id belongs to user_id and preview is prepared for the same session aggregate. +# @POST preview is persisted and the session points to the latest preview identifier. +# @SIDE_EFFECT updates prior preview statuses, inserts a preview row, mutates the parent session, and commits. def save_preview( db: Session, get_owned_session, @@ -162,15 +158,14 @@ def save_preview( return preview -# [/DEF:save_preview:Function] +# #endregion save_preview -# [DEF:save_run_context:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Persist an immutable launch audit snapshot for an owned session. -# @PRE: session_id belongs to user_id and run_context targets the same aggregate. -# @POST: run context is persisted and linked as the latest launch snapshot for the session. -# @SIDE_EFFECT: inserts a run-context row, mutates the parent session pointer, and commits. +# #region save_run_context [C:3] [TYPE Function] +# @BRIEF Persist an immutable launch audit snapshot for an owned session. +# @PRE session_id belongs to user_id and run_context targets the same aggregate. +# @POST run context is persisted and linked as the latest launch snapshot for the session. +# @SIDE_EFFECT inserts a run-context row, mutates the parent session pointer, and commits. def save_run_context( db: Session, get_owned_session, @@ -197,7 +192,7 @@ def save_run_context( return run_context -# [/DEF:save_run_context:Function] +# #endregion save_run_context -# [/DEF:SessionRepositoryMutations:Module] +# #endregion SessionRepositoryMutations diff --git a/backend/src/services/dataset_review/repositories/session_repository.py b/backend/src/services/dataset_review/repositories/session_repository.py index 6ba34e58..b61af13c 100644 --- a/backend/src/services/dataset_review/repositories/session_repository.py +++ b/backend/src/services/dataset_review/repositories/session_repository.py @@ -1,19 +1,18 @@ -# [DEF:DatasetReviewSessionRepository:Module] -# @COMPLEXITY: 5 -# @PURPOSE: Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] -# @RELATION: DEPENDS_ON -> [DatasetProfile] -# @RELATION: DEPENDS_ON -> [ValidationFinding] -# @RELATION: DEPENDS_ON -> [CompiledPreview] -# @RELATION: DISPATCHES -> [SessionRepositoryMutations:Module] -# @PRE: repository operations execute within authenticated request or task scope. -# @POST: session aggregate reads are structurally consistent and writes preserve ownership and version semantics. -# @SIDE_EFFECT: reads and writes SQLAlchemy-backed session aggregates. -# @DATA_CONTRACT: Input[SessionMutation] -> Output[PersistedSessionAggregate] -# @INVARIANT: answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session. -# @RATIONALE: Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module. -# @REJECTED: Keeping all repository operations in one file because it exceeded the fractal limit. +# #region DatasetReviewSessionRepository [C:5] [TYPE Module] +# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts. +# @LAYER Domain +# @PRE repository operations execute within authenticated request or task scope. +# @POST session aggregate reads are structurally consistent and writes preserve ownership and version semantics. +# @SIDE_EFFECT reads and writes SQLAlchemy-backed session aggregates. +# @DATA_CONTRACT Input[SessionMutation] -> Output[PersistedSessionAggregate] +# @INVARIANT answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] +# @RELATION DEPENDS_ON -> [DatasetProfile] +# @RELATION DEPENDS_ON -> [ValidationFinding] +# @RELATION DEPENDS_ON -> [CompiledPreview] +# @RELATION DISPATCHES -> [SessionRepositoryMutations:Module] +# @RATIONALE Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module. +# @REJECTED Keeping all repository operations in one file because it exceeded the fractal limit. from datetime import datetime from typing import Any, Optional, List @@ -42,9 +41,8 @@ from src.services.dataset_review.event_logger import SessionEventLogger log = MarkerLogger("SessionRepository") -# [DEF:DatasetReviewSessionVersionConflictError:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Signal optimistic-lock conflicts for dataset review session mutations. +# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class] +# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations. class DatasetReviewSessionVersionConflictError(ValueError): def __init__(self, session_id: str, expected_version: int, actual_version: int): self.session_id = session_id @@ -55,34 +53,31 @@ class DatasetReviewSessionVersionConflictError(ValueError): ) -# [/DEF:DatasetReviewSessionVersionConflictError:Class] +# #endregion DatasetReviewSessionVersionConflictError -# [DEF:DatasetReviewSessionRepository:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Enforce ownership-scoped persistence and retrieval for dataset review session aggregates. -# @RELATION: DEPENDS_ON -> [DatasetReviewSession] -# @RELATION: DEPENDS_ON -> [SessionEventLogger] -# @PRE: constructor receives a live SQLAlchemy session and callers provide authenticated user scope. -# @POST: repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning. -# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session. +# #region DatasetReviewSessionRepository [C:4] [TYPE Class] +# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates. +# @PRE constructor receives a live SQLAlchemy session and callers provide authenticated user scope. +# @POST repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning. +# @SIDE_EFFECT mutates and queries the persistence layer through the injected database session. +# @RELATION DEPENDS_ON -> [DatasetReviewSession] +# @RELATION DEPENDS_ON -> [SessionEventLogger] class DatasetReviewSessionRepository: - # [DEF:init_repo:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Bind one live SQLAlchemy session to the repository instance. - # @PRE: db_session is not None - # @POST: Repository instance initialized with valid session + # #region init_repo [C:2] [TYPE Function] + # @BRIEF Bind one live SQLAlchemy session to the repository instance. + # @PRE db_session is not None + # @POST Repository instance initialized with valid session def __init__(self, db: Session): self.db = db self.event_logger = SessionEventLogger(db) - # [/DEF:init_repo:Function] + # #endregion init_repo - # [DEF:get_owned_session:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths. - # @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope. - # @POST: returns the owned session or raises a deterministic access error. + # #region get_owned_session [C:3] [TYPE Function] + # @BRIEF Resolve one owner-scoped dataset review session for mutation paths. + # @PRE session_id and user_id are non-empty identifiers from the authenticated ownership scope. + # @POST returns the owned session or raises a deterministic access error. def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession: with belief_scope("DatasetReviewSessionRepository.get_owned_session"): log.reason("Resolving owner-scoped dataset review session", payload={"session_id": session_id, "user_id": user_id}) @@ -97,12 +92,11 @@ class DatasetReviewSessionRepository: log.reflect("Owner-scoped dataset review session resolved", payload={"session_id": session.session_id}) return session - # [/DEF:get_owned_session:Function] + # #endregion get_owned_session - # [DEF:create_sess:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persist an initial dataset review session shell. - # @POST: session is committed, refreshed, and returned with persisted identifiers. + # #region create_sess [C:3] [TYPE Function] + # @BRIEF Persist an initial dataset review session shell. + # @POST session is committed, refreshed, and returned with persisted identifiers. def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession: with belief_scope("DatasetReviewSessionRepository.create_session"): log.reason("Persisting dataset review session shell", payload={"user_id": session.user_id, "environment_id": session.environment_id}) @@ -112,12 +106,11 @@ class DatasetReviewSessionRepository: log.reflect("Dataset review session shell persisted", payload={"session_id": session.session_id}) return session - # [/DEF:create_sess:Function] + # #endregion create_sess - # [DEF:require_session_version:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted. - # @POST: returns the same session when versions match; otherwise raises deterministic conflict error. + # #region require_session_version [C:3] [TYPE Function] + # @BRIEF Enforce optimistic-lock version matching before a session mutation is persisted. + # @POST returns the same session when versions match; otherwise raises deterministic conflict error. def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession: with belief_scope("DatasetReviewSessionRepository.require_session_version"): actual_version = int(getattr(session, "version", 0) or 0) @@ -128,12 +121,11 @@ class DatasetReviewSessionRepository: log.reflect("Optimistic-lock version accepted", payload={"session_id": session.session_id, "version": actual_version}) return session - # [/DEF:require_session_version:Function] + # #endregion require_session_version - # [DEF:bump_session_version:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled. - # @POST: session version increments monotonically. + # #region bump_session_version [C:2] [TYPE Function] + # @BRIEF Increment optimistic-lock version after a successful session mutation is assembled. + # @POST session version increments monotonically. def bump_session_version(self, session: DatasetReviewSession) -> int: with belief_scope("DatasetReviewSessionRepository.bump_session_version"): next_version = int(getattr(session, "version", 0) or 0) + 1 @@ -142,12 +134,11 @@ class DatasetReviewSessionRepository: log.reflect("Prepared incremented session version", payload={"session_id": session.session_id, "version": next_version}) return next_version - # [/DEF:bump_session_version:Function] + # #endregion bump_session_version - # [DEF:commit_session_mutation:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts. - # @POST: session mutation is committed with one version increment or a deterministic conflict error is raised. + # #region commit_session_mutation [C:4] [TYPE Function] + # @BRIEF Commit one prepared session mutation and translate stale writes into deterministic conflicts. + # @POST session mutation is committed with one version increment or a deterministic conflict error is raised. def commit_session_mutation( self, session: DatasetReviewSession, *, refresh_targets: Optional[List[Any]] = None, expected_version: Optional[int] = None, ) -> DatasetReviewSession: @@ -169,12 +160,11 @@ class DatasetReviewSessionRepository: log.reflect("Session mutation committed", payload={"session_id": session.session_id, "version": getattr(session, "version", None)}) return session - # [/DEF:commit_session_mutation:Function] + # #endregion commit_session_mutation - # [DEF:load_detail:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Return the full session aggregate for API and frontend resume flows. - # @POST: Returns SessionDetail with all fields populated or None. + # #region load_detail [C:3] [TYPE Function] + # @BRIEF Return the full session aggregate for API and frontend resume flows. + # @POST Returns SessionDetail with all fields populated or None. def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]: with belief_scope("DatasetReviewSessionRepository.load_session_detail"): log.reason("Loading dataset review session detail", payload={"session_id": session_id, "user_id": user_id}) @@ -203,12 +193,11 @@ class DatasetReviewSessionRepository: log.reflect("Session detail lookup completed", payload={"session_id": session_id, "found": bool(session)}) return session - # [/DEF:load_detail:Function] + # #endregion load_detail - # [DEF:save_profile_and_findings:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Persist profile state and replace validation findings for an owned session. - # @POST: stored profile matches the current session and findings are replaced. + # #region save_profile_and_findings [C:4] [TYPE Function] + # @BRIEF Persist profile state and replace validation findings for an owned session. + # @POST stored profile matches the current session and findings are replaced. def save_profile_and_findings( self, session_id: str, user_id: str, profile: DatasetProfile, findings: List[ValidationFinding], expected_version: Optional[int] = None, ) -> DatasetReviewSession: @@ -218,11 +207,10 @@ class DatasetReviewSessionRepository: self.commit_session_mutation, session_id, user_id, profile, findings, expected_version, ) - # [/DEF:save_profile_and_findings:Function] + # #endregion save_profile_and_findings - # [DEF:save_recovery_state:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persist imported filters, template variables, and initial execution mappings. + # #region save_recovery_state [C:3] [TYPE Function] + # @BRIEF Persist imported filters, template variables, and initial execution mappings. def save_recovery_state( self, session_id: str, user_id: str, imported_filters: List[ImportedFilter], template_variables: List[TemplateVariable], execution_mappings: List[ExecutionMapping], @@ -235,11 +223,10 @@ class DatasetReviewSessionRepository: session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version, ) - # [/DEF:save_recovery_state:Function] + # #endregion save_recovery_state - # [DEF:save_preview:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persist a preview snapshot and mark prior session previews stale. + # #region save_preview [C:3] [TYPE Function] + # @BRIEF Persist a preview snapshot and mark prior session previews stale. def save_preview( self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None, ) -> CompiledPreview: @@ -249,11 +236,10 @@ class DatasetReviewSessionRepository: self.commit_session_mutation, session_id, user_id, preview, expected_version, ) - # [/DEF:save_preview:Function] + # #endregion save_preview - # [DEF:save_run_context:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Persist an immutable launch audit snapshot for an owned session. + # #region save_run_context [C:3] [TYPE Function] + # @BRIEF Persist an immutable launch audit snapshot for an owned session. def save_run_context( self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None, ) -> DatasetRunContext: @@ -263,11 +249,10 @@ class DatasetReviewSessionRepository: self.commit_session_mutation, session_id, user_id, run_context, expected_version, ) - # [/DEF:save_run_context:Function] + # #endregion save_run_context - # [DEF:list_user_sess:Function] - # @COMPLEXITY: 2 - # @PURPOSE: List review sessions owned by a specific user ordered by most recent update. + # #region list_user_sess [C:2] [TYPE Function] + # @BRIEF List review sessions owned by a specific user ordered by most recent update. def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]: with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"): log.reason("Listing dataset review sessions for owner scope", payload={"user_id": user_id}) @@ -280,9 +265,7 @@ class DatasetReviewSessionRepository: log.reflect("Session list assembled", payload={"user_id": user_id, "session_count": len(sessions)}) return sessions - # [/DEF:list_user_sess:Function] + # #endregion list_user_sess -# [/DEF:DatasetReviewSessionRepository:Class] - -# [/DEF:DatasetReviewSessionRepository:Module] +# #endregion DatasetReviewSessionRepository diff --git a/backend/src/services/dataset_review/semantic_resolver.py b/backend/src/services/dataset_review/semantic_resolver.py index ad103266..6c144588 100644 --- a/backend/src/services/dataset_review/semantic_resolver.py +++ b/backend/src/services/dataset_review/semantic_resolver.py @@ -1,20 +1,18 @@ -# [DEF:SemanticSourceResolver:Module] -# @COMPLEXITY: 4 -# @SEMANTICS: dataset_review, semantic_resolution, dictionary, trusted_sources, ranking -# @PURPOSE: Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[LLMProviderService] -# @RELATION: [DEPENDS_ON] ->[SemanticSource] -# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] -# @RELATION: [DEPENDS_ON] ->[SemanticCandidate] -# @PRE: selected source and target field set must be known. -# @POST: candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable. -# @SIDE_EFFECT: may create conflict findings and semantic candidate records. -# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values. +# #region SemanticSourceResolver [C:4] [TYPE Module] [SEMANTICS dataset_review, semantic_resolution, dictionary, trusted_sources, ranking] +# @BRIEF Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback. +# @LAYER Domain +# @PRE selected source and target field set must be known. +# @POST candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable. +# @SIDE_EFFECT may create conflict findings and semantic candidate records. +# @INVARIANT Manual overrides are never silently replaced by imported, inferred, or AI-generated values. +# @RELATION DEPENDS_ON -> [LLMProviderService] +# @RELATION DEPENDS_ON -> [SemanticSource] +# @RELATION DEPENDS_ON -> [SemanticFieldEntry] +# @RELATION DEPENDS_ON -> [SemanticCandidate] from __future__ import annotations -# [DEF:imports:Block] +# #region imports [TYPE Block] from dataclasses import dataclass, field from difflib import SequenceMatcher from typing import Any, Dict, Iterable, List, Mapping, Optional @@ -27,48 +25,44 @@ from src.models.dataset_review import ( FieldProvenance, SemanticSource, ) -# [/DEF:imports:Block] +# #endregion imports log = MarkerLogger("SemanticSourceResolver") -# [DEF:DictionaryResolutionResult:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Carries field-level dictionary resolution output with explicit review and partial-recovery state. +# #region DictionaryResolutionResult [C:2] [TYPE Class] +# @BRIEF Carries field-level dictionary resolution output with explicit review and partial-recovery state. @dataclass class DictionaryResolutionResult: source_ref: str resolved_fields: List[Dict[str, Any]] = field(default_factory=list) unresolved_fields: List[str] = field(default_factory=list) partial_recovery: bool = False -# [/DEF:DictionaryResolutionResult:Class] +# #endregion DictionaryResolutionResult -# [DEF:SemanticSourceResolver:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Resolve semantic candidates from trusted sources while preserving manual locks and confidence ordering. -# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] -# @RELATION: [DEPENDS_ON] ->[SemanticCandidate] -# @PRE: source payload and target field collection are provided by the caller. -# @POST: result contains confidence-ranked candidates and does not overwrite manual locks implicitly. -# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions. +# #region SemanticSourceResolver [C:4] [TYPE Class] +# @BRIEF Resolve semantic candidates from trusted sources while preserving manual locks and confidence ordering. +# @PRE source payload and target field collection are provided by the caller. +# @POST result contains confidence-ranked candidates and does not overwrite manual locks implicitly. +# @SIDE_EFFECT emits semantic trace logs for ranking and fallback decisions. +# @RELATION DEPENDS_ON -> [SemanticFieldEntry] +# @RELATION DEPENDS_ON -> [SemanticCandidate] class SemanticSourceResolver: - # [DEF:resolve_from_file:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize uploaded semantic file records into field-level candidates. + # #region resolve_from_file [C:2] [TYPE Function] + # @BRIEF Normalize uploaded semantic file records into field-level candidates. def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult: return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "uploaded_file")) - # [/DEF:resolve_from_file:Function] + # #endregion resolve_from_file - # [DEF:resolve_from_dictionary:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Resolve candidates from connected tabular dictionary sources. - # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] - # @RELATION: [DEPENDS_ON] ->[SemanticCandidate] - # @PRE: dictionary source exists and fields contain stable field_name values. - # @POST: returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit. - # @SIDE_EFFECT: emits belief-state logs describing trusted-match and partial-recovery outcomes. - # @DATA_CONTRACT: Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult] + # #region resolve_from_dictionary [C:4] [TYPE Function] + # @BRIEF Resolve candidates from connected tabular dictionary sources. + # @PRE dictionary source exists and fields contain stable field_name values. + # @POST returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit. + # @SIDE_EFFECT emits belief-state logs describing trusted-match and partial-recovery outcomes. + # @DATA_CONTRACT Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult] + # @RELATION DEPENDS_ON -> [SemanticFieldEntry] + # @RELATION DEPENDS_ON -> [SemanticCandidate] def resolve_from_dictionary( self, source_payload: Mapping[str, Any], @@ -210,23 +204,21 @@ class SemanticSourceResolver: }, ) return result - # [/DEF:resolve_from_dictionary:Function] + # #endregion resolve_from_dictionary - # [DEF:resolve_from_reference_dataset:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Reuse semantic metadata from trusted Superset datasets. + # #region resolve_from_reference_dataset [C:2] [TYPE Function] + # @BRIEF Reuse semantic metadata from trusted Superset datasets. def resolve_from_reference_dataset( self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]], ) -> DictionaryResolutionResult: return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "reference_dataset")) - # [/DEF:resolve_from_reference_dataset:Function] + # #endregion resolve_from_reference_dataset - # [DEF:rank_candidates:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Apply confidence ordering and determine best candidate per field. - # @RELATION: [DEPENDS_ON] ->[SemanticCandidate] + # #region rank_candidates [C:2] [TYPE Function] + # @BRIEF Apply confidence ordering and determine best candidate per field. + # @RELATION DEPENDS_ON -> [SemanticCandidate] def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]: ranked = sorted( candidates, @@ -239,33 +231,30 @@ class SemanticSourceResolver: for index, candidate in enumerate(ranked, start=1): candidate["candidate_rank"] = index return ranked - # [/DEF:rank_candidates:Function] + # #endregion rank_candidates - # [DEF:detect_conflicts:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Mark competing candidate sets that require explicit user review. + # #region detect_conflicts [C:2] [TYPE Function] + # @BRIEF Mark competing candidate sets that require explicit user review. def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool: return len(candidates) > 1 - # [/DEF:detect_conflicts:Function] + # #endregion detect_conflicts - # [DEF:apply_field_decision:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Accept, reject, or manually override a field-level semantic value. + # #region apply_field_decision [C:2] [TYPE Function] + # @BRIEF Accept, reject, or manually override a field-level semantic value. def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]: merged = dict(field_state) merged.update(decision) return merged - # [/DEF:apply_field_decision:Function] + # #endregion apply_field_decision - # [DEF:propagate_source_version_update:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values. - # @RELATION: [DEPENDS_ON] ->[SemanticSource] - # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] - # @PRE: source is persisted and fields belong to the same session aggregate. - # @POST: unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched. - # @SIDE_EFFECT: mutates in-memory field state for the caller to persist. - # @DATA_CONTRACT: Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]] + # #region propagate_source_version_update [C:4] [TYPE Function] + # @BRIEF Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values. + # @PRE source is persisted and fields belong to the same session aggregate. + # @POST unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched. + # @SIDE_EFFECT mutates in-memory field state for the caller to persist. + # @DATA_CONTRACT Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]] + # @RELATION DEPENDS_ON -> [SemanticSource] + # @RELATION DEPENDS_ON -> [SemanticFieldEntry] def propagate_source_version_update( self, source: SemanticSource, @@ -309,11 +298,10 @@ class SemanticSourceResolver: "preserved_locked": preserved_locked, "untouched": untouched, } - # [/DEF:propagate_source_version_update:Function] + # #endregion propagate_source_version_update - # [DEF:_normalize_dictionary_row:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize one dictionary row into a consistent lookup structure. + # #region _normalize_dictionary_row [C:2] [TYPE Function] + # @BRIEF Normalize one dictionary row into a consistent lookup structure. def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]: field_name = ( row.get("field_name") @@ -329,11 +317,10 @@ class SemanticSourceResolver: "description": row.get("description"), "display_format": row.get("display_format") or row.get("format"), } - # [/DEF:_normalize_dictionary_row:Function] + # #endregion _normalize_dictionary_row - # [DEF:_find_fuzzy_matches:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable. + # #region _find_fuzzy_matches [C:2] [TYPE Function] + # @BRIEF Produce confidence-scored fuzzy matches while keeping them reviewable. def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: normalized_target = self._normalize_key(field_name) fuzzy_matches: List[Dict[str, Any]] = [] @@ -347,11 +334,10 @@ class SemanticSourceResolver: fuzzy_matches.append({"row": row, "score": round(score, 3)}) fuzzy_matches.sort(key=lambda item: item["score"], reverse=True) return fuzzy_matches[:3] - # [/DEF:_find_fuzzy_matches:Function] + # #endregion _find_fuzzy_matches - # [DEF:_build_candidate_payload:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Project normalized dictionary rows into semantic candidate payloads. + # #region _build_candidate_payload [C:2] [TYPE Function] + # @BRIEF Project normalized dictionary rows into semantic candidate payloads. def _build_candidate_payload( self, rank: int, @@ -368,11 +354,10 @@ class SemanticSourceResolver: "proposed_display_format": row.get("display_format"), "status": CandidateStatus.PROPOSED.value, } - # [/DEF:_build_candidate_payload:Function] + # #endregion _build_candidate_payload - # [DEF:_match_priority:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention. + # #region _match_priority [C:2] [TYPE Function] + # @BRIEF Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention. def _match_priority(self, match_type: Optional[str]) -> int: priority = { CandidateMatchType.EXACT.value: 0, @@ -381,14 +366,11 @@ class SemanticSourceResolver: CandidateMatchType.GENERATED.value: 3, } return priority.get(str(match_type or ""), 99) - # [/DEF:_match_priority:Function] + # #endregion _match_priority - # [DEF:_normalize_key:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons. + # #region _normalize_key [C:2] [TYPE Function] + # @BRIEF Normalize field identifiers for stable exact/fuzzy comparisons. def _normalize_key(self, value: str) -> str: return "".join(ch for ch in str(value or "").strip().lower() if ch.isalnum() or ch == "_") - # [/DEF:_normalize_key:Function] -# [/DEF:SemanticSourceResolver:Class] - -# [/DEF:SemanticSourceResolver:Module] \ No newline at end of file + # #endregion _normalize_key +# #endregion SemanticSourceResolver diff --git a/backend/src/services/git/__init__.py b/backend/src/services/git/__init__.py index 02781332..20a2f78e 100644 --- a/backend/src/services/git/__init__.py +++ b/backend/src/services/git/__init__.py @@ -1,19 +1,17 @@ -# [DEF:GitServiceModule:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, service, decomposition, mixin, composition -# @PURPOSE: Composed GitService via multiple inheritance from domain-specific mixins. -# @RELATION: DEPENDS_ON -> [GitServiceBase] -# @RELATION: DEPENDS_ON -> [GitServiceBranchMixin] -# @RELATION: DEPENDS_ON -> [GitServiceSyncMixin] -# @RELATION: DEPENDS_ON -> [GitServiceStatusMixin] -# @RELATION: DEPENDS_ON -> [GitServiceMergeMixin] -# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin] -# @RELATION: DEPENDS_ON -> [GitServiceGiteaMixin] -# @RELATION: DEPENDS_ON -> [GitServiceGithubMixin] -# @RELATION: DEPENDS_ON -> [GitServiceGitlabMixin] +# #region GitServiceModule [C:3] [TYPE Module] [SEMANTICS git, service, decomposition, mixin, composition] +# @BRIEF Composed GitService via multiple inheritance from domain-specific mixins. +# @LAYER Infra +# @RELATION DEPENDS_ON -> [GitServiceBase] +# @RELATION DEPENDS_ON -> [GitServiceBranchMixin] +# @RELATION DEPENDS_ON -> [GitServiceSyncMixin] +# @RELATION DEPENDS_ON -> [GitServiceStatusMixin] +# @RELATION DEPENDS_ON -> [GitServiceMergeMixin] +# @RELATION DEPENDS_ON -> [GitServiceUrlMixin] +# @RELATION DEPENDS_ON -> [GitServiceGiteaMixin] +# @RELATION DEPENDS_ON -> [GitServiceGithubMixin] +# @RELATION DEPENDS_ON -> [GitServiceGitlabMixin] +# @RATIONALE Decomposed from monolithic git_service.py (2101 lines) into # -# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into # domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class # preserves the original public API surface — all consumers continue to import # `from src.services.git_service import GitService` without changes. @@ -31,9 +29,8 @@ from ._remote_providers import GitServiceGithubMixin, GitServiceGitlabMixin __all__ = ["GitService"] -# [DEF:GitService:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull, +# #region GitService [C:3] [TYPE Class] +# @BRIEF Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull, # merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab). # @RELATION: INHERITS -> [GitServiceBase] # @RELATION: INHERITS -> [GitServiceBranchMixin] @@ -62,5 +59,5 @@ class GitService( All consumers continue to use: from src.services.git_service import GitService """ pass -# [/DEF:GitService:Class] -# [/DEF:GitServiceModule:Module] +# #endregion GitService +# #endregion GitServiceModule diff --git a/backend/src/services/git/_base.py b/backend/src/services/git/_base.py index 6075a9f3..c040bb0c 100644 --- a/backend/src/services/git/_base.py +++ b/backend/src/services/git/_base.py @@ -1,12 +1,10 @@ -# [DEF:GitServiceBase:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, service, repository, version_control, initialization -# @PURPOSE: Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration. -# @RELATION: INHERITED_BY -> [GitService] -# @RELATION: DEPENDS_ON -> [SessionLocal] -# @RELATION: DEPENDS_ON -> [AppConfigRecord] -# @RELATION: DEPENDS_ON -> [GitRepository] +# #region GitServiceBase [C:3] [TYPE Module] [SEMANTICS git, service, repository, version_control, initialization] +# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration. +# @LAYER Infra +# @RELATION INHERITED_BY -> [GitService] +# @RELATION DEPENDS_ON -> [SessionLocal] +# @RELATION DEPENDS_ON -> [AppConfigRecord] +# @RELATION DEPENDS_ON -> [GitRepository] import os import re @@ -25,12 +23,11 @@ from src.models.config import AppConfigRecord from src.core.database import SessionLocal -# [DEF:GitServiceBase:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Base class for GitService providing initialization, path resolution, repository lifecycle, and identity. +# #region GitServiceBase [C:3] [TYPE Class] +# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, and identity. class GitServiceBase: - # [DEF:GitService_init:Function] - # @PURPOSE: Initializes the GitService with a base path for repositories. + # #region GitService_init [TYPE Function] + # @BRIEF Initializes the GitService with a base path for repositories. # @PARAM: base_path (str) - Root directory for all Git clones. # @PRE: base_path is a valid string path. # @POST: GitService is initialized; base_path directory exists. @@ -41,12 +38,12 @@ class GitServiceBase: self._uses_default_base_path = base_path == "git_repos" self.base_path = self._resolve_base_path(base_path) self._ensure_base_path_exists() - # [/DEF:GitService_init:Function] + # #endregion GitService_init - # [DEF:_ensure_base_path_exists:Function] - # @PURPOSE: Ensure the repositories root directory exists and is a directory. - # @PRE: self.base_path is resolved to filesystem path. - # @POST: self.base_path exists as directory or raises ValueError. + # #region _ensure_base_path_exists [TYPE Function] + # @BRIEF Ensure the repositories root directory exists and is a directory. + # @PRE self.base_path is resolved to filesystem path. + # @POST self.base_path exists as directory or raises ValueError. def _ensure_base_path_exists(self) -> None: base = Path(self.base_path) if base.exists() and not base.is_dir(): @@ -56,13 +53,13 @@ class GitServiceBase: except (PermissionError, OSError) as e: log.explore("Cannot create Git repositories base path", payload={"base_path": self.base_path}, error=str(e)) raise ValueError(f"Cannot create Git repositories base path: {self.base_path}. {e}") - # [/DEF:_ensure_base_path_exists:Function] + # #endregion _ensure_base_path_exists - # [DEF:_resolve_base_path:Function] - # @PURPOSE: Resolve base repository directory from explicit argument or global storage settings. - # @PRE: base_path is a string path. - # @POST: Returns absolute path for Git repositories root. - # @RETURN: str + # #region _resolve_base_path [TYPE Function] + # @BRIEF Resolve base repository directory from explicit argument or global storage settings. + # @PRE base_path is a string path. + # @POST Returns absolute path for Git repositories root. + # @RETURN str def _resolve_base_path(self, base_path: str) -> str: backend_root = Path(__file__).parents[3] fallback_path = str((backend_root / base_path).resolve()) @@ -91,23 +88,23 @@ class GitServiceBase: except Exception as e: log.explore("Falling back to default base path", error=str(e)) return fallback_path - # [/DEF:_resolve_base_path:Function] + # #endregion _resolve_base_path - # [DEF:_normalize_repo_key:Function] - # @PURPOSE: Convert user/dashboard-provided key to safe filesystem directory name. - # @PRE: repo_key can be None/empty. - # @POST: Returns normalized non-empty key. - # @RETURN: str + # #region _normalize_repo_key [TYPE Function] + # @BRIEF Convert user/dashboard-provided key to safe filesystem directory name. + # @PRE repo_key can be None/empty. + # @POST Returns normalized non-empty key. + # @RETURN str def _normalize_repo_key(self, repo_key: Optional[str]) -> str: raw_key = str(repo_key or "").strip().lower() normalized = re.sub(r"[^a-z0-9._-]+", "-", raw_key).strip("._-") return normalized or "dashboard" - # [/DEF:_normalize_repo_key:Function] + # #endregion _normalize_repo_key - # [DEF:_update_repo_local_path:Function] - # @PURPOSE: Persist repository local_path in GitRepository table when record exists. - # @PRE: dashboard_id is valid integer. - # @POST: local_path is updated for existing record. + # #region _update_repo_local_path [TYPE Function] + # @BRIEF Persist repository local_path in GitRepository table when record exists. + # @PRE dashboard_id is valid integer. + # @POST local_path is updated for existing record. def _update_repo_local_path(self, dashboard_id: int, local_path: str) -> None: try: session = SessionLocal() @@ -124,13 +121,13 @@ class GitServiceBase: session.close() except Exception as e: log.explore("Failed to update repo local path", error=str(e)) - # [/DEF:_update_repo_local_path:Function] + # #endregion _update_repo_local_path - # [DEF:_migrate_repo_directory:Function] - # @PURPOSE: Move legacy repository directory to target path and sync DB metadata. - # @PRE: source_path exists. - # @POST: Repository content available at target_path. - # @RETURN: str + # #region _migrate_repo_directory [TYPE Function] + # @BRIEF Move legacy repository directory to target path and sync DB metadata. + # @PRE source_path exists. + # @POST Repository content available at target_path. + # @RETURN str def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str: source_abs = os.path.abspath(source_path) target_abs = os.path.abspath(target_path) @@ -147,10 +144,10 @@ class GitServiceBase: self._update_repo_local_path(dashboard_id, target_abs) log.reflect(f"Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}") return target_abs - # [/DEF:_migrate_repo_directory:Function] + # #endregion _migrate_repo_directory - # [DEF:_get_repo_path:Function] - # @PURPOSE: Resolves the local filesystem path for a dashboard's repository. + # #region _get_repo_path [TYPE Function] + # @BRIEF Resolves the local filesystem path for a dashboard's repository. # @PARAM: dashboard_id (int) # @PARAM: repo_key (Optional[str]) - Slug-like key used when DB local_path is absent. # @PRE: dashboard_id is an integer. @@ -193,10 +190,10 @@ class GitServiceBase: if os.path.exists(target_path): self._update_repo_local_path(dashboard_id, target_path) return target_path - # [/DEF:_get_repo_path:Function] + # #endregion _get_repo_path - # [DEF:init_repo:Function] - # @PURPOSE: Initialize or clone a repository for a dashboard. + # #region init_repo [TYPE Function] + # @BRIEF Initialize or clone a repository for a dashboard. # @PARAM: dashboard_id (int), remote_url (str), pat (str), repo_key (Optional[str]). # @PRE: dashboard_id is int, remote_url is valid Git URL, pat is provided. # @POST: Repository is cloned or opened at the local path. @@ -232,12 +229,12 @@ class GitServiceBase: repo = Repo.clone_from(auth_url, repo_path) self._ensure_gitflow_branches(repo, dashboard_id) return repo - # [/DEF:init_repo:Function] + # #endregion init_repo - # [DEF:delete_repo:Function] - # @PURPOSE: Remove local repository and DB binding for a dashboard. - # @PRE: dashboard_id is a valid integer. - # @POST: Local path is deleted when present and GitRepository row is removed. + # #region delete_repo [TYPE Function] + # @BRIEF Remove local repository and DB binding for a dashboard. + # @PRE dashboard_id is a valid integer. + # @POST Local path is deleted when present and GitRepository row is removed. def delete_repo(self, dashboard_id: int) -> None: with belief_scope("GitService.delete_repo"): repo_path = self._get_repo_path(dashboard_id) @@ -274,13 +271,13 @@ class GitServiceBase: raise HTTPException(status_code=500, detail=f"Failed to delete repository: {str(e)}") finally: session.close() - # [/DEF:delete_repo:Function] + # #endregion delete_repo - # [DEF:get_repo:Function] - # @PURPOSE: Get Repo object for a dashboard. - # @PRE: Repository must exist on disk for the given dashboard_id. - # @POST: Returns a GitPython Repo instance for the dashboard. - # @RETURN: Repo + # #region get_repo [TYPE Function] + # @BRIEF Get Repo object for a dashboard. + # @PRE Repository must exist on disk for the given dashboard_id. + # @POST Returns a GitPython Repo instance for the dashboard. + # @RETURN Repo def get_repo(self, dashboard_id: int) -> Repo: with belief_scope("GitService.get_repo"): repo_path = self._get_repo_path(dashboard_id) @@ -292,12 +289,12 @@ class GitServiceBase: except Exception as e: log.explore(f"Failed to open repository at {repo_path}", error=str(e)) raise HTTPException(status_code=500, detail="Failed to open local Git repository") - # [/DEF:get_repo:Function] + # #endregion get_repo - # [DEF:configure_identity:Function] - # @PURPOSE: Configure repository-local Git committer identity for user-scoped operations. - # @PRE: dashboard_id repository exists; git_username/git_email may be empty. - # @POST: Repository config has user.name and user.email when both identity values are provided. + # #region configure_identity [TYPE Function] + # @BRIEF Configure repository-local Git committer identity for user-scoped operations. + # @PRE dashboard_id repository exists; git_username/git_email may be empty. + # @POST Repository config has user.name and user.email when both identity values are provided. def configure_identity(self, dashboard_id: int, git_username: Optional[str], git_email: Optional[str]) -> None: with belief_scope("GitService.configure_identity"): normalized_username = str(git_username or "").strip() @@ -313,6 +310,6 @@ class GitServiceBase: except Exception as e: log.explore("Failed to configure git identity", error=str(e)) raise HTTPException(status_code=500, detail=f"Failed to configure git identity: {str(e)}") - # [/DEF:configure_identity:Function] -# [/DEF:GitServiceBase:Class] -# [/DEF:GitServiceBase:Module] + # #endregion configure_identity +# #endregion GitServiceBase +# #endregion GitServiceBase diff --git a/backend/src/services/git/_branch.py b/backend/src/services/git/_branch.py index 7127d6df..3f18af92 100644 --- a/backend/src/services/git/_branch.py +++ b/backend/src/services/git/_branch.py @@ -1,9 +1,7 @@ -# [DEF:GitServiceBranchMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, branches, commits, checkout, gitflow -# @PURPOSE: Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes. -# @RELATION: USED_BY -> [GitService] +# #region GitServiceBranchMixin [C:3] [TYPE Module] [SEMANTICS git, branches, commits, checkout, gitflow] +# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] import os from typing import Any, Dict, List, Optional @@ -17,14 +15,13 @@ from src.core.cot_logger import MarkerLogger log = MarkerLogger("GitBranch") -# [DEF:GitServiceBranchMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing branch and commit operations for GitService. +# #region GitServiceBranchMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing branch and commit operations for GitService. class GitServiceBranchMixin: - # [DEF:_ensure_gitflow_branches:Function] - # @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin. - # @PRE: repo is a valid GitPython Repo instance. - # @POST: main, dev, preprod are available in local repository and pushed to origin when available. + # #region _ensure_gitflow_branches [TYPE Function] + # @BRIEF Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin. + # @PRE repo is a valid GitPython Repo instance. + # @POST main, dev, preprod are available in local repository and pushed to origin when available. def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None: with belief_scope("GitService._ensure_gitflow_branches"): required_branches = ["main", "dev", "preprod"] @@ -78,13 +75,13 @@ class GitServiceBranchMixin: log.reason("Checked out default branch dev", payload={"dashboard_id": dashboard_id}) except Exception as e: log.explore("Could not checkout dev branch", payload={"dashboard_id": dashboard_id}, error=str(e)) - # [/DEF:_ensure_gitflow_branches:Function] + # #endregion _ensure_gitflow_branches - # [DEF:list_branches:Function] - # @PURPOSE: List all branches for a dashboard's repository. - # @PRE: Repository for dashboard_id exists. - # @POST: Returns a list of branch metadata dictionaries. - # @RETURN: List[dict] + # #region list_branches [TYPE Function] + # @BRIEF List all branches for a dashboard's repository. + # @PRE Repository for dashboard_id exists. + # @POST Returns a list of branch metadata dictionaries. + # @RETURN List[dict] def list_branches(self, dashboard_id: int) -> List[dict]: with belief_scope("GitService.list_branches"): repo = self.get_repo(dashboard_id) @@ -118,10 +115,10 @@ class GitServiceBranchMixin: "is_remote": False, "last_updated": datetime.utcnow() }) return branches - # [/DEF:list_branches:Function] + # #endregion list_branches - # [DEF:create_branch:Function] - # @PURPOSE: Create a new branch from an existing one. + # #region create_branch [TYPE Function] + # @BRIEF Create a new branch from an existing one. # @PARAM: name (str) - New branch name. # @PARAM: from_branch (str) - Source branch. # @PRE: Repository exists; name is valid; from_branch exists or repo is empty. @@ -149,21 +146,21 @@ class GitServiceBranchMixin: except Exception as e: log.explore("Failed to create branch", error=str(e)) raise - # [/DEF:create_branch:Function] + # #endregion create_branch - # [DEF:checkout_branch:Function] - # @PURPOSE: Switch to a specific branch. - # @PRE: Repository exists and the specified branch name exists. - # @POST: The repository working directory is updated to the specified branch. + # #region checkout_branch [TYPE Function] + # @BRIEF Switch to a specific branch. + # @PRE Repository exists and the specified branch name exists. + # @POST The repository working directory is updated to the specified branch. def checkout_branch(self, dashboard_id: int, name: str): with belief_scope("GitService.checkout_branch"): repo = self.get_repo(dashboard_id) log.reason("Checking out branch", payload={"name": name}) repo.git.checkout(name) - # [/DEF:checkout_branch:Function] + # #endregion checkout_branch - # [DEF:commit_changes:Function] - # @PURPOSE: Stage and commit changes. + # #region commit_changes [TYPE Function] + # @BRIEF Stage and commit changes. # @PARAM: message (str) - Commit message. # @PARAM: files (List[str]) - Optional list of specific files to stage. # @PRE: Repository exists and has changes (dirty) or files are specified. @@ -182,6 +179,6 @@ class GitServiceBranchMixin: repo.git.add(A=True) repo.index.commit(message) log.reflect("Committed changes", payload={"message": message}) - # [/DEF:commit_changes:Function] -# [/DEF:GitServiceBranchMixin:Class] -# [/DEF:GitServiceBranchMixin:Module] + # #endregion commit_changes +# #endregion GitServiceBranchMixin +# #endregion GitServiceBranchMixin diff --git a/backend/src/services/git/_gitea.py b/backend/src/services/git/_gitea.py index e3f87363..a467f592 100644 --- a/backend/src/services/git/_gitea.py +++ b/backend/src/services/git/_gitea.py @@ -1,9 +1,7 @@ -# [DEF:GitServiceGiteaMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, gitea, api, provider, connection_test -# @PURPOSE: Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. -# @RELATION: USED_BY -> [GitService] +# #region GitServiceGiteaMixin [C:3] [TYPE Module] [SEMANTICS git, gitea, api, provider, connection_test] +# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] import httpx from typing import Any, Dict, List, Optional @@ -16,12 +14,11 @@ log = MarkerLogger("GitGitea") from src.models.git import GitProvider -# [DEF:GitServiceGiteaMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing Gitea API operations for GitService. +# #region GitServiceGiteaMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing Gitea API operations for GitService. class GitServiceGiteaMixin: - # [DEF:test_connection:Function] - # @PURPOSE: Test connection to Git provider using PAT. + # #region test_connection [TYPE Function] + # @BRIEF Test connection to Git provider using PAT. # @PARAM: provider (GitProvider), url (str), pat (str) # @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided. # @POST: Returns True if connection to the provider's API succeeds. @@ -60,13 +57,13 @@ class GitServiceGiteaMixin: except Exception as e: log.explore(f"Error testing git connection: {e}", error=str(e)) return False - # [/DEF:test_connection:Function] + # #endregion test_connection - # [DEF:_gitea_headers:Function] - # @PURPOSE: Build Gitea API authorization headers. - # @PRE: pat is provided. - # @POST: Returns headers with token auth. - # @RETURN: Dict[str, str] + # #region _gitea_headers [TYPE Function] + # @BRIEF Build Gitea API authorization headers. + # @PRE pat is provided. + # @POST Returns headers with token auth. + # @RETURN Dict[str, str] def _gitea_headers(self, pat: str) -> Dict[str, str]: token = (pat or "").strip() if not token: @@ -76,13 +73,13 @@ class GitServiceGiteaMixin: "Content-Type": "application/json", "Accept": "application/json", } - # [/DEF:_gitea_headers:Function] + # #endregion _gitea_headers - # [DEF:_gitea_request:Function] - # @PURPOSE: Execute HTTP request against Gitea API with stable error mapping. - # @PRE: method and endpoint are valid. - # @POST: Returns decoded JSON payload. - # @RETURN: Any + # #region _gitea_request [TYPE Function] + # @BRIEF Execute HTTP request against Gitea API with stable error mapping. + # @PRE method and endpoint are valid. + # @POST Returns decoded JSON payload. + # @RETURN Any async def _gitea_request( self, method: str, server_url: str, pat: str, endpoint: str, payload: Optional[Dict[str, Any]] = None, @@ -108,38 +105,38 @@ class GitServiceGiteaMixin: if response.status_code == 204: return None return response.json() - # [/DEF:_gitea_request:Function] + # #endregion _gitea_request - # [DEF:get_gitea_current_user:Function] - # @PURPOSE: Resolve current Gitea user for PAT. - # @PRE: server_url and pat are valid. - # @POST: Returns current username. - # @RETURN: str + # #region get_gitea_current_user [TYPE Function] + # @BRIEF Resolve current Gitea user for PAT. + # @PRE server_url and pat are valid. + # @POST Returns current username. + # @RETURN str async def get_gitea_current_user(self, server_url: str, pat: str) -> str: payload = await self._gitea_request("GET", server_url, pat, "/user") username = payload.get("login") or payload.get("username") if not username: raise HTTPException(status_code=500, detail="Failed to resolve Gitea username") return str(username) - # [/DEF:get_gitea_current_user:Function] + # #endregion get_gitea_current_user - # [DEF:list_gitea_repositories:Function] - # @PURPOSE: List repositories visible to authenticated Gitea user. - # @PRE: server_url and pat are valid. - # @POST: Returns repository list from Gitea. - # @RETURN: List[dict] + # #region list_gitea_repositories [TYPE Function] + # @BRIEF List repositories visible to authenticated Gitea user. + # @PRE server_url and pat are valid. + # @POST Returns repository list from Gitea. + # @RETURN List[dict] async def list_gitea_repositories(self, server_url: str, pat: str) -> List[dict]: payload = await self._gitea_request("GET", server_url, pat, "/user/repos?limit=100&page=1") if not isinstance(payload, list): return [] return payload - # [/DEF:list_gitea_repositories:Function] + # #endregion list_gitea_repositories - # [DEF:create_gitea_repository:Function] - # @PURPOSE: Create repository in Gitea for authenticated user. - # @PRE: name is non-empty and PAT has repo creation permission. - # @POST: Returns created repository payload. - # @RETURN: dict + # #region create_gitea_repository [TYPE Function] + # @BRIEF Create repository in Gitea for authenticated user. + # @PRE name is non-empty and PAT has repo creation permission. + # @POST Returns created repository payload. + # @RETURN dict async def create_gitea_repository( self, server_url: str, pat: str, name: str, private: bool = True, description: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = "main", @@ -153,23 +150,23 @@ class GitServiceGiteaMixin: if not isinstance(created, dict): raise HTTPException(status_code=500, detail="Unexpected Gitea response while creating repository") return created - # [/DEF:create_gitea_repository:Function] + # #endregion create_gitea_repository - # [DEF:delete_gitea_repository:Function] - # @PURPOSE: Delete repository in Gitea. - # @PRE: owner and repo_name are non-empty. - # @POST: Repository deleted on Gitea server. + # #region delete_gitea_repository [TYPE Function] + # @BRIEF Delete repository in Gitea. + # @PRE owner and repo_name are non-empty. + # @POST Repository deleted on Gitea server. async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None: if not owner or not repo_name: raise HTTPException(status_code=400, detail="owner and repo_name are required") await self._gitea_request("DELETE", server_url, pat, f"/repos/{owner}/{repo_name}") - # [/DEF:delete_gitea_repository:Function] + # #endregion delete_gitea_repository - # [DEF:_gitea_branch_exists:Function] - # @PURPOSE: Check whether a branch exists in Gitea repository. - # @PRE: owner/repo/branch are non-empty. - # @POST: Returns True when branch exists, False when 404. - # @RETURN: bool + # #region _gitea_branch_exists [TYPE Function] + # @BRIEF Check whether a branch exists in Gitea repository. + # @PRE owner/repo/branch are non-empty. + # @POST Returns True when branch exists, False when 404. + # @RETURN bool async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool: if not owner or not repo or not branch: return False @@ -181,13 +178,13 @@ class GitServiceGiteaMixin: if exc.status_code == 404: return False raise - # [/DEF:_gitea_branch_exists:Function] + # #endregion _gitea_branch_exists - # [DEF:_build_gitea_pr_404_detail:Function] - # @PURPOSE: Build actionable error detail for Gitea PR 404 responses. - # @PRE: owner/repo/from_branch/to_branch are provided. - # @POST: Returns specific branch-missing message when detected. - # @RETURN: Optional[str] + # #region _build_gitea_pr_404_detail [TYPE Function] + # @BRIEF Build actionable error detail for Gitea PR 404 responses. + # @PRE owner/repo/from_branch/to_branch are provided. + # @POST Returns specific branch-missing message when detected. + # @RETURN Optional[str] async def _build_gitea_pr_404_detail( self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str, ) -> Optional[str]: @@ -202,13 +199,13 @@ class GitServiceGiteaMixin: if not target_exists: return f"Gitea branch not found: target branch '{to_branch}' in {owner}/{repo}" return None - # [/DEF:_build_gitea_pr_404_detail:Function] + # #endregion _build_gitea_pr_404_detail - # [DEF:create_gitea_pull_request:Function] - # @PURPOSE: Create pull request in Gitea. - # @PRE: Config and remote URL are valid. - # @POST: Returns normalized PR metadata. - # @RETURN: Dict[str, Any] + # #region create_gitea_pull_request [TYPE Function] + # @BRIEF Create pull request in Gitea. + # @PRE Config and remote URL are valid. + # @POST Returns normalized PR metadata. + # @RETURN Dict[str, Any] async def create_gitea_pull_request( self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str, title: str, description: Optional[str] = None, @@ -257,6 +254,6 @@ class GitServiceGiteaMixin: "url": data.get("html_url") or data.get("url"), "status": data.get("state") or "open", } - # [/DEF:create_gitea_pull_request:Function] -# [/DEF:GitServiceGiteaMixin:Class] -# [/DEF:GitServiceGiteaMixin:Module] + # #endregion create_gitea_pull_request +# #endregion GitServiceGiteaMixin +# #endregion GitServiceGiteaMixin diff --git a/backend/src/services/git/_merge.py b/backend/src/services/git/_merge.py index abc7379a..0d307eb5 100644 --- a/backend/src/services/git/_merge.py +++ b/backend/src/services/git/_merge.py @@ -1,9 +1,7 @@ -# [DEF:GitServiceMergeMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, merge, conflicts, resolution, promote -# @PURPOSE: Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote. -# @RELATION: USED_BY -> [GitService] +# #region GitServiceMergeMixin [C:3] [TYPE Module] [SEMANTICS git, merge, conflicts, resolution, promote] +# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] import os from pathlib import Path @@ -15,12 +13,11 @@ from fastapi import HTTPException from src.core.logger import logger, belief_scope -# [DEF:GitServiceMergeMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing merge operations for GitService. +# #region GitServiceMergeMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing merge operations for GitService. class GitServiceMergeMixin: - # [DEF:_read_blob_text:Function] - # @PURPOSE: Read text from a Git blob. + # #region _read_blob_text [TYPE Function] + # @BRIEF Read text from a Git blob. def _read_blob_text(self, blob: Blob) -> str: with belief_scope("GitService._read_blob_text"): if blob is None: @@ -29,20 +26,20 @@ class GitServiceMergeMixin: return blob.data_stream.read().decode("utf-8", errors="replace") except Exception: return "" - # [/DEF:_read_blob_text:Function] + # #endregion _read_blob_text - # [DEF:_get_unmerged_file_paths:Function] - # @PURPOSE: List files with merge conflicts. + # #region _get_unmerged_file_paths [TYPE Function] + # @BRIEF List files with merge conflicts. def _get_unmerged_file_paths(self, repo: Repo) -> List[str]: with belief_scope("GitService._get_unmerged_file_paths"): try: return sorted(list(repo.index.unmerged_blobs().keys())) except Exception: return [] - # [/DEF:_get_unmerged_file_paths:Function] + # #endregion _get_unmerged_file_paths - # [DEF:_build_unfinished_merge_payload:Function] - # @PURPOSE: Build payload for unfinished merge state. + # #region _build_unfinished_merge_payload [TYPE Function] + # @BRIEF Build payload for unfinished merge state. def _build_unfinished_merge_payload(self, repo: Repo) -> Dict[str, Any]: with belief_scope("GitService._build_unfinished_merge_payload"): merge_head_path = os.path.join(repo.git_dir, "MERGE_HEAD") @@ -86,10 +83,10 @@ class GitServiceMergeMixin: ], "manual_commands": ["git status", "git add ", 'git commit -m "resolve merge conflicts"', "git merge --abort"], } - # [/DEF:_build_unfinished_merge_payload:Function] + # #endregion _build_unfinished_merge_payload - # [DEF:get_merge_status:Function] - # @PURPOSE: Get current merge status for a dashboard repository. + # #region get_merge_status [TYPE Function] + # @BRIEF Get current merge status for a dashboard repository. def get_merge_status(self, dashboard_id: int) -> Dict[str, Any]: with belief_scope("GitService.get_merge_status"): repo = self.get_repo(dashboard_id) @@ -119,10 +116,10 @@ class GitServiceMergeMixin: "merge_message_preview": payload["merge_message_preview"], "conflicts_count": int(payload.get("conflicts_count") or 0), } - # [/DEF:get_merge_status:Function] + # #endregion get_merge_status - # [DEF:get_merge_conflicts:Function] - # @PURPOSE: List all files with conflicts and their contents. + # #region get_merge_conflicts [TYPE Function] + # @BRIEF List all files with conflicts and their contents. def get_merge_conflicts(self, dashboard_id: int) -> List[Dict[str, Any]]: with belief_scope("GitService.get_merge_conflicts"): repo = self.get_repo(dashboard_id) @@ -142,10 +139,10 @@ class GitServiceMergeMixin: "theirs": self._read_blob_text(theirs_blob) if theirs_blob else "", }) return sorted(conflicts, key=lambda item: item["file_path"]) - # [/DEF:get_merge_conflicts:Function] + # #endregion get_merge_conflicts - # [DEF:resolve_merge_conflicts:Function] - # @PURPOSE: Resolve conflicts using specified strategy. + # #region resolve_merge_conflicts [TYPE Function] + # @BRIEF Resolve conflicts using specified strategy. def resolve_merge_conflicts(self, dashboard_id: int, resolutions: List[Dict[str, Any]]) -> List[str]: with belief_scope("GitService.resolve_merge_conflicts"): repo = self.get_repo(dashboard_id) @@ -175,10 +172,10 @@ class GitServiceMergeMixin: repo.git.add(file_path) resolved_files.append(file_path) return resolved_files - # [/DEF:resolve_merge_conflicts:Function] + # #endregion resolve_merge_conflicts - # [DEF:abort_merge:Function] - # @PURPOSE: Abort ongoing merge. + # #region abort_merge [TYPE Function] + # @BRIEF Abort ongoing merge. def abort_merge(self, dashboard_id: int) -> Dict[str, Any]: with belief_scope("GitService.abort_merge"): repo = self.get_repo(dashboard_id) @@ -191,10 +188,10 @@ class GitServiceMergeMixin: return {"status": "no_merge_in_progress"} raise HTTPException(status_code=409, detail=f"Cannot abort merge: {details}") return {"status": "aborted"} - # [/DEF:abort_merge:Function] + # #endregion abort_merge - # [DEF:continue_merge:Function] - # @PURPOSE: Finalize merge after conflict resolution. + # #region continue_merge [TYPE Function] + # @BRIEF Finalize merge after conflict resolution. def continue_merge(self, dashboard_id: int, message: Optional[str] = None) -> Dict[str, Any]: with belief_scope("GitService.continue_merge"): repo = self.get_repo(dashboard_id) @@ -226,13 +223,13 @@ class GitServiceMergeMixin: except Exception: commit_hash = "" return {"status": "committed", "commit_hash": commit_hash} - # [/DEF:continue_merge:Function] + # #endregion continue_merge - # [DEF:promote_direct_merge:Function] - # @PURPOSE: Perform direct merge between branches in local repo and push target branch. - # @PRE: Repository exists and both branches are valid. - # @POST: Target branch contains merged changes from source branch. - # @RETURN: Dict[str, Any] + # #region promote_direct_merge [TYPE Function] + # @BRIEF Perform direct merge between branches in local repo and push target branch. + # @PRE Repository exists and both branches are valid. + # @POST Target branch contains merged changes from source branch. + # @RETURN Dict[str, Any] def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> Dict[str, Any]: with belief_scope("GitService.promote_direct_merge"): if not from_branch or not to_branch: @@ -273,6 +270,6 @@ class GitServiceMergeMixin: raise HTTPException(status_code=409, detail=f"Merge conflict during direct promote: {message}") raise HTTPException(status_code=500, detail=f"Direct promote failed: {message}") return {"mode": "direct", "from_branch": source, "to_branch": target, "status": "merged"} - # [/DEF:promote_direct_merge:Function] -# [/DEF:GitServiceMergeMixin:Class] -# [/DEF:GitServiceMergeMixin:Module] + # #endregion promote_direct_merge +# #endregion GitServiceMergeMixin +# #endregion GitServiceMergeMixin diff --git a/backend/src/services/git/_remote_providers.py b/backend/src/services/git/_remote_providers.py index 89846653..4723a347 100644 --- a/backend/src/services/git/_remote_providers.py +++ b/backend/src/services/git/_remote_providers.py @@ -1,9 +1,7 @@ -# [DEF:GitServiceRemoteMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, github, gitlab, api, provider, repository, pull_request, merge_request -# @PURPOSE: GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. -# @RELATION: USED_BY -> [GitService] +# #region GitServiceRemoteMixin [C:3] [TYPE Module] [SEMANTICS git, github, gitlab, api, provider, repository, pull_request, merge_request] +# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] import httpx from typing import Any, Dict, Optional @@ -16,11 +14,11 @@ from src.core.logger import logger, belief_scope # @COMPLEXITY: 3 # @PURPOSE: Mixin providing GitHub API operations for GitService. class GitServiceGithubMixin: - # [DEF:create_github_repository:Function] - # @PURPOSE: Create repository in GitHub or GitHub Enterprise. - # @PRE: PAT has repository create permission. - # @POST: Returns created repository payload. - # @RETURN: dict + # #region create_github_repository [TYPE Function] + # @BRIEF Create repository in GitHub or GitHub Enterprise. + # @PRE PAT has repository create permission. + # @POST Returns created repository payload. + # @RETURN dict async def create_github_repository( self, server_url: str, pat: str, name: str, private: bool = True, description: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = "main", @@ -54,13 +52,13 @@ class GitServiceGithubMixin: pass raise HTTPException(status_code=response.status_code, detail=f"GitHub API error: {detail}") return response.json() - # [/DEF:create_github_repository:Function] + # #endregion create_github_repository - # [DEF:create_github_pull_request:Function] - # @PURPOSE: Create pull request in GitHub or GitHub Enterprise. - # @PRE: Config and remote URL are valid. - # @POST: Returns normalized PR metadata. - # @RETURN: Dict[str, Any] + # #region create_github_pull_request [TYPE Function] + # @BRIEF Create pull request in GitHub or GitHub Enterprise. + # @PRE Config and remote URL are valid. + # @POST Returns normalized PR metadata. + # @RETURN Dict[str, Any] async def create_github_pull_request( self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str, title: str, description: Optional[str] = None, draft: bool = False, @@ -94,18 +92,17 @@ class GitServiceGithubMixin: raise HTTPException(status_code=response.status_code, detail=f"GitHub API error: {detail}") data = response.json() return {"id": data.get("number") or data.get("id"), "url": data.get("html_url") or data.get("url"), "status": data.get("state") or "open"} - # [/DEF:create_github_pull_request:Function] + # #endregion create_github_pull_request -# [DEF:GitServiceGitlabMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing GitLab API operations for GitService. +# #region GitServiceGitlabMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing GitLab API operations for GitService. class GitServiceGitlabMixin: - # [DEF:create_gitlab_repository:Function] - # @PURPOSE: Create repository(project) in GitLab. - # @PRE: PAT has api scope. - # @POST: Returns created repository payload. - # @RETURN: dict + # #region create_gitlab_repository [TYPE Function] + # @BRIEF Create repository(project) in GitLab. + # @PRE PAT has api scope. + # @POST Returns created repository payload. + # @RETURN dict async def create_gitlab_repository( self, server_url: str, pat: str, name: str, private: bool = True, description: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = "main", @@ -145,13 +142,13 @@ class GitServiceGitlabMixin: if "full_name" not in data: data["full_name"] = data.get("path_with_namespace") or data.get("name") return data - # [/DEF:create_gitlab_repository:Function] + # #endregion create_gitlab_repository - # [DEF:create_gitlab_merge_request:Function] - # @PURPOSE: Create merge request in GitLab. - # @PRE: Config and remote URL are valid. - # @POST: Returns normalized MR metadata. - # @RETURN: Dict[str, Any] + # #region create_gitlab_merge_request [TYPE Function] + # @BRIEF Create merge request in GitLab. + # @PRE Config and remote URL are valid. + # @POST Returns normalized MR metadata. + # @RETURN Dict[str, Any] async def create_gitlab_merge_request( self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str, title: str, description: Optional[str] = None, remove_source_branch: bool = False, @@ -181,6 +178,6 @@ class GitServiceGitlabMixin: raise HTTPException(status_code=response.status_code, detail=f"GitLab API error: {detail}") data = response.json() return {"id": data.get("iid") or data.get("id"), "url": data.get("web_url") or data.get("url"), "status": data.get("state") or "opened"} - # [/DEF:create_gitlab_merge_request:Function] -# [/DEF:GitServiceGitlabMixin:Class] -# [/DEF:GitServiceRemoteMixin:Module] + # #endregion create_gitlab_merge_request +# #endregion GitServiceGitlabMixin +# #endregion GitServiceRemoteMixin diff --git a/backend/src/services/git/_status.py b/backend/src/services/git/_status.py index dff3801f..1356abe2 100644 --- a/backend/src/services/git/_status.py +++ b/backend/src/services/git/_status.py @@ -1,9 +1,7 @@ -# [DEF:GitServiceStatusMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, status, diff, history, porcelain -# @PURPOSE: Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues. -# @RELATION: USED_BY -> [GitService] +# #region GitServiceStatusMixin [C:3] [TYPE Module] [SEMANTICS git, status, diff, history, porcelain] +# @BRIEF Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] from typing import Any, Dict, List from datetime import datetime @@ -14,16 +12,14 @@ from src.core.logger import belief_scope log = MarkerLogger("GitStatus") -# [DEF:GitServiceStatusMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing repository status, diff, and commit history for GitService. +# #region GitServiceStatusMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing repository status, diff, and commit history for GitService. class GitServiceStatusMixin: - # [DEF:_parse_status_porcelain:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists. - # @PRE: `repo` is an open GitPython Repo instance. - # @POST: Returns (staged, modified, untracked) tuple of file path lists. - # @RATIONALE: Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally + # #region _parse_status_porcelain [C:2] [TYPE Function] + # @BRIEF Parse git status --porcelain output into staged, modified, and untracked file lists. + # @PRE `repo` is an open GitPython Repo instance. + # @POST Returns (staged, modified, untracked) tuple of file path lists. + # @RATIONALE Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally # call git diff --cached, a flag unsupported in some Git environments (exit 129). # Using git status --porcelain is self-contained and avoids the --cached flag entirely. def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]: @@ -56,13 +52,13 @@ class GitServiceStatusMixin: if path not in modified: modified.append(path) return staged, modified, untracked - # [/DEF:_parse_status_porcelain:Function] + # #endregion _parse_status_porcelain - # [DEF:get_status:Function] - # @PURPOSE: Get current repository status (dirty files, untracked, etc.) - # @PRE: Repository for dashboard_id exists. - # @POST: Returns a dictionary representing the Git status. - # @RETURN: dict + # #region get_status [TYPE Function] + # @BRIEF Get current repository status (dirty files, untracked, etc.) + # @PRE Repository for dashboard_id exists. + # @POST Returns a dictionary representing the Git status. + # @RETURN dict def get_status(self, dashboard_id: int) -> dict: with belief_scope("GitService.get_status"): repo = self.get_repo(dashboard_id) @@ -116,10 +112,10 @@ class GitServiceStatusMixin: "is_diverged": is_diverged, "sync_state": sync_state, } - # [/DEF:get_status:Function] + # #endregion get_status - # [DEF:get_diff:Function] - # @PURPOSE: Generate diff for a file or the whole repository. + # #region get_diff [TYPE Function] + # @BRIEF Generate diff for a file or the whole repository. # @PARAM: file_path (str) - Optional specific file. # @PARAM: staged (bool) - Whether to show staged changes. # @PRE: Repository for dashboard_id exists. @@ -134,10 +130,10 @@ class GitServiceStatusMixin: if file_path: return repo.git.diff(*diff_args, "--", file_path) return repo.git.diff(*diff_args) - # [/DEF:get_diff:Function] + # #endregion get_diff - # [DEF:get_commit_history:Function] - # @PURPOSE: Retrieve commit history for a repository. + # #region get_commit_history [TYPE Function] + # @BRIEF Retrieve commit history for a repository. # @PARAM: limit (int) - Max number of commits to return. # @PRE: Repository for dashboard_id exists. # @POST: Returns a list of dictionaries for each commit in history. @@ -162,6 +158,5 @@ class GitServiceStatusMixin: log.explore(f"Could not retrieve commit history for dashboard {dashboard_id}: {e}", error=str(e)) return [] return commits - # [/DEF:get_commit_history:Function] -# [/DEF:GitServiceStatusMixin:Class] -# [/DEF:GitServiceStatusMixin:Module] + # #endregion get_commit_history +# #endregion GitServiceStatusMixin diff --git a/backend/src/services/git/_sync.py b/backend/src/services/git/_sync.py index 6a3c7975..99d7ed05 100644 --- a/backend/src/services/git/_sync.py +++ b/backend/src/services/git/_sync.py @@ -1,10 +1,8 @@ -# [DEF:GitServiceSyncMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, push, pull, sync, remote -# @PURPOSE: Push and pull operations for GitService with origin host auto-alignment. -# @RELATION: USED_BY -> [GitService] -# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin] +# #region GitServiceSyncMixin [C:3] [TYPE Module] [SEMANTICS git, push, pull, sync, remote] +# @BRIEF Push and pull operations for GitService with origin host auto-alignment. +# @LAYER Infra +# @RELATION USED_BY -> [GitService] +# @RELATION DEPENDS_ON -> [GitServiceUrlMixin] import os from typing import Optional @@ -19,14 +17,13 @@ log = MarkerLogger("GitSync") from src.models.git import GitRepository, GitServerConfig -# [DEF:GitServiceSyncMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Mixin providing push and pull operations with origin host alignment. +# #region GitServiceSyncMixin [C:3] [TYPE Class] +# @BRIEF Mixin providing push and pull operations with origin host alignment. class GitServiceSyncMixin: - # [DEF:push_changes:Function] - # @PURPOSE: Push local commits to remote. - # @PRE: Repository exists and has an 'origin' remote. - # @POST: Local branch commits are pushed to origin. + # #region push_changes [TYPE Function] + # @BRIEF Push local commits to remote. + # @PRE Repository exists and has an 'origin' remote. + # @POST Local branch commits are pushed to origin. def push_changes(self, dashboard_id: int): with belief_scope("GitService.push_changes"): repo = self.get_repo(dashboard_id) @@ -115,12 +112,12 @@ class GitServiceSyncMixin: except Exception as e: log.explore("Failed to push changes", error=str(e)) raise HTTPException(status_code=500, detail=f"Git push failed: {str(e)}") - # [/DEF:push_changes:Function] + # #endregion push_changes - # [DEF:pull_changes:Function] - # @PURPOSE: Pull changes from remote. - # @PRE: Repository exists and has an 'origin' remote. - # @POST: Changes from origin are pulled and merged into the active branch. + # #region pull_changes [TYPE Function] + # @BRIEF Pull changes from remote. + # @PRE Repository exists and has an 'origin' remote. + # @POST Changes from origin are pulled and merged into the active branch. def pull_changes(self, dashboard_id: int): with belief_scope("GitService.pull_changes"): repo = self.get_repo(dashboard_id) @@ -189,6 +186,6 @@ class GitServiceSyncMixin: except Exception as e: log.explore("Failed to pull changes", error=str(e)) raise HTTPException(status_code=500, detail=f"Git pull failed: {str(e)}") - # [/DEF:pull_changes:Function] -# [/DEF:GitServiceSyncMixin:Class] -# [/DEF:GitServiceSyncMixin:Module] + # #endregion pull_changes +# #endregion GitServiceSyncMixin +# #endregion GitServiceSyncMixin diff --git a/backend/src/services/git/_url.py b/backend/src/services/git/_url.py index 3875087e..70ad1e75 100644 --- a/backend/src/services/git/_url.py +++ b/backend/src/services/git/_url.py @@ -1,11 +1,9 @@ -# [DEF:GitServiceUrlMixin:Module] -# @COMPLEXITY: 3 -# @LAYER: Infra -# @SEMANTICS: git, url, parsing, normalization, host_alignment -# @PURPOSE: URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs. -# @RELATION: USED_BY -> [GitServiceSyncMixin] -# @RELATION: USED_BY -> [GitServiceGiteaMixin] -# @RELATION: USED_BY -> [GitServiceRemoteMixin] +# #region GitServiceUrlMixin [C:3] [TYPE Module] [SEMANTICS git, url, parsing, normalization, host_alignment] +# @BRIEF URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs. +# @LAYER Infra +# @RELATION USED_BY -> [GitServiceSyncMixin] +# @RELATION USED_BY -> [GitServiceGiteaMixin] +# @RELATION USED_BY -> [GitServiceRemoteMixin] import os from typing import Any, Dict, List, Optional @@ -18,15 +16,14 @@ from src.core.logger import belief_scope log = MarkerLogger("GitUrl") from src.models.git import GitRepository, GitServerConfig -# [DEF:GitServiceUrlMixin:Class] -# @COMPLEXITY: 3 -# @PURPOSE: URL helper methods for GitService — extract host, strip credentials, replace host, align origin, parse remote identity, derive server URL, normalize server URL. +# #region GitServiceUrlMixin [C:3] [TYPE Class] +# @BRIEF URL helper methods for GitService — extract host, strip credentials, replace host, align origin, parse remote identity, derive server URL, normalize server URL. class GitServiceUrlMixin: - # [DEF:_extract_http_host:Function] - # @PURPOSE: Extract normalized host[:port] from HTTP(S) URL. - # @PRE: url_value may be empty. - # @POST: Returns lowercase host token or None. - # @RETURN: Optional[str] + # #region _extract_http_host [TYPE Function] + # @BRIEF Extract normalized host[:port] from HTTP(S) URL. + # @PRE url_value may be empty. + # @POST Returns lowercase host token or None. + # @RETURN Optional[str] def _extract_http_host(self, url_value: Optional[str]) -> Optional[str]: normalized = str(url_value or "").strip() if not normalized: @@ -43,13 +40,13 @@ class GitServiceUrlMixin: if parsed.port: return f"{host.lower()}:{parsed.port}" return host.lower() - # [/DEF:_extract_http_host:Function] + # #endregion _extract_http_host - # [DEF:_strip_url_credentials:Function] - # @PURPOSE: Remove credentials from URL while preserving scheme/host/path. - # @PRE: url_value may contain credentials. - # @POST: Returns URL without username/password. - # @RETURN: str + # #region _strip_url_credentials [TYPE Function] + # @BRIEF Remove credentials from URL while preserving scheme/host/path. + # @PRE url_value may contain credentials. + # @POST Returns URL without username/password. + # @RETURN str def _strip_url_credentials(self, url_value: str) -> str: normalized = str(url_value or "").strip() if not normalized: @@ -64,13 +61,13 @@ class GitServiceUrlMixin: if parsed.port: host = f"{host}:{parsed.port}" return parsed._replace(netloc=host).geturl() - # [/DEF:_strip_url_credentials:Function] + # #endregion _strip_url_credentials - # [DEF:_replace_host_in_url:Function] - # @PURPOSE: Replace source URL host with host from configured server URL. - # @PRE: source_url and config_url are HTTP(S) URLs. - # @POST: Returns source URL with updated host (credentials preserved) or None. - # @RETURN: Optional[str] + # #region _replace_host_in_url [TYPE Function] + # @BRIEF Replace source URL host with host from configured server URL. + # @PRE source_url and config_url are HTTP(S) URLs. + # @POST Returns source URL with updated host (credentials preserved) or None. + # @RETURN Optional[str] def _replace_host_in_url(self, source_url: Optional[str], config_url: Optional[str]) -> Optional[str]: source = str(source_url or "").strip() config = str(config_url or "").strip() @@ -96,13 +93,13 @@ class GitServiceUrlMixin: auth_part = f"{auth_part}@" new_netloc = f"{auth_part}{target_host}" return source_parsed._replace(netloc=new_netloc).geturl() - # [/DEF:_replace_host_in_url:Function] + # #endregion _replace_host_in_url - # [DEF:_align_origin_host_with_config:Function] - # @PURPOSE: Auto-align local origin host to configured Git server host when they drift. - # @PRE: origin remote exists. - # @POST: origin URL host updated and DB binding normalized when mismatch detected. - # @RETURN: Optional[str] + # #region _align_origin_host_with_config [TYPE Function] + # @BRIEF Auto-align local origin host to configured Git server host when they drift. + # @PRE origin remote exists. + # @POST origin URL host updated and DB binding normalized when mismatch detected. + # @RETURN Optional[str] def _align_origin_host_with_config( self, dashboard_id: int, @@ -143,13 +140,13 @@ class GitServiceUrlMixin: except Exception as e: log.explore(f"Failed to persist aligned remote_url for dashboard {dashboard_id}: {e}", error=str(e)) return aligned_url - # [/DEF:_align_origin_host_with_config:Function] + # #endregion _align_origin_host_with_config - # [DEF:_parse_remote_repo_identity:Function] - # @PURPOSE: Parse owner/repo from remote URL for Git server API operations. - # @PRE: remote_url is a valid git URL. - # @POST: Returns owner/repo tokens. - # @RETURN: Dict[str, str] + # #region _parse_remote_repo_identity [TYPE Function] + # @BRIEF Parse owner/repo from remote URL for Git server API operations. + # @PRE remote_url is a valid git URL. + # @POST Returns owner/repo tokens. + # @RETURN Dict[str, str] def _parse_remote_repo_identity(self, remote_url: str) -> Dict[str, str]: normalized = str(remote_url or "").strip() if not normalized: @@ -169,13 +166,13 @@ class GitServiceUrlMixin: repo = parts[-1] namespace = "/".join(parts[:-1]) return {"owner": owner, "repo": repo, "namespace": namespace, "full_name": f"{namespace}/{repo}"} - # [/DEF:_parse_remote_repo_identity:Function] + # #endregion _parse_remote_repo_identity - # [DEF:_derive_server_url_from_remote:Function] - # @PURPOSE: Build API base URL from remote repository URL without credentials. - # @PRE: remote_url may be any git URL. - # @POST: Returns normalized http(s) base URL or None when derivation is impossible. - # @RETURN: Optional[str] + # #region _derive_server_url_from_remote [TYPE Function] + # @BRIEF Build API base URL from remote repository URL without credentials. + # @PRE remote_url may be any git URL. + # @POST Returns normalized http(s) base URL or None when derivation is impossible. + # @RETURN Optional[str] def _derive_server_url_from_remote(self, remote_url: str) -> Optional[str]: normalized = str(remote_url or "").strip() if not normalized or normalized.startswith("git@"): @@ -189,18 +186,18 @@ class GitServiceUrlMixin: if parsed.port: netloc = f"{netloc}:{parsed.port}" return f"{parsed.scheme}://{netloc}".rstrip("/") - # [/DEF:_derive_server_url_from_remote:Function] + # #endregion _derive_server_url_from_remote - # [DEF:_normalize_git_server_url:Function] - # @PURPOSE: Normalize Git server URL for provider API calls. - # @PRE: raw_url is non-empty. - # @POST: Returns URL without trailing slash. - # @RETURN: str + # #region _normalize_git_server_url [TYPE Function] + # @BRIEF Normalize Git server URL for provider API calls. + # @PRE raw_url is non-empty. + # @POST Returns URL without trailing slash. + # @RETURN str def _normalize_git_server_url(self, raw_url: str) -> str: normalized = (raw_url or "").strip() if not normalized: raise HTTPException(status_code=400, detail="Git server URL is required") return normalized.rstrip("/") - # [/DEF:_normalize_git_server_url:Function] -# [/DEF:GitServiceUrlMixin:Class] -# [/DEF:GitServiceUrlMixin:Module] + # #endregion _normalize_git_server_url +# #endregion GitServiceUrlMixin +# #endregion GitServiceUrlMixin diff --git a/backend/src/services/git_service.py b/backend/src/services/git_service.py index 8a8084a5..92328cc6 100644 --- a/backend/src/services/git_service.py +++ b/backend/src/services/git_service.py @@ -1,6 +1,5 @@ -# [DEF:git_service:Module:Tombstone] -# @COMPLEXITY: 1 -# @PURPOSE: Re-export shim — GitService has been decomposed into services/git/ package. +# #region git_service [C:1] [TYPE Module:Tombstone] +# @BRIEF Re-export shim — GitService has been decomposed into services/git/ package. # All consumers continue to import from this same path without changes. # @RELATION: REDIRECTS_TO -> [GitServiceModule] # @RATIONALE: Monolithic GitService (2101 lines) was decomposed into 8 domain-specific mixins @@ -8,4 +7,4 @@ # the original import path for all 5 consumers. # @REJECTED: Breaking 5 consumer imports to remove this shim — unacceptable migration cost. from src.services.git import GitService # noqa: F401 -# [/DEF:git_service:Module:Tombstone] +# #endregion git_service diff --git a/backend/src/services/health_service.py b/backend/src/services/health_service.py index d52bc53f..5dcfe244 100644 --- a/backend/src/services/health_service.py +++ b/backend/src/services/health_service.py @@ -1,12 +1,10 @@ -# [DEF:health_service:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: health, aggregation, dashboards -# @PURPOSE: Business logic for aggregating dashboard health status from validation records. -# @LAYER: Domain/Service -# @RELATION: [DEPENDS_ON] ->[ValidationRecord] -# @RELATION: [DEPENDS_ON] ->[SupersetClient] -# @RELATION: [DEPENDS_ON] ->[TaskCleanupService] -# @RELATION: [DEPENDS_ON] ->[TaskManager] +# #region health_service [C:3] [TYPE Module] [SEMANTICS health, aggregation, dashboards] +# @BRIEF Business logic for aggregating dashboard health status from validation records. +# @LAYER Domain/Service +# @RELATION DEPENDS_ON -> [ValidationRecord] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [TaskCleanupService] +# @RELATION DEPENDS_ON -> [TaskManager] from typing import List, Dict, Any, Optional, Tuple, cast import time @@ -25,19 +23,18 @@ def _empty_dashboard_meta() -> Dict[str, Optional[str]]: return cast(Dict[str, Optional[str]], {"slug": None, "title": None}) -# [DEF:HealthService:Class] -# @COMPLEXITY: 4 -# @PURPOSE: Aggregate latest dashboard validation state and manage persisted health report lifecycle. -# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager. -# @POST: Exposes health summary aggregation and validation report deletion operations. -# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators. -# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool] -# @RELATION: [DEPENDS_ON] ->[ValidationRecord] -# @RELATION: [DEPENDS_ON] ->[DashboardHealthItem] -# @RELATION: [DEPENDS_ON] ->[HealthSummaryResponse] -# @RELATION: [DEPENDS_ON] ->[SupersetClient] -# @RELATION: [DEPENDS_ON] ->[TaskCleanupService] -# @RELATION: [DEPENDS_ON] ->[TaskManager] +# #region HealthService [C:4] [TYPE Class] +# @BRIEF Aggregate latest dashboard validation state and manage persisted health report lifecycle. +# @PRE Service is constructed with a live SQLAlchemy session and optional config manager. +# @POST Exposes health summary aggregation and validation report deletion operations. +# @SIDE_EFFECT Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators. +# @DATA_CONTRACT Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool] +# @RELATION DEPENDS_ON -> [ValidationRecord] +# @RELATION DEPENDS_ON -> [DashboardHealthItem] +# @RELATION DEPENDS_ON -> [HealthSummaryResponse] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [TaskCleanupService] +# @RELATION DEPENDS_ON -> [TaskManager] class HealthService: _dashboard_summary_cache: Dict[ str, Tuple[float, Dict[str, Dict[str, Optional[str]]]] @@ -48,31 +45,29 @@ class HealthService: @PURPOSE: Service for managing and querying dashboard health data. """ - # [DEF:HealthService_init:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution. - # @PRE: db is a valid SQLAlchemy session. - # @POST: Service is ready to aggregate summaries and delete health reports. - # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache. - # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService] - # @RELATION: [BINDS_TO] ->[HealthService] + # #region HealthService_init [C:3] [TYPE Function] + # @BRIEF Initialize health service with DB session and optional config access for dashboard metadata resolution. + # @PRE db is a valid SQLAlchemy session. + # @POST Service is ready to aggregate summaries and delete health reports. + # @SIDE_EFFECT Initializes per-instance dashboard metadata cache. + # @DATA_CONTRACT Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService] + # @RELATION BINDS_TO -> [HealthService] def __init__(self, db: Session, config_manager=None): self.db = db self.config_manager = config_manager self._dashboard_meta_cache: Dict[Tuple[str, str], Dict[str, Optional[str]]] = {} - # [/DEF:HealthService_init:Function] + # #endregion HealthService_init - # [DEF:_prime_dashboard_meta_cache:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment. - # @PRE: records may contain mixed numeric and slug dashboard identifiers. - # @POST: Numeric dashboard ids for known environments are cached when discoverable. - # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment. - # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None] - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] - # @RELATION: [DEPENDS_ON] ->[ConfigManager] - # @RELATION: [DEPENDS_ON] ->[SupersetClient] + # #region _prime_dashboard_meta_cache [C:3] [TYPE Function] + # @BRIEF Warm dashboard slug/title cache with one Superset list fetch per environment. + # @PRE records may contain mixed numeric and slug dashboard identifiers. + # @POST Numeric dashboard ids for known environments are cached when discoverable. + # @SIDE_EFFECT May call Superset dashboard list API once per referenced environment. + # @DATA_CONTRACT Input[records: List[ValidationRecord]] -> Output[None] + # @RELATION DEPENDS_ON -> [ValidationRecord] + # @RELATION DEPENDS_ON -> [ConfigManager] + # @RELATION DEPENDS_ON -> [SupersetClient] def _prime_dashboard_meta_cache(self, records: List[ValidationRecord]) -> None: if not self.config_manager or not records: return @@ -153,14 +148,13 @@ class HealthService: _empty_dashboard_meta() ) - # [/DEF:_prime_dashboard_meta_cache:Function] + # #endregion _prime_dashboard_meta_cache - # [DEF:_resolve_dashboard_meta:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record. - # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty. - # @POST: Returns dict with `slug` and `title` keys, using cache when possible. - # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids. + # #region _resolve_dashboard_meta [C:1] [TYPE Function] + # @BRIEF Resolve slug/title for a dashboard referenced by persisted validation record. + # @PRE dashboard_id may be numeric or slug-like; environment_id may be empty. + # @POST Returns dict with `slug` and `title` keys, using cache when possible. + # @SIDE_EFFECT Writes default cache entries for unresolved numeric dashboard ids. def _resolve_dashboard_meta( self, dashboard_id: str, environment_id: Optional[str] ) -> Dict[str, Optional[str]]: @@ -184,17 +178,16 @@ class HealthService: self._dashboard_meta_cache[cache_key] = meta return meta - # [/DEF:_resolve_dashboard_meta:Function] + # #endregion _resolve_dashboard_meta - # [DEF:get_health_summary:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title. - # @PRE: environment_id may be omitted to aggregate across all environments. - # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard. - # @SIDE_EFFECT: May call Superset API to resolve dashboard metadata. - # @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse] - # @RELATION: [CALLS] ->[_prime_dashboard_meta_cache] - # @RELATION: [CALLS] ->[_resolve_dashboard_meta] + # #region get_health_summary [C:3] [TYPE Function] + # @BRIEF Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title. + # @PRE environment_id may be omitted to aggregate across all environments. + # @POST Returns HealthSummaryResponse with counts and latest record row per dashboard. + # @SIDE_EFFECT May call Superset API to resolve dashboard metadata. + # @DATA_CONTRACT Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse] + # @RELATION CALLS -> [_prime_dashboard_meta_cache] + # @RELATION CALLS -> [_resolve_dashboard_meta] async def get_health_summary( self, environment_id: str = "" ) -> HealthSummaryResponse: @@ -288,18 +281,17 @@ class HealthService: unknown_count=unknown_count, ) - # [/DEF:get_health_summary:Function] + # #endregion get_health_summary - # [DEF:delete_validation_report:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts. - # @PRE: record_id is a validation record identifier. - # @POST: Returns True only when a matching record was deleted. - # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence. - # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool] - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] - # @RELATION: [DEPENDS_ON] ->[TaskManager] - # @RELATION: [DEPENDS_ON] ->[TaskCleanupService] + # #region delete_validation_report [C:3] [TYPE Function] + # @BRIEF Delete one persisted health report and optionally clean linked task/log artifacts. + # @PRE record_id is a validation record identifier. + # @POST Returns True only when a matching record was deleted. + # @SIDE_EFFECT Deletes DB rows, optional screenshot file, and optional task/log persistence. + # @DATA_CONTRACT Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool] + # @RELATION DEPENDS_ON -> [ValidationRecord] + # @RELATION DEPENDS_ON -> [TaskManager] + # @RELATION DEPENDS_ON -> [TaskCleanupService] def delete_validation_report( self, record_id: str, task_manager: Optional[TaskManager] = None ) -> bool: @@ -376,9 +368,7 @@ class HealthService: return True - # [/DEF:delete_validation_report:Function] + # #endregion delete_validation_report -# [/DEF:HealthService:Class] - -# [/DEF:health_service:Module] +# #endregion HealthService diff --git a/backend/src/services/llm_prompt_templates.py b/backend/src/services/llm_prompt_templates.py index 593703ca..38933268 100644 --- a/backend/src/services/llm_prompt_templates.py +++ b/backend/src/services/llm_prompt_templates.py @@ -1,10 +1,8 @@ -# [DEF:llm_prompt_templates:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: llm, prompts, templates, settings -# @PURPOSE: Provide default LLM prompt templates and normalization helpers for runtime usage. -# @LAYER: Domain -# @RELATION: DEPENDS_ON ->[backend.src.core.config_manager:Function] -# @INVARIANT: All required prompt template keys are always present after normalization. +# #region llm_prompt_templates [C:2] [TYPE Module] [SEMANTICS llm, prompts, templates, settings] +# @BRIEF Provide default LLM prompt templates and normalization helpers for runtime usage. +# @LAYER Domain +# @INVARIANT All required prompt template keys are always present after normalization. +# @RELATION DEPENDS_ON -> [backend.src.core.config_manager:Function] from __future__ import annotations @@ -12,9 +10,8 @@ from copy import deepcopy from typing import Dict, Any, Optional -# [DEF:DEFAULT_LLM_PROMPTS:Constant] -# @COMPLEXITY: 2 -# @PURPOSE: Default prompt templates used by documentation, dashboard validation, and git commit generation. +# #region DEFAULT_LLM_PROMPTS [C:2] [TYPE Constant] +# @BRIEF Default prompt templates used by documentation, dashboard validation, and git commit generation. DEFAULT_LLM_PROMPTS: Dict[str, str] = { "dashboard_validation_prompt": ( "Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\n\n" @@ -58,36 +55,33 @@ DEFAULT_LLM_PROMPTS: Dict[str, str] = { "Commit Message:" ), } -# [/DEF:DEFAULT_LLM_PROMPTS:Constant] +# #endregion DEFAULT_LLM_PROMPTS -# [DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant] -# @COMPLEXITY: 2 -# @PURPOSE: Default provider binding per task domain. +# #region DEFAULT_LLM_PROVIDER_BINDINGS [C:2] [TYPE Constant] +# @BRIEF Default provider binding per task domain. DEFAULT_LLM_PROVIDER_BINDINGS: Dict[str, str] = { "dashboard_validation": "", "documentation": "", "git_commit": "", } -# [/DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant] +# #endregion DEFAULT_LLM_PROVIDER_BINDINGS -# [DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant] -# @COMPLEXITY: 2 -# @PURPOSE: Default planner settings for assistant chat intent model/provider resolution. +# #region DEFAULT_LLM_ASSISTANT_SETTINGS [C:2] [TYPE Constant] +# @BRIEF Default planner settings for assistant chat intent model/provider resolution. DEFAULT_LLM_ASSISTANT_SETTINGS: Dict[str, str] = { "assistant_planner_provider": "", "assistant_planner_model": "", } -# [/DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant] +# #endregion DEFAULT_LLM_ASSISTANT_SETTINGS -# [DEF:normalize_llm_settings:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Ensure llm settings contain stable schema with prompts section and default templates. -# @PRE: llm_settings is dictionary-like value or None. -# @POST: Returned dict contains prompts with all required template keys. -# @RELATION: DEPENDS_ON -> LLMProviderService +# #region normalize_llm_settings [C:3] [TYPE Function] +# @BRIEF Ensure llm settings contain stable schema with prompts section and default templates. +# @PRE llm_settings is dictionary-like value or None. +# @POST Returned dict contains prompts with all required template keys. +# @RELATION DEPENDS_ON -> [LLMProviderService] def normalize_llm_settings(llm_settings: Any) -> Dict[str, Any]: normalized: Dict[str, Any] = { "providers": [], @@ -124,15 +118,14 @@ def normalize_llm_settings(llm_settings: Any) -> Dict[str, Any]: value = normalized.get(key, default_value) normalized[key] = value.strip() if isinstance(value, str) else default_value return normalized -# [/DEF:normalize_llm_settings:Function] +# #endregion normalize_llm_settings -# [DEF:is_multimodal_model:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Heuristically determine whether model supports image input required for dashboard validation. -# @PRE: model_name may be empty or mixed-case. -# @POST: Returns True when model likely supports multimodal input. -# @RELATION: DEPENDS_ON -> LLMProviderService +# #region is_multimodal_model [C:3] [TYPE Function] +# @BRIEF Heuristically determine whether model supports image input required for dashboard validation. +# @PRE model_name may be empty or mixed-case. +# @POST Returns True when model likely supports multimodal input. +# @RELATION DEPENDS_ON -> [LLMProviderService] def is_multimodal_model(model_name: str, provider_type: Optional[str] = None) -> bool: token = (model_name or "").strip().lower() if not token: @@ -167,15 +160,14 @@ def is_multimodal_model(model_name: str, provider_type: Optional[str] = None) -> if any(marker in token for marker in multimodal_markers): return True return False -# [/DEF:is_multimodal_model:Function] +# #endregion is_multimodal_model -# [DEF:resolve_bound_provider_id:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Resolve provider id configured for a task binding with fallback to default provider. -# @PRE: llm_settings is normalized or raw dict from config. -# @POST: Returns configured provider id or fallback id/empty string when not defined. -# @RELATION: DEPENDS_ON -> LLMProviderService +# #region resolve_bound_provider_id [C:3] [TYPE Function] +# @BRIEF Resolve provider id configured for a task binding with fallback to default provider. +# @PRE llm_settings is normalized or raw dict from config. +# @POST Returns configured provider id or fallback id/empty string when not defined. +# @RELATION DEPENDS_ON -> [LLMProviderService] def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str: normalized = normalize_llm_settings(llm_settings) bindings = normalized.get("provider_bindings", {}) @@ -184,21 +176,20 @@ def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str: return bound.strip() default_provider = normalized.get("default_provider", "") return default_provider.strip() if isinstance(default_provider, str) else "" -# [/DEF:resolve_bound_provider_id:Function] +# #endregion resolve_bound_provider_id -# [DEF:render_prompt:Function] -# @COMPLEXITY: 3 -# @PURPOSE: Render prompt template using deterministic placeholder replacement with graceful fallback. -# @PRE: template is a string and variables values are already stringifiable. -# @POST: Returns rendered prompt text with known placeholders substituted. -# @RELATION: DEPENDS_ON -> LLMProviderService +# #region render_prompt [C:3] [TYPE Function] +# @BRIEF Render prompt template using deterministic placeholder replacement with graceful fallback. +# @PRE template is a string and variables values are already stringifiable. +# @POST Returns rendered prompt text with known placeholders substituted. +# @RELATION DEPENDS_ON -> [LLMProviderService] def render_prompt(template: str, variables: Dict[str, Any]) -> str: rendered = template for key, value in variables.items(): rendered = rendered.replace("{" + key + "}", str(value)) return rendered -# [/DEF:render_prompt:Function] +# #endregion render_prompt -# [/DEF:llm_prompt_templates:Module] +# #endregion llm_prompt_templates diff --git a/backend/src/services/llm_provider.py b/backend/src/services/llm_provider.py index f9f44187..bd899a7c 100644 --- a/backend/src/services/llm_provider.py +++ b/backend/src/services/llm_provider.py @@ -1,11 +1,9 @@ -# [DEF:llm_provider:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: service, llm, provider, encryption -# @PURPOSE: Service for managing LLM provider configurations with encrypted API keys. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[LLMProvider] -# @RELATION: [DEPENDS_ON] ->[EncryptionManager] -# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig] +# #region llm_provider [C:3] [TYPE Module] [SEMANTICS service, llm, provider, encryption] +# @BRIEF Service for managing LLM provider configurations with encrypted API keys. +# @LAYER Domain +# @RELATION DEPENDS_ON -> [LLMProvider] +# @RELATION DEPENDS_ON -> [EncryptionManager] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] from typing import List, Optional, TYPE_CHECKING from sqlalchemy.orm import Session from ..models.llm import LLMProvider @@ -19,15 +17,14 @@ if TYPE_CHECKING: MASKED_API_KEY_PLACEHOLDER = "********" -# [DEF:_require_fernet_key:Function] -# @COMPLEXITY: 5 -# @PURPOSE: Load and validate the Fernet key used for secret encryption. -# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key. -# @POST: Returns validated key bytes ready for Fernet initialization. -# @RELATION: DEPENDS_ON ->[backend.src.core.logger:Function] -# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration. -# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes] -# @INVARIANT: Encryption initialization never falls back to a hardcoded secret. +# #region _require_fernet_key [C:5] [TYPE Function] +# @BRIEF Load and validate the Fernet key used for secret encryption. +# @PRE ENCRYPTION_KEY environment variable must be set to a valid Fernet key. +# @POST Returns validated key bytes ready for Fernet initialization. +# @SIDE_EFFECT Emits belief-state logs for missing or invalid encryption configuration. +# @DATA_CONTRACT Input[ENCRYPTION_KEY:str] -> Output[bytes] +# @INVARIANT Encryption initialization never falls back to a hardcoded secret. +# @RELATION DEPENDS_ON -> [backend.src.core.logger:Function] def _require_fernet_key() -> bytes: with belief_scope("_require_fernet_key"): raw_key = os.getenv("ENCRYPTION_KEY", "").strip() @@ -50,18 +47,17 @@ def _require_fernet_key() -> bytes: return key -# [/DEF:_require_fernet_key:Function] +# #endregion _require_fernet_key -# [DEF:EncryptionManager:Class] -# @COMPLEXITY: 5 -# @PURPOSE: Handles encryption and decryption of sensitive data like API keys. -# @RELATION: [CALLS] ->[_require_fernet_key] -# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation. -# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets. -# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment. -# @DATA_CONTRACT: Input[str] -> Output[str] -# @INVARIANT: Uses only a validated secret key from environment. +# #region EncryptionManager [C:5] [TYPE Class] +# @BRIEF Handles encryption and decryption of sensitive data like API keys. +# @PRE ENCRYPTION_KEY is configured with a valid Fernet key before instantiation. +# @POST Manager exposes reversible encrypt/decrypt operations for persisted secrets. +# @SIDE_EFFECT Initializes Fernet cryptography state from process environment. +# @DATA_CONTRACT Input[str] -> Output[str] +# @INVARIANT Uses only a validated secret key from environment. +# @RELATION CALLS -> [_require_fernet_key] # # @TEST_CONTRACT: EncryptionManagerModel -> # { @@ -75,92 +71,88 @@ def _require_fernet_key() -> bytes: # @TEST_EDGE: empty_string_encryption -> {"data": ""} # @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption] class EncryptionManager: - # [DEF:EncryptionManager_init:Function] - # @PURPOSE: Initialize the encryption manager with a Fernet key. - # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key. - # @POST: Fernet instance ready for encryption/decryption. + # #region EncryptionManager_init [TYPE Function] + # @BRIEF Initialize the encryption manager with a Fernet key. + # @PRE ENCRYPTION_KEY env var must be set to a valid Fernet key. + # @POST Fernet instance ready for encryption/decryption. def __init__(self): self.key = _require_fernet_key() self.fernet = Fernet(self.key) - # [/DEF:EncryptionManager_init:Function] + # #endregion EncryptionManager_init - # [DEF:encrypt:Function] - # @PURPOSE: Encrypt a plaintext string. - # @PRE: data must be a non-empty string. - # @POST: Returns encrypted string. + # #region encrypt [TYPE Function] + # @BRIEF Encrypt a plaintext string. + # @PRE data must be a non-empty string. + # @POST Returns encrypted string. def encrypt(self, data: str) -> str: with belief_scope("encrypt"): return self.fernet.encrypt(data.encode()).decode() - # [/DEF:encrypt:Function] + # #endregion encrypt - # [DEF:decrypt:Function] - # @PURPOSE: Decrypt an encrypted string. - # @PRE: encrypted_data must be a valid Fernet-encrypted string. - # @POST: Returns original plaintext string. + # #region decrypt [TYPE Function] + # @BRIEF Decrypt an encrypted string. + # @PRE encrypted_data must be a valid Fernet-encrypted string. + # @POST Returns original plaintext string. def decrypt(self, encrypted_data: str) -> str: with belief_scope("decrypt"): return self.fernet.decrypt(encrypted_data.encode()).decode() - # [/DEF:decrypt:Function] + # #endregion decrypt -# [/DEF:EncryptionManager:Class] +# #endregion EncryptionManager -# [DEF:LLMProviderService:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Service to manage LLM provider lifecycle. -# @RELATION: [DEPENDS_ON] ->[LLMProvider] -# @RELATION: [DEPENDS_ON] ->[EncryptionManager] -# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig] +# #region LLMProviderService [C:3] [TYPE Class] +# @BRIEF Service to manage LLM provider lifecycle. +# @RELATION DEPENDS_ON -> [LLMProvider] +# @RELATION DEPENDS_ON -> [EncryptionManager] +# @RELATION DEPENDS_ON -> [LLMProviderConfig] class LLMProviderService: - # [DEF:LLMProviderService_init:Function] - # @PURPOSE: Initialize the service with database session. - # @PRE: db must be a valid SQLAlchemy Session. - # @POST: Service ready for provider operations. - # @RELATION: [DEPENDS_ON] ->[EncryptionManager] + # #region LLMProviderService_init [TYPE Function] + # @BRIEF Initialize the service with database session. + # @PRE db must be a valid SQLAlchemy Session. + # @POST Service ready for provider operations. + # @RELATION DEPENDS_ON -> [EncryptionManager] def __init__(self, db: Session): self.db = db self.encryption = EncryptionManager() - # [/DEF:LLMProviderService_init:Function] + # #endregion LLMProviderService_init - # [DEF:get_all_providers:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Returns all configured LLM providers. - # @PRE: Database connection must be active. - # @POST: Returns list of all LLMProvider records. - # @RELATION: [DEPENDS_ON] ->[LLMProvider] + # #region get_all_providers [C:3] [TYPE Function] + # @BRIEF Returns all configured LLM providers. + # @PRE Database connection must be active. + # @POST Returns list of all LLMProvider records. + # @RELATION DEPENDS_ON -> [LLMProvider] def get_all_providers(self) -> List[LLMProvider]: with belief_scope("get_all_providers"): return self.db.query(LLMProvider).all() - # [/DEF:get_all_providers:Function] + # #endregion get_all_providers - # [DEF:get_provider:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Returns a single LLM provider by ID. - # @PRE: provider_id must be a valid string. - # @POST: Returns LLMProvider or None if not found. - # @RELATION: [DEPENDS_ON] ->[LLMProvider] + # #region get_provider [C:3] [TYPE Function] + # @BRIEF Returns a single LLM provider by ID. + # @PRE provider_id must be a valid string. + # @POST Returns LLMProvider or None if not found. + # @RELATION DEPENDS_ON -> [LLMProvider] def get_provider(self, provider_id: str) -> Optional[LLMProvider]: with belief_scope("get_provider"): return ( self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first() ) - # [/DEF:get_provider:Function] + # #endregion get_provider - # [DEF:create_provider:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Creates a new LLM provider with encrypted API key. - # @PRE: config must contain valid provider configuration. - # @POST: New provider created and persisted to database. - # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig] - # @RELATION: [DEPENDS_ON] ->[LLMProvider] - # @RELATION: [CALLS] ->[encrypt] + # #region create_provider [C:3] [TYPE Function] + # @BRIEF Creates a new LLM provider with encrypted API key. + # @PRE config must contain valid provider configuration. + # @POST New provider created and persisted to database. + # @RELATION DEPENDS_ON -> [LLMProviderConfig] + # @RELATION DEPENDS_ON -> [LLMProvider] + # @RELATION CALLS -> [encrypt] def create_provider(self, config: "LLMProviderConfig") -> LLMProvider: with belief_scope("create_provider"): encrypted_key = self.encryption.encrypt(config.api_key) @@ -177,16 +169,15 @@ class LLMProviderService: self.db.refresh(db_provider) return db_provider - # [/DEF:create_provider:Function] + # #endregion create_provider - # [DEF:update_provider:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Updates an existing LLM provider. - # @PRE: provider_id must exist, config must be valid. - # @POST: Provider updated and persisted to database. - # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig] - # @RELATION: [DEPENDS_ON] ->[LLMProvider] - # @RELATION: [CALLS] ->[encrypt] + # #region update_provider [C:3] [TYPE Function] + # @BRIEF Updates an existing LLM provider. + # @PRE provider_id must exist, config must be valid. + # @POST Provider updated and persisted to database. + # @RELATION DEPENDS_ON -> [LLMProviderConfig] + # @RELATION DEPENDS_ON -> [LLMProvider] + # @RELATION CALLS -> [encrypt] def update_provider( self, provider_id: str, config: "LLMProviderConfig" ) -> Optional[LLMProvider]: @@ -212,14 +203,13 @@ class LLMProviderService: self.db.refresh(db_provider) return db_provider - # [/DEF:update_provider:Function] + # #endregion update_provider - # [DEF:delete_provider:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Deletes an LLM provider. - # @PRE: provider_id must exist. - # @POST: Provider removed from database. - # @RELATION: [DEPENDS_ON] ->[LLMProvider] + # #region delete_provider [C:3] [TYPE Function] + # @BRIEF Deletes an LLM provider. + # @PRE provider_id must exist. + # @POST Provider removed from database. + # @RELATION DEPENDS_ON -> [LLMProvider] def delete_provider(self, provider_id: str) -> bool: with belief_scope("delete_provider"): db_provider = self.get_provider(provider_id) @@ -229,15 +219,14 @@ class LLMProviderService: self.db.commit() return True - # [/DEF:delete_provider:Function] + # #endregion delete_provider - # [DEF:get_decrypted_api_key:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Returns the decrypted API key for a provider. - # @PRE: provider_id must exist with valid encrypted key. - # @POST: Returns decrypted API key or None on failure. - # @RELATION: [DEPENDS_ON] ->[LLMProvider] - # @RELATION: [CALLS] ->[decrypt] + # #region get_decrypted_api_key [C:3] [TYPE Function] + # @BRIEF Returns the decrypted API key for a provider. + # @PRE provider_id must exist with valid encrypted key. + # @POST Returns decrypted API key or None on failure. + # @RELATION DEPENDS_ON -> [LLMProvider] + # @RELATION CALLS -> [decrypt] def get_decrypted_api_key(self, provider_id: str) -> Optional[str]: with belief_scope("get_decrypted_api_key"): db_provider = self.get_provider(provider_id) @@ -262,9 +251,7 @@ class LLMProviderService: logger.error(f"[get_decrypted_api_key] Decryption failed: {str(e)}") return None - # [/DEF:get_decrypted_api_key:Function] + # #endregion get_decrypted_api_key -# [/DEF:LLMProviderService:Class] - -# [/DEF:llm_provider:Module] +# #endregion LLMProviderService diff --git a/backend/src/services/mapping_service.py b/backend/src/services/mapping_service.py index ccdf9798..8a7a9d0e 100644 --- a/backend/src/services/mapping_service.py +++ b/backend/src/services/mapping_service.py @@ -1,17 +1,15 @@ -# [DEF:mapping_service:Module] +# #region mapping_service [C:3] [TYPE Module] [SEMANTICS service, mapping, fuzzy-matching, superset] +# @BRIEF Orchestrates database fetching and fuzzy matching suggestions. +# @LAYER Service +# @PRE source/target environment identifiers are provided by caller. +# @POST Exposes stateless mapping suggestion orchestration over configured environments. +# @SIDE_EFFECT Performs remote metadata reads through Superset API clients. +# @DATA_CONTRACT Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]] +# @INVARIANT Suggestions are based on database names. +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [suggest_mappings] # -# @SEMANTICS: service, mapping, fuzzy-matching, superset -# @PURPOSE: Orchestrates database fetching and fuzzy matching suggestions. -# @COMPLEXITY: 3 -# @LAYER: Service -# @PRE: source/target environment identifiers are provided by caller. -# @POST: Exposes stateless mapping suggestion orchestration over configured environments. -# @SIDE_EFFECT: Performs remote metadata reads through Superset API clients. -# @DATA_CONTRACT: Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]] -# @RELATION: DEPENDS_ON -> SupersetClient -# @RELATION: DEPENDS_ON -> suggest_mappings # -# @INVARIANT: Suggestions are based on database names. # [SECTION: IMPORTS] from typing import List, Dict @@ -21,20 +19,18 @@ from ..core.utils.matching import suggest_mappings # [/SECTION] -# [DEF:MappingService:Class] -# @PURPOSE: Service for handling database mapping logic. -# @COMPLEXITY: 3 -# @PRE: config_manager exposes get_environments() with environment objects containing id. -# @POST: Provides client resolution and mapping suggestion methods. -# @SIDE_EFFECT: Instantiates Superset clients and performs upstream metadata reads. -# @DATA_CONTRACT: Input[config_manager] -> Output[List[Dict]] -# @RELATION: DEPENDS_ON -> SupersetClient -# @RELATION: DEPENDS_ON -> suggest_mappings +# #region MappingService [C:3] [TYPE Class] +# @BRIEF Service for handling database mapping logic. +# @PRE config_manager exposes get_environments() with environment objects containing id. +# @POST Provides client resolution and mapping suggestion methods. +# @SIDE_EFFECT Instantiates Superset clients and performs upstream metadata reads. +# @DATA_CONTRACT Input[config_manager] -> Output[List[Dict]] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [suggest_mappings] class MappingService: - # [DEF:init:Function] - # @PURPOSE: Initializes the mapping service with a config manager. - # @COMPLEXITY: 3 - # @PRE: config_manager is provided. + # #region init [C:3] [TYPE Function] + # @BRIEF Initializes the mapping service with a config manager. + # @PRE config_manager is provided. # @PARAM: config_manager (ConfigManager) - The configuration manager. # @POST: Service is initialized. # @RELATION: DEPENDS_ON -> MappingService @@ -42,11 +38,10 @@ class MappingService: with belief_scope("MappingService.__init__"): self.config_manager = config_manager - # [/DEF:init:Function] + # #endregion init - # [DEF:_get_client:Function] - # @PURPOSE: Helper to get an initialized SupersetClient for an environment. - # @COMPLEXITY: 3 + # #region _get_client [C:3] [TYPE Function] + # @BRIEF Helper to get an initialized SupersetClient for an environment. # @PARAM: env_id (str) - The ID of the environment. # @PRE: environment must exist in config. # @POST: Returns an initialized SupersetClient. @@ -61,11 +56,10 @@ class MappingService: return SupersetClient(env) - # [/DEF:_get_client:Function] + # #endregion _get_client - # [DEF:get_suggestions:Function] - # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions. - # @COMPLEXITY: 3 + # #region get_suggestions [C:3] [TYPE Function] + # @BRIEF Fetches databases from both environments and returns fuzzy matching suggestions. # @PARAM: source_env_id (str) - Source environment ID. # @PARAM: target_env_id (str) - Target environment ID. # @PRE: Both environments must be accessible. @@ -91,9 +85,7 @@ class MappingService: return suggest_mappings(source_dbs, target_dbs) - # [/DEF:get_suggestions:Function] + # #endregion get_suggestions -# [/DEF:MappingService:Class] - -# [/DEF:mapping_service:Module] +# #endregion MappingService diff --git a/backend/src/services/notifications/__init__.py b/backend/src/services/notifications/__init__.py index a45791ee..9ab5c749 100644 --- a/backend/src/services/notifications/__init__.py +++ b/backend/src/services/notifications/__init__.py @@ -1,4 +1,4 @@ -# [DEF:notifications:Package] -# @PURPOSE: Notification service package root. -# @RELATION: EXPORTS ->[NotificationService:Class] -# [/DEF:notifications:Package] +# #region notifications [TYPE Package] +# @BRIEF Notification service package root. +# @RELATION EXPORTS -> [NotificationService:Class] +# #endregion notifications diff --git a/backend/src/services/notifications/__tests__/test_notification_service.py b/backend/src/services/notifications/__tests__/test_notification_service.py index e4be5663..6fd80c86 100644 --- a/backend/src/services/notifications/__tests__/test_notification_service.py +++ b/backend/src/services/notifications/__tests__/test_notification_service.py @@ -1,7 +1,6 @@ -# [DEF:test_notification_service:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Unit tests for NotificationService routing and dispatch logic. -# @RELATION: TESTS ->[NotificationService:Class] +# #region test_notification_service [C:2] [TYPE Module] +# @BRIEF Unit tests for NotificationService routing and dispatch logic. +# @RELATION TESTS -> [NotificationService:Class] import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -118,4 +117,4 @@ async def test_dispatch_report_calls_providers(service, mock_db): service._providers["TELEGRAM"].send.assert_called_once() service._providers["SMTP"].send.assert_called_once() -# [/DEF:test_notification_service:Module] \ No newline at end of file +# #endregion test_notification_service diff --git a/backend/src/services/notifications/providers.py b/backend/src/services/notifications/providers.py index 70c0baf0..e426c731 100644 --- a/backend/src/services/notifications/providers.py +++ b/backend/src/services/notifications/providers.py @@ -1,22 +1,20 @@ -# [DEF:providers:Module] +# #region providers [C:5] [TYPE Module] [SEMANTICS notifications, providers, smtp, slack, telegram, abstraction] +# @BRIEF Defines abstract base and concrete implementations for external notification delivery. +# @LAYER Infra +# @PRE Provider configuration dictionaries are supplied by trusted configuration sources. +# @POST Each provider exposes async send contract returning boolean delivery outcome. +# @SIDE_EFFECT Performs outbound network I/O to SMTP or HTTP endpoints. +# @DATA_CONTRACT Input[target, subject, body, context?] -> Output[bool] +# @INVARIANT Concrete providers preserve boolean send contract and swallow transport exceptions into False. +# @INVARIANT Providers must be stateless and resilient to network failures. +# @INVARIANT Sensitive credentials must be handled via encrypted config. +# @RELATION DEPENDED_ON_BY -> [NotificationService] +# @RELATION DEPENDS_ON -> [NotificationProvider] +# @RELATION DEPENDS_ON -> [SMTPProvider] +# @RELATION DEPENDS_ON -> [TelegramProvider] +# @RELATION DEPENDS_ON -> [SlackProvider] # -# @COMPLEXITY: 5 -# @SEMANTICS: notifications, providers, smtp, slack, telegram, abstraction -# @PURPOSE: Defines abstract base and concrete implementations for external notification delivery. -# @RELATION: [DEPENDED_ON_BY] ->[NotificationService] -# @RELATION: [DEPENDS_ON] ->[NotificationProvider] -# @RELATION: [DEPENDS_ON] ->[SMTPProvider] -# @RELATION: [DEPENDS_ON] ->[TelegramProvider] -# @RELATION: [DEPENDS_ON] ->[SlackProvider] -# @LAYER: Infra -# @PRE: Provider configuration dictionaries are supplied by trusted configuration sources. -# @POST: Each provider exposes async send contract returning boolean delivery outcome. -# @SIDE_EFFECT: Performs outbound network I/O to SMTP or HTTP endpoints. -# @DATA_CONTRACT: Input[target, subject, body, context?] -> Output[bool] -# @INVARIANT: Concrete providers preserve boolean send contract and swallow transport exceptions into False. # -# @INVARIANT: Providers must be stateless and resilient to network failures. -# @INVARIANT: Sensitive credentials must be handled via encrypted config. from abc import ABC, abstractmethod from typing import Any, Dict, Optional @@ -28,12 +26,11 @@ from email.mime.multipart import MIMEMultipart from ...core.logger import logger -# [DEF:NotificationProvider:Class] -# @COMPLEXITY: 2 -# @PURPOSE: Abstract base class for all notification providers. -# @RELATION: [DEPENDED_ON_BY] ->[SMTPProvider] -# @RELATION: [DEPENDED_ON_BY] ->[TelegramProvider] -# @RELATION: [DEPENDED_ON_BY] ->[SlackProvider] +# #region NotificationProvider [C:2] [TYPE Class] +# @BRIEF Abstract base class for all notification providers. +# @RELATION DEPENDED_ON_BY -> [SMTPProvider] +# @RELATION DEPENDED_ON_BY -> [TelegramProvider] +# @RELATION DEPENDED_ON_BY -> [SlackProvider] class NotificationProvider(ABC): @abstractmethod async def send( @@ -54,13 +51,12 @@ class NotificationProvider(ABC): pass -# [/DEF:NotificationProvider:Class] +# #endregion NotificationProvider -# [DEF:SMTPProvider:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Delivers notifications via SMTP. -# @RELATION: [INHERITS] ->[NotificationProvider] +# #region SMTPProvider [C:3] [TYPE Class] +# @BRIEF Delivers notifications via SMTP. +# @RELATION INHERITS -> [NotificationProvider] class SMTPProvider(NotificationProvider): def __init__(self, config: Dict[str, Any]): self.host = config.get("host") @@ -99,13 +95,12 @@ class SMTPProvider(NotificationProvider): return False -# [/DEF:SMTPProvider:Class] +# #endregion SMTPProvider -# [DEF:TelegramProvider:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Delivers notifications via Telegram Bot API. -# @RELATION: [INHERITS] ->[NotificationProvider] +# #region TelegramProvider [C:3] [TYPE Class] +# @BRIEF Delivers notifications via Telegram Bot API. +# @RELATION INHERITS -> [NotificationProvider] class TelegramProvider(NotificationProvider): def __init__(self, config: Dict[str, Any]): self.bot_token = config.get("bot_token") @@ -139,13 +134,12 @@ class TelegramProvider(NotificationProvider): return False -# [/DEF:TelegramProvider:Class] +# #endregion TelegramProvider -# [DEF:SlackProvider:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Delivers notifications via Slack Webhooks or API. -# @RELATION: [INHERITS] ->[NotificationProvider] +# #region SlackProvider [C:3] [TYPE Class] +# @BRIEF Delivers notifications via Slack Webhooks or API. +# @RELATION INHERITS -> [NotificationProvider] class SlackProvider(NotificationProvider): def __init__(self, config: Dict[str, Any]): self.webhook_url = config.get("webhook_url") @@ -172,6 +166,6 @@ class SlackProvider(NotificationProvider): return False -# [/DEF:SlackProvider:Class] +# #endregion SlackProvider -# [/DEF:providers:Module] +# #endregion providers diff --git a/backend/src/services/notifications/service.py b/backend/src/services/notifications/service.py index 4be669d9..ff4aa0be 100644 --- a/backend/src/services/notifications/service.py +++ b/backend/src/services/notifications/service.py @@ -1,22 +1,20 @@ -# [DEF:service:Module] +# #region service [C:5] [TYPE Module] [SEMANTICS notifications, service, routing, dispatch, background-tasks] +# @BRIEF Orchestrates notification routing based on user preferences and policy context. +# @LAYER Domain +# @PRE channel_config is loaded +# @POST Notification dispatched via configured providers +# @SIDE_EFFECT Sends notifications via configured providers +# @DATA_CONTRACT NotificationChannelConfig -> NotificationRecipient +# @INVARIANT NotificationService maintains singleton pattern for per-channel notifications +# @RELATION DEPENDS_ON -> [NotificationProvider] +# @RELATION DEPENDS_ON -> [SMTPProvider] +# @RELATION DEPENDS_ON -> [TelegramProvider] +# @RELATION DEPENDS_ON -> [SlackProvider] +# @RELATION DEPENDS_ON -> [ValidationRecord] +# @RELATION DEPENDS_ON -> [ValidationPolicy] +# @RELATION DEPENDS_ON -> [UserDashboardPreference] # -# @COMPLEXITY: 5 -# @SEMANTICS: notifications, service, routing, dispatch, background-tasks -# @PURPOSE: Orchestrates notification routing based on user preferences and policy context. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[NotificationProvider] -# @RELATION: [DEPENDS_ON] ->[SMTPProvider] -# @RELATION: [DEPENDS_ON] ->[TelegramProvider] -# @RELATION: [DEPENDS_ON] ->[SlackProvider] -# @RELATION: [DEPENDS_ON] ->[ValidationRecord] -# @RELATION: [DEPENDS_ON] ->[ValidationPolicy] -# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference] # -# @INVARIANT: NotificationService maintains singleton pattern for per-channel notifications -# @DATA_CONTRACT: NotificationChannelConfig -> NotificationRecipient -# @PRE: channel_config is loaded -# @POST: Notification dispatched via configured providers -# @SIDE_EFFECT: Sends notifications via configured providers from typing import Any, Dict, List, Optional from fastapi import BackgroundTasks @@ -34,37 +32,34 @@ from .providers import ( ) -# [DEF:NotificationService:Class] -# @PURPOSE: Routes validation reports to appropriate users and channels. -# @COMPLEXITY: 4 -# @RELATION: [DEPENDS_ON] ->[NotificationProvider] -# @RELATION: [DEPENDS_ON] ->[ValidationRecord] -# @RELATION: [DEPENDS_ON] ->[ValidationPolicy] -# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference] -# @PRE: Service receives a live DB session and configuration manager with notification payload settings. -# @POST: Service can resolve targets and dispatch provider sends without mutating validation records. -# @SIDE_EFFECT: Reads notification configuration, queries user preferences, and dispatches provider I/O. -# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None] +# #region NotificationService [C:4] [TYPE Class] +# @BRIEF Routes validation reports to appropriate users and channels. +# @PRE Service receives a live DB session and configuration manager with notification payload settings. +# @POST Service can resolve targets and dispatch provider sends without mutating validation records. +# @SIDE_EFFECT Reads notification configuration, queries user preferences, and dispatches provider I/O. +# @DATA_CONTRACT Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None] +# @RELATION DEPENDS_ON -> [NotificationProvider] +# @RELATION DEPENDS_ON -> [ValidationRecord] +# @RELATION DEPENDS_ON -> [ValidationPolicy] +# @RELATION DEPENDS_ON -> [UserDashboardPreference] class NotificationService: - # [DEF:NotificationService_init:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing. - # @RELATION: [BINDS_TO] ->[NotificationService] - # @RELATION: [DEPENDS_ON] ->[ValidationPolicy] + # #region NotificationService_init [C:3] [TYPE Function] + # @BRIEF Bind DB and configuration collaborators used for provider initialization and routing. + # @RELATION BINDS_TO -> [NotificationService] + # @RELATION DEPENDS_ON -> [ValidationPolicy] def __init__(self, db: Session, config_manager: ConfigManager): self.db = db self.config_manager = config_manager self._providers: Dict[str, NotificationProvider] = {} self._initialized = False - # [/DEF:NotificationService_init:Function] + # #endregion NotificationService_init - # [DEF:_initialize_providers:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Materialize configured notification channel adapters once per service lifetime. - # @RELATION: [DEPENDS_ON] ->[SMTPProvider] - # @RELATION: [DEPENDS_ON] ->[TelegramProvider] - # @RELATION: [DEPENDS_ON] ->[SlackProvider] + # #region _initialize_providers [C:3] [TYPE Function] + # @BRIEF Materialize configured notification channel adapters once per service lifetime. + # @RELATION DEPENDS_ON -> [SMTPProvider] + # @RELATION DEPENDS_ON -> [TelegramProvider] + # @RELATION DEPENDS_ON -> [SlackProvider] def _initialize_providers(self): if self._initialized: return @@ -83,19 +78,18 @@ class NotificationService: self._initialized = True - # [/DEF:_initialize_providers:Function] + # #endregion _initialize_providers - # [DEF:dispatch_report:Function] - # @COMPLEXITY: 4 - # @PURPOSE: Route one validation record to resolved owners and configured custom channels. - # @RELATION: [CALLS] ->[_initialize_providers] - # @RELATION: [CALLS] ->[_should_notify] - # @RELATION: [CALLS] ->[_resolve_targets] - # @RELATION: [CALLS] ->[_build_body] - # @PRE: record is persisted and providers can be initialized from configuration payload. - # @POST: Eligible notification sends are scheduled in background or awaited inline. - # @SIDE_EFFECT: Schedules or performs outbound provider sends and emits notification logs. - # @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None] + # #region dispatch_report [C:4] [TYPE Function] + # @BRIEF Route one validation record to resolved owners and configured custom channels. + # @PRE record is persisted and providers can be initialized from configuration payload. + # @POST Eligible notification sends are scheduled in background or awaited inline. + # @SIDE_EFFECT Schedules or performs outbound provider sends and emits notification logs. + # @DATA_CONTRACT Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None] + # @RELATION CALLS -> [_initialize_providers] + # @RELATION CALLS -> [_should_notify] + # @RELATION CALLS -> [_resolve_targets] + # @RELATION CALLS -> [_build_body] async def dispatch_report( self, record: ValidationRecord, @@ -141,13 +135,12 @@ class NotificationService: # Fallback to sync for tests or if no background_tasks provided await provider.send(recipient, subject, body) - # [/DEF:dispatch_report:Function] + # #endregion dispatch_report - # [DEF:_should_notify:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Evaluate record status against effective alert policy. - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] - # @RELATION: [DEPENDS_ON] ->[ValidationPolicy] + # #region _should_notify [C:3] [TYPE Function] + # @BRIEF Evaluate record status against effective alert policy. + # @RELATION DEPENDS_ON -> [ValidationRecord] + # @RELATION DEPENDS_ON -> [ValidationPolicy] def _should_notify( self, record: ValidationRecord, policy: Optional[ValidationPolicy] ) -> bool: @@ -159,14 +152,13 @@ class NotificationService: return record.status in ("WARN", "FAIL") return record.status == "FAIL" - # [/DEF:_should_notify:Function] + # #endregion _should_notify - # [DEF:_resolve_targets:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record. - # @RELATION: [CALLS] ->[_find_dashboard_owners] - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] - # @RELATION: [DEPENDS_ON] ->[ValidationPolicy] + # #region _resolve_targets [C:3] [TYPE Function] + # @BRIEF Resolve owner and policy-defined delivery targets for one validation record. + # @RELATION CALLS -> [_find_dashboard_owners] + # @RELATION DEPENDS_ON -> [ValidationRecord] + # @RELATION DEPENDS_ON -> [ValidationPolicy] def _resolve_targets( self, record: ValidationRecord, policy: Optional[ValidationPolicy] ) -> List[tuple]: @@ -196,13 +188,12 @@ class NotificationService: return targets - # [/DEF:_resolve_targets:Function] + # #endregion _resolve_targets - # [DEF:_find_dashboard_owners:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Load candidate dashboard owners from persisted profile preferences. - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] - # @RELATION: [DEPENDS_ON] ->[UserDashboardPreference] + # #region _find_dashboard_owners [C:3] [TYPE Function] + # @BRIEF Load candidate dashboard owners from persisted profile preferences. + # @RELATION DEPENDS_ON -> [ValidationRecord] + # @RELATION DEPENDS_ON -> [UserDashboardPreference] def _find_dashboard_owners( self, record: ValidationRecord ) -> List[UserDashboardPreference]: @@ -218,12 +209,11 @@ class NotificationService: .all() ) - # [/DEF:_find_dashboard_owners:Function] + # #endregion _find_dashboard_owners - # [DEF:_build_body:Function] - # @COMPLEXITY: 2 - # @PURPOSE: Format one validation record into provider-ready body text. - # @RELATION: [DEPENDS_ON] ->[ValidationRecord] + # #region _build_body [C:2] [TYPE Function] + # @BRIEF Format one validation record into provider-ready body text. + # @RELATION DEPENDS_ON -> [ValidationRecord] def _build_body(self, record: ValidationRecord) -> str: return ( f"Dashboard ID: {record.dashboard_id}\n" @@ -233,9 +223,7 @@ class NotificationService: f"Issues found: {len(record.issues)}" ) - # [/DEF:_build_body:Function] + # #endregion _build_body -# [/DEF:NotificationService:Class] - -# [/DEF:service:Module] +# #endregion NotificationService diff --git a/backend/src/services/profile_service.py b/backend/src/services/profile_service.py index 1a40f94b..243e8e13 100644 --- a/backend/src/services/profile_service.py +++ b/backend/src/services/profile_service.py @@ -1,17 +1,15 @@ -# [DEF:profile_service:Module] +# #region profile_service [C:5] [TYPE Module] [SEMANTICS profile, service, validation, ownership, filtering, superset, preferences] +# @BRIEF Orchestrates profile preference persistence, Superset account lookup, and deterministic actor matching. +# @LAYER Domain +# @INVARIANT Profile ID needs to unique per-user session +# @RELATION DEPENDS_ON -> [UserDashboardPreference] +# @RELATION DEPENDS_ON -> [ProfilePreferenceResponse] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [AuthRepositoryModule] +# @RELATION DEPENDS_ON -> [User] +# @RELATION DEPENDS_ON -> [sqlalchemy.orm.Session] # -# @COMPLEXITY: 5 -# @SEMANTICS: profile, service, validation, ownership, filtering, superset, preferences -# @PURPOSE: Orchestrates profile preference persistence, Superset account lookup, and deterministic actor matching. -# @LAYER: Domain -# @RELATION: DEPENDS_ON -> [UserDashboardPreference] -# @RELATION: DEPENDS_ON -> [ProfilePreferenceResponse] -# @RELATION: DEPENDS_ON -> [SupersetClient] -# @RELATION: DEPENDS_ON -> [AuthRepositoryModule] -# @RELATION: DEPENDS_ON -> [User] -# @RELATION: DEPENDS_ON -> [sqlalchemy.orm.Session] # -# @INVARIANT: Profile ID needs to unique per-user session # # @TEST_CONTRACT: ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse # @TEST_FIXTURE: valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true} @@ -53,61 +51,57 @@ SUPPORTED_START_PAGES = {"dashboards", "datasets", "reports"} SUPPORTED_DENSITIES = {"compact", "comfortable"} -# [DEF:ProfileValidationError:Class] -# @RELATION: INHERITS -> Exception -# @COMPLEXITY: 2 -# @PURPOSE: Domain validation error for profile preference update requests. +# #region ProfileValidationError [C:2] [TYPE Class] +# @BRIEF Domain validation error for profile preference update requests. +# @RELATION INHERITS -> [Exception] class ProfileValidationError(Exception): def __init__(self, errors: Sequence[str]): self.errors = list(errors) super().__init__("Profile preference validation failed") -# [/DEF:ProfileValidationError:Class] +# #endregion ProfileValidationError -# [DEF:EnvironmentNotFoundError:Class] -# @RELATION: INHERITS -> Exception -# @COMPLEXITY: 2 -# @PURPOSE: Raised when environment_id from lookup request is unknown in app configuration. +# #region EnvironmentNotFoundError [C:2] [TYPE Class] +# @BRIEF Raised when environment_id from lookup request is unknown in app configuration. +# @RELATION INHERITS -> [Exception] class EnvironmentNotFoundError(Exception): pass -# [/DEF:EnvironmentNotFoundError:Class] +# #endregion EnvironmentNotFoundError -# [DEF:ProfileAuthorizationError:Class] -# @RELATION: INHERITS -> Exception -# @COMPLEXITY: 2 -# @PURPOSE: Raised when caller attempts cross-user preference mutation. +# #region ProfileAuthorizationError [C:2] [TYPE Class] +# @BRIEF Raised when caller attempts cross-user preference mutation. +# @RELATION INHERITS -> [Exception] class ProfileAuthorizationError(Exception): pass -# [/DEF:ProfileAuthorizationError:Class] +# #endregion ProfileAuthorizationError -# [DEF:ProfileService:Class] -# @RELATION: [DEPENDS_ON] ->[sqlalchemy.orm.Session] -# @RELATION: [DEPENDS_ON] ->[AuthRepository] -# @RELATION: [DEPENDS_ON] ->[SupersetClient] -# @RELATION: [DEPENDS_ON] ->[SupersetAccountLookupAdapter] -# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference] -# @RELATION: [CALLS] ->[discover_declared_permissions] -# @COMPLEXITY: 5 -# @PURPOSE: Implements profile preference read/update flow and Superset account lookup degradation strategy. -# @PRE: Caller provides authenticated User context for external service methods. -# @POST: Preference operations remain user-scoped and return normalized profile/lookup responses. -# @SIDE_EFFECT: Writes preference records and encrypted tokens; performs external account lookups when requested. -# @DATA_CONTRACT: Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool] -# @INVARIANT: Profile data integrity maintained, cache consistency with database state +# #region ProfileService [C:5] [TYPE Class] +# @BRIEF Implements profile preference read/update flow and Superset account lookup degradation strategy. +# @PRE Caller provides authenticated User context for external service methods. +# @POST Preference operations remain user-scoped and return normalized profile/lookup responses. +# @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested. +# @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool] +# @INVARIANT Profile data integrity maintained, cache consistency with database state +# @RELATION DEPENDS_ON -> [sqlalchemy.orm.Session] +# @RELATION DEPENDS_ON -> [AuthRepository] +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter] +# @RELATION DEPENDS_ON -> [UserDashboardPreference] +# @RELATION CALLS -> [discover_declared_permissions] class ProfileService: - # [DEF:init:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Initialize service with DB session and config manager. - # @PRE: db session is active and config_manager supports get_environments(). - # @POST: Service is ready for preference persistence and lookup operations. + # #region init [TYPE Function] + # @BRIEF Initialize service with DB session and config manager. + # @PRE db session is active and config_manager supports get_environments(). + # @POST Service is ready for preference persistence and lookup operations. + # @RELATION BINDS_TO -> [ProfileService] def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None): self.db = db self.config_manager = config_manager @@ -115,13 +109,13 @@ class ProfileService: self.auth_repository = AuthRepository(db) self.encryption = EncryptionManager() - # [/DEF:init:Function] + # #endregion init - # [DEF:get_my_preference:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Return current user's persisted preference or default non-configured view. - # @PRE: current_user is authenticated. - # @POST: Returned payload belongs to current_user only. + # #region get_my_preference [TYPE Function] + # @BRIEF Return current user's persisted preference or default non-configured view. + # @PRE current_user is authenticated. + # @POST Returned payload belongs to current_user only. + # @RELATION BINDS_TO -> [ProfileService] def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse: with belief_scope( "ProfileService.get_my_preference", f"user_id={current_user.id}" @@ -146,13 +140,13 @@ class ProfileService: security=security_summary, ) - # [/DEF:get_my_preference:Function] + # #endregion get_my_preference - # [DEF:get_dashboard_filter_binding:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Return only dashboard-filter fields required by dashboards listing hot path. - # @PRE: current_user is authenticated. - # @POST: Returns normalized username and profile-default filter toggles without security summary expansion. + # #region get_dashboard_filter_binding [TYPE Function] + # @BRIEF Return only dashboard-filter fields required by dashboards listing hot path. + # @PRE current_user is authenticated. + # @POST Returns normalized username and profile-default filter toggles without security summary expansion. + # @RELATION BINDS_TO -> [ProfileService] def get_dashboard_filter_binding(self, current_user: User) -> dict: with belief_scope( "ProfileService.get_dashboard_filter_binding", f"user_id={current_user.id}" @@ -181,13 +175,13 @@ class ProfileService: ), } - # [/DEF:get_dashboard_filter_binding:Function] + # #endregion get_dashboard_filter_binding - # [DEF:update_my_preference:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Validate and persist current user's profile preference in self-scoped mode. - # @PRE: current_user is authenticated and payload is provided. - # @POST: Preference row for current_user is created/updated when validation passes. + # #region update_my_preference [TYPE Function] + # @BRIEF Validate and persist current user's profile preference in self-scoped mode. + # @PRE current_user is authenticated and payload is provided. + # @POST Preference row for current_user is created/updated when validation passes. + # @RELATION BINDS_TO -> [ProfileService] def update_my_preference( self, current_user: User, @@ -330,13 +324,13 @@ class ProfileService: security=self._build_security_summary(current_user), ) - # [/DEF:update_my_preference:Function] + # #endregion update_my_preference - # [DEF:lookup_superset_accounts:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Query Superset users in selected environment and project canonical account candidates. - # @PRE: current_user is authenticated and environment_id exists. - # @POST: Returns success payload or degraded payload with warning while preserving manual fallback. + # #region lookup_superset_accounts [TYPE Function] + # @BRIEF Query Superset users in selected environment and project canonical account candidates. + # @PRE current_user is authenticated and environment_id exists. + # @POST Returns success payload or degraded payload with warning while preserving manual fallback. + # @RELATION BINDS_TO -> [ProfileService] def lookup_superset_accounts( self, current_user: User, @@ -412,13 +406,13 @@ class ProfileService: items=[], ) - # [/DEF:lookup_superset_accounts:Function] + # #endregion lookup_superset_accounts - # [DEF:matches_dashboard_actor:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Apply trim+case-insensitive actor match across owners OR modified_by. - # @PRE: bound_username can be empty; owners may contain mixed payload. - # @POST: Returns True when normalized username matches owners or modified_by. + # #region matches_dashboard_actor [TYPE Function] + # @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by. + # @PRE bound_username can be empty; owners may contain mixed payload. + # @POST Returns True when normalized username matches owners or modified_by. + # @RELATION BINDS_TO -> [ProfileService] def matches_dashboard_actor( self, bound_username: Optional[str], @@ -438,13 +432,13 @@ class ProfileService: return True return False - # [/DEF:matches_dashboard_actor:Function] + # #endregion matches_dashboard_actor - # [DEF:_build_security_summary:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Build read-only security snapshot with role and permission badges. - # @PRE: current_user is authenticated. - # @POST: Returns deterministic security projection for profile UI. + # #region _build_security_summary [TYPE Function] + # @BRIEF Build read-only security snapshot with role and permission badges. + # @PRE current_user is authenticated. + # @POST Returns deterministic security projection for profile UI. + # @RELATION BINDS_TO -> [ProfileService] def _build_security_summary(self, current_user: User) -> ProfileSecuritySummary: role_names_set: Set[str] = set() roles = getattr(current_user, "roles", []) or [] @@ -502,13 +496,13 @@ class ProfileService: permissions=permission_states, ) - # [/DEF:_build_security_summary:Function] + # #endregion _build_security_summary - # [DEF:_collect_user_permission_pairs:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Collect effective permission tuples from current user's roles. - # @PRE: current_user can include role/permission graph. - # @POST: Returns unique normalized (resource, ACTION) tuples. + # #region _collect_user_permission_pairs [TYPE Function] + # @BRIEF Collect effective permission tuples from current user's roles. + # @PRE current_user can include role/permission graph. + # @POST Returns unique normalized (resource, ACTION) tuples. + # @RELATION BINDS_TO -> [ProfileService] def _collect_user_permission_pairs( self, current_user: User ) -> Set[Tuple[str, str]]: @@ -523,13 +517,13 @@ class ProfileService: collected.add((resource, action)) return collected - # [/DEF:_collect_user_permission_pairs:Function] + # #endregion _collect_user_permission_pairs - # [DEF:_format_permission_key:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Convert normalized permission pair to compact UI key. - # @PRE: resource and action are normalized. - # @POST: Returns user-facing badge key. + # #region _format_permission_key [TYPE Function] + # @BRIEF Convert normalized permission pair to compact UI key. + # @PRE resource and action are normalized. + # @POST Returns user-facing badge key. + # @RELATION BINDS_TO -> [ProfileService] def _format_permission_key(self, resource: str, action: str) -> str: normalized_resource = self._sanitize_text(resource) or "" normalized_action = str(action or "").strip().upper() @@ -537,13 +531,13 @@ class ProfileService: return normalized_resource return f"{normalized_resource}:{normalized_action.lower()}" - # [/DEF:_format_permission_key:Function] + # #endregion _format_permission_key - # [DEF:_to_preference_payload:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Map ORM preference row to API DTO with token metadata. - # @PRE: preference row can contain nullable optional fields. - # @POST: Returns normalized ProfilePreference object. + # #region _to_preference_payload [TYPE Function] + # @BRIEF Map ORM preference row to API DTO with token metadata. + # @PRE preference row can contain nullable optional fields. + # @POST Returns normalized ProfilePreference object. + # @RELATION BINDS_TO -> [ProfileService] def _to_preference_payload( self, preference: UserDashboardPreference, @@ -597,13 +591,13 @@ class ProfileService: updated_at=updated_at, ) - # [/DEF:_to_preference_payload:Function] + # #endregion _to_preference_payload - # [DEF:_mask_secret_value:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Build a safe display value for sensitive secrets. - # @PRE: secret may be None or plaintext. - # @POST: Returns masked representation or None. + # #region _mask_secret_value [TYPE Function] + # @BRIEF Build a safe display value for sensitive secrets. + # @PRE secret may be None or plaintext. + # @POST Returns masked representation or None. + # @RELATION BINDS_TO -> [ProfileService] def _mask_secret_value(self, secret: Optional[str]) -> Optional[str]: sanitized_secret = self._sanitize_secret(secret) if sanitized_secret is None: @@ -612,26 +606,26 @@ class ProfileService: return "***" return f"{sanitized_secret[:2]}***{sanitized_secret[-2:]}" - # [/DEF:_mask_secret_value:Function] + # #endregion _mask_secret_value - # [DEF:_sanitize_text:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize optional text into trimmed form or None. - # @PRE: value may be empty or None. - # @POST: Returns trimmed value or None. + # #region _sanitize_text [TYPE Function] + # @BRIEF Normalize optional text into trimmed form or None. + # @PRE value may be empty or None. + # @POST Returns trimmed value or None. + # @RELATION BINDS_TO -> [ProfileService] def _sanitize_text(self, value: Optional[str]) -> Optional[str]: normalized = str(value or "").strip() if not normalized: return None return normalized - # [/DEF:_sanitize_text:Function] + # #endregion _sanitize_text - # [DEF:_sanitize_secret:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize secret input into trimmed form or None. - # @PRE: value may be None or blank. - # @POST: Returns trimmed secret or None. + # #region _sanitize_secret [TYPE Function] + # @BRIEF Normalize secret input into trimmed form or None. + # @PRE value may be None or blank. + # @POST Returns trimmed secret or None. + # @RELATION BINDS_TO -> [ProfileService] def _sanitize_secret(self, value: Optional[str]) -> Optional[str]: if value is None: return None @@ -640,13 +634,13 @@ class ProfileService: return None return normalized - # [/DEF:_sanitize_secret:Function] + # #endregion _sanitize_secret - # [DEF:_normalize_start_page:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize supported start page aliases to canonical values. - # @PRE: value may be None or alias. - # @POST: Returns one of SUPPORTED_START_PAGES. + # #region _normalize_start_page [TYPE Function] + # @BRIEF Normalize supported start page aliases to canonical values. + # @PRE value may be None or alias. + # @POST Returns one of SUPPORTED_START_PAGES. + # @RELATION BINDS_TO -> [ProfileService] def _normalize_start_page(self, value: Optional[str]) -> str: normalized = str(value or "").strip().lower() if normalized == "reports-logs": @@ -655,13 +649,13 @@ class ProfileService: return normalized return "dashboards" - # [/DEF:_normalize_start_page:Function] + # #endregion _normalize_start_page - # [DEF:_normalize_density:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize supported density aliases to canonical values. - # @PRE: value may be None or alias. - # @POST: Returns one of SUPPORTED_DENSITIES. + # #region _normalize_density [TYPE Function] + # @BRIEF Normalize supported density aliases to canonical values. + # @PRE value may be None or alias. + # @POST Returns one of SUPPORTED_DENSITIES. + # @RELATION BINDS_TO -> [ProfileService] def _normalize_density(self, value: Optional[str]) -> str: normalized = str(value or "").strip().lower() if normalized == "free": @@ -670,13 +664,13 @@ class ProfileService: return normalized return "comfortable" - # [/DEF:_normalize_density:Function] + # #endregion _normalize_density - # [DEF:_resolve_environment:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Resolve environment model from configured environments by id. - # @PRE: environment_id is provided. - # @POST: Returns environment object when found else None. + # #region _resolve_environment [TYPE Function] + # @BRIEF Resolve environment model from configured environments by id. + # @PRE environment_id is provided. + # @POST Returns environment object when found else None. + # @RELATION BINDS_TO -> [ProfileService] def _resolve_environment(self, environment_id: str): environments = self.config_manager.get_environments() for env in environments: @@ -684,36 +678,36 @@ class ProfileService: return env return None - # [/DEF:_resolve_environment:Function] + # #endregion _resolve_environment - # [DEF:_get_preference_row:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Return persisted preference row for user or None. - # @PRE: user_id is provided. - # @POST: Returns matching row or None. + # #region _get_preference_row [TYPE Function] + # @BRIEF Return persisted preference row for user or None. + # @PRE user_id is provided. + # @POST Returns matching row or None. + # @RELATION BINDS_TO -> [ProfileService] def _get_preference_row(self, user_id: str) -> Optional[UserDashboardPreference]: return self.auth_repository.get_user_dashboard_preference(str(user_id)) - # [/DEF:_get_preference_row:Function] + # #endregion _get_preference_row - # [DEF:_get_or_create_preference_row:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Return existing preference row or create new unsaved row. - # @PRE: user_id is provided. - # @POST: Returned row always contains user_id. + # #region _get_or_create_preference_row [TYPE Function] + # @BRIEF Return existing preference row or create new unsaved row. + # @PRE user_id is provided. + # @POST Returned row always contains user_id. + # @RELATION BINDS_TO -> [ProfileService] def _get_or_create_preference_row(self, user_id: str) -> UserDashboardPreference: existing = self._get_preference_row(user_id) if existing is not None: return existing return UserDashboardPreference(user_id=str(user_id)) - # [/DEF:_get_or_create_preference_row:Function] + # #endregion _get_or_create_preference_row - # [DEF:_build_default_preference:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Build non-persisted default preference DTO for unconfigured users. - # @PRE: user_id is provided. - # @POST: Returns ProfilePreference with disabled toggle and empty username. + # #region _build_default_preference [TYPE Function] + # @BRIEF Build non-persisted default preference DTO for unconfigured users. + # @PRE user_id is provided. + # @POST Returns ProfilePreference with disabled toggle and empty username. + # @RELATION BINDS_TO -> [ProfileService] def _build_default_preference(self, user_id: str) -> ProfilePreference: now = datetime.utcnow() return ProfilePreference( @@ -736,13 +730,13 @@ class ProfileService: updated_at=now, ) - # [/DEF:_build_default_preference:Function] + # #endregion _build_default_preference - # [DEF:_validate_update_payload:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Validate username/toggle constraints for preference mutation. - # @PRE: payload is provided. - # @POST: Returns validation errors list; empty list means valid. + # #region _validate_update_payload [TYPE Function] + # @BRIEF Validate username/toggle constraints for preference mutation. + # @PRE payload is provided. + # @POST Returns validation errors list; empty list means valid. + # @RELATION BINDS_TO -> [ProfileService] def _validate_update_payload( self, superset_username: Optional[str], @@ -792,36 +786,36 @@ class ProfileService: return errors - # [/DEF:_validate_update_payload:Function] + # #endregion _validate_update_payload - # [DEF:_sanitize_username:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize raw username into trimmed form or None for empty input. - # @PRE: value can be empty or None. - # @POST: Returns trimmed username or None. + # #region _sanitize_username [TYPE Function] + # @BRIEF Normalize raw username into trimmed form or None for empty input. + # @PRE value can be empty or None. + # @POST Returns trimmed username or None. + # @RELATION BINDS_TO -> [ProfileService] def _sanitize_username(self, value: Optional[str]) -> Optional[str]: return self._sanitize_text(value) - # [/DEF:_sanitize_username:Function] + # #endregion _sanitize_username - # [DEF:_normalize_username:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Apply deterministic trim+lower normalization for actor matching. - # @PRE: value can be empty or None. - # @POST: Returns lowercase normalized token or None. + # #region _normalize_username [TYPE Function] + # @BRIEF Apply deterministic trim+lower normalization for actor matching. + # @PRE value can be empty or None. + # @POST Returns lowercase normalized token or None. + # @RELATION BINDS_TO -> [ProfileService] def _normalize_username(self, value: Optional[str]) -> Optional[str]: sanitized = self._sanitize_username(value) if sanitized is None: return None return sanitized.lower() - # [/DEF:_normalize_username:Function] + # #endregion _normalize_username - # [DEF:_normalize_owner_tokens:Function] - # @RELATION: BINDS_TO -> ProfileService - # @PURPOSE: Normalize owners payload into deduplicated lower-cased tokens. - # @PRE: owners can be iterable of scalars or dict-like values. - # @POST: Returns list of unique normalized owner tokens. + # #region _normalize_owner_tokens [TYPE Function] + # @BRIEF Normalize owners payload into deduplicated lower-cased tokens. + # @PRE owners can be iterable of scalars or dict-like values. + # @POST Returns list of unique normalized owner tokens. + # @RELATION BINDS_TO -> [ProfileService] def _normalize_owner_tokens(self, owners: Optional[Iterable[Any]]) -> List[str]: if owners is None: return [] @@ -857,9 +851,9 @@ class ProfileService: normalized.append(token) return normalized - # [/DEF:_normalize_owner_tokens:Function] + # #endregion _normalize_owner_tokens -# [/DEF:ProfileService:Class] +# #endregion ProfileService -# [/DEF:profile_service:Module] +# #endregion profile_service diff --git a/backend/src/services/rbac_permission_catalog.py b/backend/src/services/rbac_permission_catalog.py index 7f858a86..491308e7 100644 --- a/backend/src/services/rbac_permission_catalog.py +++ b/backend/src/services/rbac_permission_catalog.py @@ -1,7 +1,6 @@ -# [DEF:rbac_permission_catalog:Module] +# #region rbac_permission_catalog [C:2] [TYPE Module] +# @BRIEF Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database. # -# @COMPLEXITY: 2 -# @PURPOSE: Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database. # [SECTION: IMPORTS] import re @@ -15,24 +14,24 @@ from ..core.logger import belief_scope, logger from ..models.auth import Permission # [/SECTION: IMPORTS] -# [DEF:HAS_PERMISSION_PATTERN:Constant] -# @PURPOSE: Regex pattern for extracting has_permission("resource", "ACTION") declarations. +# #region HAS_PERMISSION_PATTERN [TYPE Constant] +# @BRIEF Regex pattern for extracting has_permission("resource", "ACTION") declarations. HAS_PERMISSION_PATTERN = re.compile( r"""has_permission\(\s*['"]([^'"]+)['"]\s*,\s*['"]([A-Z]+)['"]\s*\)""" ) -# [/DEF:HAS_PERMISSION_PATTERN:Constant] +# #endregion HAS_PERMISSION_PATTERN -# [DEF:ROUTES_DIR:Constant] -# @PURPOSE: Absolute directory path where API route RBAC declarations are defined. +# #region ROUTES_DIR [TYPE Constant] +# @BRIEF Absolute directory path where API route RBAC declarations are defined. ROUTES_DIR = Path(__file__).resolve().parent.parent / "api" / "routes" -# [/DEF:ROUTES_DIR:Constant] +# #endregion ROUTES_DIR -# [DEF:_iter_route_files:Function] -# @PURPOSE: Iterates API route files that may contain RBAC declarations. -# @PRE: ROUTES_DIR points to backend/src/api/routes. -# @POST: Yields Python files excluding test and cache directories. -# @RETURN: Iterable[Path] - Route file paths for permission extraction. +# #region _iter_route_files [TYPE Function] +# @BRIEF Iterates API route files that may contain RBAC declarations. +# @PRE ROUTES_DIR points to backend/src/api/routes. +# @POST Yields Python files excluding test and cache directories. +# @RETURN Iterable[Path] - Route file paths for permission extraction. def _iter_route_files() -> Iterable[Path]: with belief_scope("rbac_permission_catalog._iter_route_files"): if not ROUTES_DIR.exists(): @@ -45,14 +44,14 @@ def _iter_route_files() -> Iterable[Path]: continue files.append(file_path) return files -# [/DEF:_iter_route_files:Function] +# #endregion _iter_route_files -# [DEF:_discover_route_permissions:Function] -# @PURPOSE: Extracts explicit has_permission declarations from API route source code. -# @PRE: Route files are readable UTF-8 text files. -# @POST: Returns unique set of (resource, action) pairs declared in route guards. -# @RETURN: Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations. +# #region _discover_route_permissions [TYPE Function] +# @BRIEF Extracts explicit has_permission declarations from API route source code. +# @PRE Route files are readable UTF-8 text files. +# @POST Returns unique set of (resource, action) pairs declared in route guards. +# @RETURN Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations. def _discover_route_permissions() -> Set[Tuple[str, str]]: with belief_scope("rbac_permission_catalog._discover_route_permissions"): discovered: Set[Tuple[str, str]] = set() @@ -73,25 +72,25 @@ def _discover_route_permissions() -> Set[Tuple[str, str]]: if normalized_resource and normalized_action: discovered.add((normalized_resource, normalized_action)) return discovered -# [/DEF:_discover_route_permissions:Function] +# #endregion _discover_route_permissions -# [DEF:_discover_route_permissions_cached:Function] -# @PURPOSE: Cache route permission discovery because route source files are static during normal runtime. -# @PRE: None. -# @POST: Returns stable discovered route permission pairs without repeated filesystem scans. +# #region _discover_route_permissions_cached [TYPE Function] +# @BRIEF Cache route permission discovery because route source files are static during normal runtime. +# @PRE None. +# @POST Returns stable discovered route permission pairs without repeated filesystem scans. @lru_cache(maxsize=1) def _discover_route_permissions_cached() -> Tuple[Tuple[str, str], ...]: with belief_scope("rbac_permission_catalog._discover_route_permissions_cached"): return tuple(sorted(_discover_route_permissions())) -# [/DEF:_discover_route_permissions_cached:Function] +# #endregion _discover_route_permissions_cached -# [DEF:_discover_plugin_execute_permissions:Function] -# @PURPOSE: Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry. -# @PRE: plugin_loader is optional and may expose get_all_plugin_configs. -# @POST: Returns unique plugin EXECUTE permissions if loader is available. -# @RETURN: Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs. +# #region _discover_plugin_execute_permissions [TYPE Function] +# @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry. +# @PRE plugin_loader is optional and may expose get_all_plugin_configs. +# @POST Returns unique plugin EXECUTE permissions if loader is available. +# @RETURN Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs. def _discover_plugin_execute_permissions(plugin_loader=None) -> Set[Tuple[str, str]]: with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions"): discovered: Set[Tuple[str, str]] = set() @@ -112,27 +111,27 @@ def _discover_plugin_execute_permissions(plugin_loader=None) -> Set[Tuple[str, s if plugin_id: discovered.add((f"plugin:{plugin_id}", "EXECUTE")) return discovered -# [/DEF:_discover_plugin_execute_permissions:Function] +# #endregion _discover_plugin_execute_permissions -# [DEF:_discover_plugin_execute_permissions_cached:Function] -# @PURPOSE: Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple. -# @PRE: plugin_ids is a deterministic tuple of plugin ids. -# @POST: Returns stable permission tuple without repeated plugin catalog expansion. +# #region _discover_plugin_execute_permissions_cached [TYPE Function] +# @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple. +# @PRE plugin_ids is a deterministic tuple of plugin ids. +# @POST Returns stable permission tuple without repeated plugin catalog expansion. @lru_cache(maxsize=8) def _discover_plugin_execute_permissions_cached( plugin_ids: Tuple[str, ...], ) -> Tuple[Tuple[str, str], ...]: with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions_cached"): return tuple((f"plugin:{plugin_id}", "EXECUTE") for plugin_id in plugin_ids) -# [/DEF:_discover_plugin_execute_permissions_cached:Function] +# #endregion _discover_plugin_execute_permissions_cached -# [DEF:discover_declared_permissions:Function] -# @PURPOSE: Builds canonical RBAC permission catalog from routes and plugin registry. -# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery. -# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions. -# @RETURN: Set[Tuple[str, str]] - Complete discovered permission set. +# #region discover_declared_permissions [TYPE Function] +# @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry. +# @PRE plugin_loader may be provided for dynamic task plugin permission discovery. +# @POST Returns union of route-declared and dynamic plugin EXECUTE permissions. +# @RETURN Set[Tuple[str, str]] - Complete discovered permission set. def discover_declared_permissions(plugin_loader=None) -> Set[Tuple[str, str]]: with belief_scope("rbac_permission_catalog.discover_declared_permissions"): permissions = set(_discover_route_permissions_cached()) @@ -147,16 +146,16 @@ def discover_declared_permissions(plugin_loader=None) -> Set[Tuple[str, str]]: ) permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids)) return permissions -# [/DEF:discover_declared_permissions:Function] +# #endregion discover_declared_permissions -# [DEF:sync_permission_catalog:Function] -# @PURPOSE: Persists missing RBAC permission pairs into auth database. -# @PRE: db is a valid SQLAlchemy session bound to auth database. -# @PRE: declared_permissions is an iterable of (resource, action) tuples. -# @POST: Missing permissions are inserted; existing permissions remain untouched. -# @SIDE_EFFECT: Commits auth database transaction when new permissions are added. -# @RETURN: int - Number of inserted permission records. +# #region sync_permission_catalog [TYPE Function] +# @BRIEF Persists missing RBAC permission pairs into auth database. +# @PRE db is a valid SQLAlchemy session bound to auth database. +# @PRE declared_permissions is an iterable of (resource, action) tuples. +# @POST Missing permissions are inserted; existing permissions remain untouched. +# @SIDE_EFFECT Commits auth database transaction when new permissions are added. +# @RETURN int - Number of inserted permission records. def sync_permission_catalog( db: Session, declared_permissions: Iterable[Tuple[str, str]], @@ -180,6 +179,6 @@ def sync_permission_catalog( db.commit() return len(missing_pairs) -# [/DEF:sync_permission_catalog:Function] +# #endregion sync_permission_catalog -# [/DEF:rbac_permission_catalog:Module] +# #endregion rbac_permission_catalog diff --git a/backend/src/services/reports/__init__.py b/backend/src/services/reports/__init__.py index 612ec71c..cc58332d 100644 --- a/backend/src/services/reports/__init__.py +++ b/backend/src/services/reports/__init__.py @@ -1,3 +1,3 @@ -# [DEF:reports:Package] -# @PURPOSE: Report service package root. -# [/DEF:reports:Package] +# #region reports [TYPE Package] +# @BRIEF Report service package root. +# #endregion reports diff --git a/backend/src/services/reports/__tests__/test_report_normalizer.py b/backend/src/services/reports/__tests__/test_report_normalizer.py index 5562ed4e..aa226839 100644 --- a/backend/src/services/reports/__tests__/test_report_normalizer.py +++ b/backend/src/services/reports/__tests__/test_report_normalizer.py @@ -1,10 +1,8 @@ -# [DEF:test_report_normalizer:Module] -# @COMPLEXITY: 2 -# @SEMANTICS: tests, reports, normalizer, fallback -# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior. -# @RELATION: TESTS ->[normalize_report:Function] -# @LAYER: Domain (Tests) -# @INVARIANT: Unknown plugin types are mapped to canonical unknown task type. +# #region test_report_normalizer [C:2] [TYPE Module] [SEMANTICS tests, reports, normalizer, fallback] +# @BRIEF Validate unknown task type fallback and partial payload normalization behavior. +# @LAYER Domain (Tests) +# @INVARIANT Unknown plugin types are mapped to canonical unknown task type. +# @RELATION TESTS -> [normalize_report:Function] from datetime import datetime @@ -12,9 +10,9 @@ from src.core.task_manager.models import Task, TaskStatus from src.services.reports.normalizer import normalize_task_report -# [DEF:test_unknown_type_maps_to_unknown_profile:Function] -# @RELATION: BINDS_TO -> test_report_normalizer -# @PURPOSE: Ensure unknown plugin IDs map to unknown profile with populated summary and error context. +# #region test_unknown_type_maps_to_unknown_profile [TYPE Function] +# @BRIEF Ensure unknown plugin IDs map to unknown profile with populated summary and error context. +# @RELATION BINDS_TO -> [test_report_normalizer] def test_unknown_type_maps_to_unknown_profile(): task = Task( id="unknown-1", @@ -33,12 +31,12 @@ def test_unknown_type_maps_to_unknown_profile(): assert report.error_context is not None -# [/DEF:test_unknown_type_maps_to_unknown_profile:Function] +# #endregion test_unknown_type_maps_to_unknown_profile -# [DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function] -# @RELATION: BINDS_TO -> test_report_normalizer -# @PURPOSE: Ensure missing result payload still yields visible report details with result placeholder. +# #region test_partial_payload_keeps_report_visible_with_placeholders [TYPE Function] +# @BRIEF Ensure missing result payload still yields visible report details with result placeholder. +# @RELATION BINDS_TO -> [test_report_normalizer] def test_partial_payload_keeps_report_visible_with_placeholders(): task = Task( id="partial-1", @@ -57,12 +55,12 @@ def test_partial_payload_keeps_report_visible_with_placeholders(): assert "result" in report.details -# [/DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function] +# #endregion test_partial_payload_keeps_report_visible_with_placeholders -# [DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function] -# @RELATION: BINDS_TO -> test_report_normalizer -# @PURPOSE: Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough. +# #region test_clean_release_plugin_maps_to_clean_release_task_type [TYPE Function] +# @BRIEF Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough. +# @RELATION BINDS_TO -> [test_report_normalizer] def test_clean_release_plugin_maps_to_clean_release_task_type(): task = Task( id="clean-release-1", @@ -80,5 +78,5 @@ def test_clean_release_plugin_maps_to_clean_release_task_type(): assert report.summary == "Clean release compliance passed" -# [/DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function] -# [/DEF:test_report_normalizer:Module] +# #endregion test_clean_release_plugin_maps_to_clean_release_task_type +# #endregion test_report_normalizer diff --git a/backend/src/services/reports/__tests__/test_report_service.py b/backend/src/services/reports/__tests__/test_report_service.py index 49560848..5f033879 100644 --- a/backend/src/services/reports/__tests__/test_report_service.py +++ b/backend/src/services/reports/__tests__/test_report_service.py @@ -1,8 +1,7 @@ -# [DEF:test_report_service:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Unit tests for ReportsService list/detail operations -# @RELATION: TESTS ->[ReportsService:Class] -# @LAYER: Domain +# #region test_report_service [C:2] [TYPE Module] +# @BRIEF Unit tests for ReportsService list/detail operations +# @LAYER Domain +# @RELATION TESTS -> [ReportsService:Class] import sys from pathlib import Path @@ -13,8 +12,8 @@ from unittest.mock import MagicMock, patch from datetime import datetime, timezone, timedelta -# [DEF:_make_task:Function] -# @RELATION: BINDS_TO -> test_report_service +# #region _make_task [TYPE Function] +# @RELATION BINDS_TO -> [test_report_service] def _make_task(task_id="task-1", plugin_id="superset-backup", status_value="SUCCESS", started_at=None, finished_at=None, result=None, params=None, logs=None): """Create a mock Task object matching the Task model interface.""" @@ -30,7 +29,7 @@ def _make_task(task_id="task-1", plugin_id="superset-backup", status_value="SUCC return task -# [/DEF:_make_task:Function] +# #endregion _make_task class TestReportsServiceList: """Tests for ReportsService.list_reports.""" @@ -184,4 +183,4 @@ class TestReportsServiceDetail: detail = svc.get_report_detail("ok-task") assert detail.next_actions == [] -# [/DEF:test_report_service:Module] +# #endregion test_report_service diff --git a/backend/src/services/reports/__tests__/test_type_profiles.py b/backend/src/services/reports/__tests__/test_type_profiles.py index 74f3fdad..0fae7b9a 100644 --- a/backend/src/services/reports/__tests__/test_type_profiles.py +++ b/backend/src/services/reports/__tests__/test_type_profiles.py @@ -1,7 +1,7 @@ -# [DEF:__tests__/test_report_type_profiles:Module] -# @RELATION: VERIFIES -> ../type_profiles.py -# @PURPOSE: Contract testing for task type profiles and resolution logic. -# [/DEF:__tests__/test_report_type_profiles:Module] +# #region __tests__/test_report_type_profiles [TYPE Module] +# @BRIEF Contract testing for task type profiles and resolution logic. +# @RELATION VERIFIES -> [../type_profiles.py] +# #endregion __tests__/test_report_type_profiles import pytest from src.models.report import TaskType @@ -9,9 +9,9 @@ from src.services.reports.type_profiles import resolve_task_type, get_type_profi # @TEST_CONTRACT: ResolveTaskType -> Invariants # @TEST_INVARIANT: fallback_to_unknown -# [DEF:test_resolve_task_type_fallbacks:Function] -# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles -# @PURPOSE: Verify resolve_task_type_fallbacks returns correct fallback type when primary is missing. +# #region test_resolve_task_type_fallbacks [TYPE Function] +# @BRIEF Verify resolve_task_type_fallbacks returns correct fallback type when primary is missing. +# @RELATION BINDS_TO -> [__tests__/test_report_type_profiles] def test_resolve_task_type_fallbacks(): """Verify missing/unmapped plugin_id returns TaskType.UNKNOWN.""" assert resolve_task_type(None) == TaskType.UNKNOWN @@ -20,11 +20,11 @@ def test_resolve_task_type_fallbacks(): assert resolve_task_type("invalid_plugin") == TaskType.UNKNOWN # @TEST_FIXTURE: valid_plugin -# [/DEF:test_resolve_task_type_fallbacks:Function] +# #endregion test_resolve_task_type_fallbacks -# [DEF:test_resolve_task_type_valid:Function] -# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles -# @PURPOSE: Verify resolve_task_type_valid returns the correct type when valid input is provided. +# #region test_resolve_task_type_valid [TYPE Function] +# @BRIEF Verify resolve_task_type_valid returns the correct type when valid input is provided. +# @RELATION BINDS_TO -> [__tests__/test_report_type_profiles] def test_resolve_task_type_valid(): """Verify known plugin IDs map correctly.""" assert resolve_task_type("superset-migration") == TaskType.MIGRATION @@ -33,11 +33,11 @@ def test_resolve_task_type_valid(): assert resolve_task_type("documentation") == TaskType.DOCUMENTATION # @TEST_FIXTURE: valid_profile -# [/DEF:test_resolve_task_type_valid:Function] +# #endregion test_resolve_task_type_valid -# [DEF:test_get_type_profile_valid:Function] -# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles -# @PURPOSE: Verify get_type_profile_valid returns the correct profile for a valid task type. +# #region test_get_type_profile_valid [TYPE Function] +# @BRIEF Verify get_type_profile_valid returns the correct profile for a valid task type. +# @RELATION BINDS_TO -> [__tests__/test_report_type_profiles] def test_get_type_profile_valid(): """Verify known task types return correct profile metadata.""" profile = get_type_profile(TaskType.MIGRATION) @@ -47,11 +47,11 @@ def test_get_type_profile_valid(): # @TEST_INVARIANT: always_returns_dict # @TEST_EDGE: missing_profile -# [/DEF:test_get_type_profile_valid:Function] +# #endregion test_get_type_profile_valid -# [DEF:test_get_type_profile_fallback:Function] -# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles -# @PURPOSE: Verify get_type_profile_fallback returns default profile when type is unknown. +# #region test_get_type_profile_fallback [TYPE Function] +# @BRIEF Verify get_type_profile_fallback returns default profile when type is unknown. +# @RELATION BINDS_TO -> [__tests__/test_report_type_profiles] def test_get_type_profile_fallback(): """Verify unknown task type returns fallback profile.""" # Assuming TaskType.UNKNOWN or any non-mapped value @@ -63,4 +63,4 @@ def test_get_type_profile_fallback(): profile_fallback = get_type_profile("non-enum-value") assert profile_fallback["display_label"] == "Other / Unknown" assert profile_fallback["fallback"] is True -# [/DEF:test_get_type_profile_fallback:Function] +# #endregion test_get_type_profile_fallback diff --git a/backend/src/services/reports/normalizer.py b/backend/src/services/reports/normalizer.py index 5613e734..0f65d194 100644 --- a/backend/src/services/reports/normalizer.py +++ b/backend/src/services/reports/normalizer.py @@ -1,16 +1,14 @@ -# [DEF:normalizer:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: reports, normalization, tasks, fallback -# @PURPOSE: Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior. -# @LAYER: Domain -# @RELATION: DEPENDS_ON ->[backend.src.core.task_manager.models.Task:Function] -# @RELATION: DEPENDS_ON ->[backend.src.models.report:Function] -# @RELATION: DEPENDS_ON ->[backend.src.services.reports.type_profiles:Function] -# @INVARIANT: Normalizer instance maintains consistent field order -# @DATA_CONTRACT: ReportRow -> NormalizerInput; session_id -> valid UUID -# @PRE: session is active and valid -# @POST: Returns Normalizer output with normalized fields -# @SIDE_EFFECT: Read-only database operations +# #region normalizer [C:5] [TYPE Module] [SEMANTICS reports, normalization, tasks, fallback] +# @BRIEF Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior. +# @LAYER Domain +# @PRE session is active and valid +# @POST Returns Normalizer output with normalized fields +# @SIDE_EFFECT Read-only database operations +# @DATA_CONTRACT ReportRow -> NormalizerInput; session_id -> valid UUID +# @INVARIANT Normalizer instance maintains consistent field order +# @RELATION DEPENDS_ON -> [backend.src.core.task_manager.models.Task:Function] +# @RELATION DEPENDS_ON -> [backend.src.models.report:Function] +# @RELATION DEPENDS_ON -> [backend.src.services.reports.type_profiles:Function] # [SECTION: IMPORTS] from datetime import datetime @@ -23,10 +21,10 @@ from .type_profiles import get_type_profile, resolve_task_type # [/SECTION] -# [DEF:status_to_report_status:Function] -# @PURPOSE: Normalize internal task status to canonical report status. -# @PRE: status may be known or unknown string/enum value. -# @POST: Always returns one of canonical ReportStatus values. +# #region status_to_report_status [TYPE Function] +# @BRIEF Normalize internal task status to canonical report status. +# @PRE status may be known or unknown string/enum value. +# @POST Always returns one of canonical ReportStatus values. # @PARAM: status (Any) - Internal task status value. # @RETURN: ReportStatus - Canonical report status. def status_to_report_status(status: Any) -> ReportStatus: @@ -39,13 +37,13 @@ def status_to_report_status(status: Any) -> ReportStatus: if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}: return ReportStatus.IN_PROGRESS return ReportStatus.PARTIAL -# [/DEF:status_to_report_status:Function] +# #endregion status_to_report_status -# [DEF:build_summary:Function] -# @PURPOSE: Build deterministic user-facing summary from task payload and status. -# @PRE: report_status is canonical; plugin_id may be unknown. -# @POST: Returns non-empty summary text. +# #region build_summary [TYPE Function] +# @BRIEF Build deterministic user-facing summary from task payload and status. +# @PRE report_status is canonical; plugin_id may be unknown. +# @POST Returns non-empty summary text. # @PARAM: task (Task) - Source task object. # @PARAM: report_status (ReportStatus) - Canonical status. # @RETURN: str - Normalized summary. @@ -64,13 +62,13 @@ def build_summary(task: Task, report_status: ReportStatus) -> str: if report_status == ReportStatus.IN_PROGRESS: return "Task is in progress" return "Task completed with partial data" -# [/DEF:build_summary:Function] +# #endregion build_summary -# [DEF:extract_error_context:Function] -# @PURPOSE: Extract normalized error context and next actions for failed/partial reports. -# @PRE: task is a valid Task object. -# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None. +# #region extract_error_context [TYPE Function] +# @BRIEF Extract normalized error context and next actions for failed/partial reports. +# @PRE task is a valid Task object. +# @POST Returns ErrorContext for failed/partial when context exists; otherwise None. # @PARAM: task (Task) - Source task. # @PARAM: report_status (ReportStatus) - Canonical status. # @RETURN: Optional[ErrorContext] - Error context block. @@ -108,13 +106,13 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> Optional[E next_actions = ["Review task diagnostics", "Retry the operation"] return ErrorContext(code=code, message=message, next_actions=next_actions) -# [/DEF:extract_error_context:Function] +# #endregion extract_error_context -# [DEF:normalize_task_report:Function] -# @PURPOSE: Convert one Task to canonical TaskReport envelope. -# @PRE: task has valid id and plugin_id fields. -# @POST: Returns TaskReport with required fields and deterministic fallback behavior. +# #region normalize_task_report [TYPE Function] +# @BRIEF Convert one Task to canonical TaskReport envelope. +# @PRE task has valid id and plugin_id fields. +# @POST Returns TaskReport with required fields and deterministic fallback behavior. # @PARAM: task (Task) - Source task. # @RETURN: TaskReport - Canonical normalized report. # @@ -170,6 +168,6 @@ def normalize_task_report(task: Task) -> TaskReport: error_context=extract_error_context(task, report_status), source_ref=source_ref or None, ) -# [/DEF:normalize_task_report:Function] +# #endregion normalize_task_report -# [/DEF:normalizer:Module] \ No newline at end of file +# #endregion normalizer diff --git a/backend/src/services/reports/report_service.py b/backend/src/services/reports/report_service.py index 1fe5706d..ab975d23 100644 --- a/backend/src/services/reports/report_service.py +++ b/backend/src/services/reports/report_service.py @@ -1,20 +1,18 @@ -# [DEF:report_service:Module] -# @COMPLEXITY: 5 -# @SEMANTICS: reports, service, aggregation, filtering, pagination, detail -# @PURPOSE: Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases. -# @LAYER: Domain -# @RELATION: [DEPENDS_ON] ->[TaskManager] -# @RELATION: [DEPENDS_ON] ->[TaskReport] -# @RELATION: [DEPENDS_ON] ->[ReportQuery] -# @RELATION: [DEPENDS_ON] ->[ReportCollection] -# @RELATION: [DEPENDS_ON] ->[ReportDetailView] -# @RELATION: [DEPENDS_ON] ->[normalize_task_report] -# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository] -# @INVARIANT: ReportService maintains consistent report structure -# @DATA_CONTRACT: ReportQuery -> ReportRow; session_id -> valid UUID -# @PRE: session is active and valid -# @POST: Returns Report with generated summary -# @SIDE_EFFECT: Read-only database operations; logs report generation +# #region report_service [C:5] [TYPE Module] [SEMANTICS reports, service, aggregation, filtering, pagination, detail] +# @BRIEF Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases. +# @LAYER Domain +# @PRE session is active and valid +# @POST Returns Report with generated summary +# @SIDE_EFFECT Read-only database operations; logs report generation +# @DATA_CONTRACT ReportQuery -> ReportRow; session_id -> valid UUID +# @INVARIANT ReportService maintains consistent report structure +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [TaskReport] +# @RELATION DEPENDS_ON -> [ReportQuery] +# @RELATION DEPENDS_ON -> [ReportCollection] +# @RELATION DEPENDS_ON -> [ReportDetailView] +# @RELATION DEPENDS_ON -> [normalize_task_report] +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] # [SECTION: IMPORTS] from datetime import datetime, timezone @@ -36,17 +34,16 @@ from .normalizer import normalize_task_report # [/SECTION] -# [DEF:ReportsService:Class] -# @PURPOSE: Service layer for list/detail report retrieval and normalization. -# @COMPLEXITY: 5 -# @PRE: TaskManager dependency is initialized. -# @POST: Provides deterministic list/detail report responses. -# @RELATION: [DEPENDS_ON] ->[TaskManager] -# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository] -# @RELATION: [CALLS] ->[normalize_task_report] -# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records. -# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None] -# @INVARIANT: Service methods are read-only over task history source. +# #region ReportsService [C:5] [TYPE Class] +# @BRIEF Service layer for list/detail report retrieval and normalization. +# @PRE TaskManager dependency is initialized. +# @POST Provides deterministic list/detail report responses. +# @SIDE_EFFECT Reads task history and optional clean-release repository state without mutating source records. +# @DATA_CONTRACT Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None] +# @INVARIANT Service methods are read-only over task history source. +# @RELATION DEPENDS_ON -> [TaskManager] +# @RELATION DEPENDS_ON -> [CleanReleaseRepository] +# @RELATION CALLS -> [normalize_task_report] # # @TEST_CONTRACT: ReportsServiceModel -> # { @@ -61,17 +58,16 @@ from .normalizer import normalize_task_report # @TEST_EDGE: report_not_found -> get_report_detail returns None # @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service] class ReportsService: - # [DEF:init:Function] - # @COMPLEXITY: 5 - # @PURPOSE: Initialize service with TaskManager dependency. - # @PRE: task_manager is a live TaskManager instance. - # @POST: self.task_manager is assigned and ready for read operations. - # @INVARIANT: Constructor performs no task mutations. - # @RELATION: [BINDS_TO] ->[ReportsService] - # @RELATION: [DEPENDS_ON] ->[TaskManager] - # @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository] - # @SIDE_EFFECT: Stores collaborator references for later read-only report projections. - # @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService] + # #region init [C:5] [TYPE Function] + # @BRIEF Initialize service with TaskManager dependency. + # @PRE task_manager is a live TaskManager instance. + # @POST self.task_manager is assigned and ready for read operations. + # @SIDE_EFFECT Stores collaborator references for later read-only report projections. + # @DATA_CONTRACT Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService] + # @INVARIANT Constructor performs no task mutations. + # @RELATION BINDS_TO -> [ReportsService] + # @RELATION DEPENDS_ON -> [TaskManager] + # @RELATION DEPENDS_ON -> [CleanReleaseRepository] # @PARAM: task_manager (TaskManager) - Task manager providing source task history. def __init__( self, @@ -82,27 +78,27 @@ class ReportsService: self.task_manager = task_manager self.clean_release_repository = clean_release_repository - # [/DEF:init:Function] + # #endregion init - # [DEF:_load_normalized_reports:Function] - # @PURPOSE: Build normalized reports from all available tasks. - # @PRE: Task manager returns iterable task history records. - # @POST: Returns normalized report list preserving source cardinality. - # @INVARIANT: Every returned item is a TaskReport. - # @RETURN: List[TaskReport] - Reports sorted later by list logic. + # #region _load_normalized_reports [TYPE Function] + # @BRIEF Build normalized reports from all available tasks. + # @PRE Task manager returns iterable task history records. + # @POST Returns normalized report list preserving source cardinality. + # @INVARIANT Every returned item is a TaskReport. + # @RETURN List[TaskReport] - Reports sorted later by list logic. def _load_normalized_reports(self) -> List[TaskReport]: with belief_scope("_load_normalized_reports"): tasks = self.task_manager.get_all_tasks() reports = [normalize_task_report(task) for task in tasks] return reports - # [/DEF:_load_normalized_reports:Function] + # #endregion _load_normalized_reports - # [DEF:_to_utc_datetime:Function] - # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons. - # @PRE: value is either datetime or None. - # @POST: Returns UTC-aware datetime or None. - # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering. + # #region _to_utc_datetime [TYPE Function] + # @BRIEF Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons. + # @PRE value is either datetime or None. + # @POST Returns UTC-aware datetime or None. + # @INVARIANT Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering. # @PARAM: value (Optional[datetime]) - Source datetime value. # @RETURN: Optional[datetime] - UTC-aware datetime or None. def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]: @@ -113,13 +109,13 @@ class ReportsService: return value.replace(tzinfo=timezone.utc) return value.astimezone(timezone.utc) - # [/DEF:_to_utc_datetime:Function] + # #endregion _to_utc_datetime - # [DEF:_datetime_sort_key:Function] - # @PURPOSE: Produce stable numeric sort key for report timestamps. - # @PRE: report contains updated_at datetime. - # @POST: Returns float timestamp suitable for deterministic sorting. - # @INVARIANT: Mixed naive/aware datetimes never raise TypeError. + # #region _datetime_sort_key [TYPE Function] + # @BRIEF Produce stable numeric sort key for report timestamps. + # @PRE report contains updated_at datetime. + # @POST Returns float timestamp suitable for deterministic sorting. + # @INVARIANT Mixed naive/aware datetimes never raise TypeError. # @PARAM: report (TaskReport) - Report item. # @RETURN: float - UTC timestamp key. def _datetime_sort_key(self, report: TaskReport) -> float: @@ -129,13 +125,13 @@ class ReportsService: return 0.0 return updated.timestamp() - # [/DEF:_datetime_sort_key:Function] + # #endregion _datetime_sort_key - # [DEF:_matches_query:Function] - # @PURPOSE: Apply query filtering to a report. - # @PRE: report and query are normalized schema instances. - # @POST: Returns True iff report satisfies all active query filters. - # @INVARIANT: Filter evaluation is side-effect free. + # #region _matches_query [TYPE Function] + # @BRIEF Apply query filtering to a report. + # @PRE report and query are normalized schema instances. + # @POST Returns True iff report satisfies all active query filters. + # @INVARIANT Filter evaluation is side-effect free. # @PARAM: report (TaskReport) - Candidate report. # @PARAM: query (ReportQuery) - Applied query. # @RETURN: bool - True if report matches all filters. @@ -168,13 +164,13 @@ class ReportsService: return False return True - # [/DEF:_matches_query:Function] + # #endregion _matches_query - # [DEF:_sort_reports:Function] - # @PURPOSE: Sort reports deterministically according to query settings. - # @PRE: reports contains only TaskReport items. - # @POST: Returns reports ordered by selected sort field and order. - # @INVARIANT: Sorting criteria are deterministic for equal input. + # #region _sort_reports [TYPE Function] + # @BRIEF Sort reports deterministically according to query settings. + # @PRE reports contains only TaskReport items. + # @POST Returns reports ordered by selected sort field and order. + # @INVARIANT Sorting criteria are deterministic for equal input. # @PARAM: reports (List[TaskReport]) - Filtered reports. # @PARAM: query (ReportQuery) - Sort config. # @RETURN: List[TaskReport] - Sorted reports. @@ -193,12 +189,12 @@ class ReportsService: return reports - # [/DEF:_sort_reports:Function] + # #endregion _sort_reports - # [DEF:list_reports:Function] - # @PURPOSE: Return filtered, sorted, paginated report collection. - # @PRE: query has passed schema validation. - # @POST: Returns {items,total,page,page_size,has_next,applied_filters}. + # #region list_reports [TYPE Function] + # @BRIEF Return filtered, sorted, paginated report collection. + # @PRE query has passed schema validation. + # @POST Returns {items,total,page,page_size,has_next,applied_filters}. # @PARAM: query (ReportQuery) - List filters and pagination. # @RETURN: ReportCollection - Paginated unified reports payload. def list_reports(self, query: ReportQuery) -> ReportCollection: @@ -224,12 +220,12 @@ class ReportsService: applied_filters=query, ) - # [/DEF:list_reports:Function] + # #endregion list_reports - # [DEF:get_report_detail:Function] - # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions. - # @PRE: report_id exists in normalized report set. - # @POST: Returns normalized detail envelope with diagnostics and next actions where applicable. + # #region get_report_detail [TYPE Function] + # @BRIEF Return one normalized report with timeline/diagnostics/next actions. + # @PRE report_id exists in normalized report set. + # @POST Returns normalized detail envelope with diagnostics and next actions where applicable. # @PARAM: report_id (str) - Stable report identifier. # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found. def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]: @@ -302,9 +298,8 @@ class ReportsService: next_actions=next_actions, ) - # [/DEF:get_report_detail:Function] + # #endregion get_report_detail -# [/DEF:ReportsService:Class] - -# [/DEF:report_service:Module] +# #endregion ReportsService +# #endregion report_service diff --git a/backend/src/services/reports/type_profiles.py b/backend/src/services/reports/type_profiles.py index a6b01f23..da958dfe 100644 --- a/backend/src/services/reports/type_profiles.py +++ b/backend/src/services/reports/type_profiles.py @@ -1,6 +1,5 @@ -# [DEF:type_profiles:Module] -# @COMPLEXITY: 2 -# @PURPOSE: Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata. +# #region type_profiles [C:2] [TYPE Module] +# @BRIEF Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata. # [SECTION: IMPORTS] from typing import Any, Dict, Optional @@ -9,8 +8,8 @@ from ...core.logger import belief_scope from ...models.report import TaskType # [/SECTION] -# [DEF:PLUGIN_TO_TASK_TYPE:Data] -# @PURPOSE: Maps plugin identifiers to normalized report task types. +# #region PLUGIN_TO_TASK_TYPE [TYPE Data] +# @BRIEF Maps plugin identifiers to normalized report task types. PLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = { "llm_dashboard_validation": TaskType.LLM_VERIFICATION, "superset-backup": TaskType.BACKUP, @@ -19,10 +18,10 @@ PLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = { "clean-release-compliance": TaskType.CLEAN_RELEASE, "clean_release_compliance": TaskType.CLEAN_RELEASE, } -# [/DEF:PLUGIN_TO_TASK_TYPE:Data] +# #endregion PLUGIN_TO_TASK_TYPE -# [DEF:TASK_TYPE_PROFILES:Data] -# @PURPOSE: Profile metadata registry for each normalized task type. +# #region TASK_TYPE_PROFILES [TYPE Data] +# @BRIEF Profile metadata registry for each normalized task type. TASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = { TaskType.LLM_VERIFICATION: { "display_label": "LLM Verification", @@ -67,13 +66,13 @@ TASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = { "fallback": True, }, } -# [/DEF:TASK_TYPE_PROFILES:Data] +# #endregion TASK_TYPE_PROFILES -# [DEF:resolve_task_type:Function] -# @PURPOSE: Resolve canonical task type from plugin/task identifier with guaranteed fallback. -# @PRE: plugin_id may be None or unknown. -# @POST: Always returns one of TaskType enum values. +# #region resolve_task_type [TYPE Function] +# @BRIEF Resolve canonical task type from plugin/task identifier with guaranteed fallback. +# @PRE plugin_id may be None or unknown. +# @POST Always returns one of TaskType enum values. # @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record. # @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback. # @@ -95,13 +94,13 @@ def resolve_task_type(plugin_id: Optional[str]) -> TaskType: return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN) -# [/DEF:resolve_task_type:Function] +# #endregion resolve_task_type -# [DEF:get_type_profile:Function] -# @PURPOSE: Return deterministic profile metadata for a task type. -# @PRE: task_type may be known or unknown. -# @POST: Returns a profile dict and never raises for unknown types. +# #region get_type_profile [TYPE Function] +# @BRIEF Return deterministic profile metadata for a task type. +# @PRE task_type may be known or unknown. +# @POST Returns a profile dict and never raises for unknown types. # @PARAM: task_type (TaskType) - Canonical task type. # @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts. # @@ -118,6 +117,6 @@ def get_type_profile(task_type: TaskType) -> Dict[str, Any]: return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN]) -# [/DEF:get_type_profile:Function] +# #endregion get_type_profile -# [/DEF:type_profiles:Module] +# #endregion type_profiles diff --git a/backend/src/services/resource_service.py b/backend/src/services/resource_service.py index 3d0da734..bce4ac93 100644 --- a/backend/src/services/resource_service.py +++ b/backend/src/services/resource_service.py @@ -1,13 +1,11 @@ -# [DEF:ResourceServiceModule:Module] -# @COMPLEXITY: 3 -# @SEMANTICS: service, resources, dashboards, datasets, tasks, git -# @PURPOSE: Shared service for fetching resource data with Git status and task status -# @LAYER: Service -# @RELATION: DEPENDS_ON ->[SupersetClient] -# @RELATION: DEPENDS_ON ->[TaskManagerPackage] -# @RELATION: DEPENDS_ON ->[TaskManagerModels] -# @RELATION: DEPENDS_ON ->[GitService] -# @INVARIANT: All resources include metadata about their current state +# #region ResourceServiceModule [C:3] [TYPE Module] [SEMANTICS service, resources, dashboards, datasets, tasks, git] +# @BRIEF Shared service for fetching resource data with Git status and task status +# @LAYER Service +# @INVARIANT All resources include metadata about their current state +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [TaskManagerPackage] +# @RELATION DEPENDS_ON -> [TaskManagerModels] +# @RELATION DEPENDS_ON -> [GitService] # [SECTION: IMPORTS] from typing import List, Dict, Optional, Any @@ -21,29 +19,26 @@ from ..core.logger import logger, belief_scope log = MarkerLogger("ResourceService") # [/SECTION] -# [DEF:ResourceService:Class] -# @COMPLEXITY: 3 -# @PURPOSE: Provides centralized access to resource data with enhanced metadata -# @RELATION: DEPENDS_ON ->[SupersetClient] -# @RELATION: DEPENDS_ON ->[GitService] +# #region ResourceService [C:3] [TYPE Class] +# @BRIEF Provides centralized access to resource data with enhanced metadata +# @RELATION DEPENDS_ON -> [SupersetClient] +# @RELATION DEPENDS_ON -> [GitService] class ResourceService: - # [DEF:ResourceService_init:Function] - # @COMPLEXITY: 1 - # @PURPOSE: Initialize the resource service with dependencies - # @PRE: None - # @POST: ResourceService is ready to fetch resources + # #region ResourceService_init [C:1] [TYPE Function] + # @BRIEF Initialize the resource service with dependencies + # @PRE None + # @POST ResourceService is ready to fetch resources def __init__(self): with belief_scope("ResourceService.__init__"): self.git_service = GitService() log.reason("Initialized ResourceService") - # [/DEF:ResourceService_init:Function] + # #endregion ResourceService_init - # [DEF:get_dashboards_with_status:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch dashboards from environment with Git status and last task status - # @PRE: env is a valid Environment object - # @POST: Returns list of dashboards with enhanced metadata + # #region get_dashboards_with_status [C:3] [TYPE Function] + # @BRIEF Fetch dashboards from environment with Git status and last task status + # @PRE env is a valid Environment object + # @POST Returns list of dashboards with enhanced metadata # @PARAM: env (Environment) - The environment to fetch from # @PARAM: tasks (List[Task]) - List of tasks to check for status # @RETURN: List[Dict] - Dashboards with git_status and last_task fields @@ -87,13 +82,12 @@ class ResourceService: log.reflect(f"Fetched {len(result)} dashboards with status") return result - # [/DEF:get_dashboards_with_status:Function] + # #endregion get_dashboards_with_status - # [DEF:get_dashboards_page_with_status:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata. - # @PRE: env is valid; page >= 1; page_size > 0. - # @POST: Returns page items plus total counters without scanning all pages locally. + # #region get_dashboards_page_with_status [C:3] [TYPE Function] + # @BRIEF Fetch one dashboard page from environment and enrich only that page with status metadata. + # @PRE env is valid; page >= 1; page_size > 0. + # @POST Returns page items plus total counters without scanning all pages locally. # @PARAM: env (Environment) - Source environment. # @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status. # @PARAM: page (int) - 1-based page number. @@ -148,13 +142,12 @@ class ResourceService: "total": total, "total_pages": total_pages, } - # [/DEF:get_dashboards_page_with_status:Function] + # #endregion get_dashboards_page_with_status - # [DEF:_get_last_llm_task_for_dashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment - # @PRE: dashboard_id is a valid integer identifier - # @POST: Returns the newest llm_dashboard_validation task summary or None + # #region _get_last_llm_task_for_dashboard [C:3] [TYPE Function] + # @BRIEF Get most recent LLM validation task for a dashboard in an environment + # @PRE dashboard_id is a valid integer identifier + # @POST Returns the newest llm_dashboard_validation task summary or None # @PARAM: dashboard_id (int) - The dashboard ID # @PARAM: env_id (Optional[str]) - Environment ID to match task params # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search @@ -232,13 +225,12 @@ class ResourceService: "status": self._normalize_task_status(getattr(latest_task, "status", "")), "validation_status": validation_status, } - # [/DEF:_get_last_llm_task_for_dashboard:Function] + # #endregion _get_last_llm_task_for_dashboard - # [DEF:_normalize_task_status:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections - # @PRE: raw_status can be enum or string - # @POST: Returns uppercase status without enum class prefix + # #region _normalize_task_status [C:3] [TYPE Function] + # @BRIEF Normalize task status to stable uppercase values for UI/API projections + # @PRE raw_status can be enum or string + # @POST Returns uppercase status without enum class prefix # @PARAM: raw_status (Any) - Raw task status object/value # @RETURN: str - Normalized status token # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard] @@ -250,13 +242,12 @@ class ResourceService: if "." in status_text: status_text = status_text.split(".")[-1] return status_text.upper() - # [/DEF:_normalize_task_status:Function] + # #endregion _normalize_task_status - # [DEF:_normalize_validation_status:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN - # @PRE: raw_status can be any scalar type - # @POST: Returns normalized validation status token or None + # #region _normalize_validation_status [C:3] [TYPE Function] + # @BRIEF Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN + # @PRE raw_status can be any scalar type + # @POST Returns normalized validation status token or None # @PARAM: raw_status (Any) - Raw validation status from task result # @RETURN: Optional[str] - PASS|FAIL|WARN|UNKNOWN # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard] @@ -267,13 +258,12 @@ class ResourceService: if status_text in {"PASS", "FAIL", "WARN"}: return status_text return "UNKNOWN" - # [/DEF:_normalize_validation_status:Function] + # #endregion _normalize_validation_status - # [DEF:_normalize_datetime_for_compare:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons. - # @PRE: value may be datetime or any scalar. - # @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime. + # #region _normalize_datetime_for_compare [C:3] [TYPE Function] + # @BRIEF Normalize datetime values to UTC-aware values for safe comparisons. + # @PRE value may be datetime or any scalar. + # @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime. # @PARAM: value (Any) - Candidate datetime-like value. # @RETURN: datetime - UTC-aware comparable datetime. # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard] @@ -284,13 +274,12 @@ class ResourceService: return value.replace(tzinfo=timezone.utc) return value.astimezone(timezone.utc) return datetime.min.replace(tzinfo=timezone.utc) - # [/DEF:_normalize_datetime_for_compare:Function] + # #endregion _normalize_datetime_for_compare - # [DEF:get_datasets_with_status:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Fetch datasets from environment with mapping progress and last task status - # @PRE: env is a valid Environment object - # @POST: Returns list of datasets with enhanced metadata + # #region get_datasets_with_status [C:3] [TYPE Function] + # @BRIEF Fetch datasets from environment with mapping progress and last task status + # @PRE env is a valid Environment object + # @POST Returns list of datasets with enhanced metadata # @PARAM: env (Environment) - The environment to fetch from # @PARAM: tasks (List[Task]) - List of tasks to check for status # @RETURN: List[Dict] - Datasets with mapped_fields and last_task fields @@ -323,13 +312,12 @@ class ResourceService: log.reflect(f"Fetched {len(result)} datasets with status") return result - # [/DEF:get_datasets_with_status:Function] + # #endregion get_datasets_with_status - # [DEF:get_activity_summary:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get summary of active and recent tasks for the activity indicator - # @PRE: tasks is a list of Task objects - # @POST: Returns summary with active_count and recent_tasks + # #region get_activity_summary [C:3] [TYPE Function] + # @BRIEF Get summary of active and recent tasks for the activity indicator + # @PRE tasks is a list of Task objects + # @POST Returns summary with active_count and recent_tasks # @PARAM: tasks (List[Task]) - List of tasks to summarize # @RETURN: Dict - Activity summary # @RELATION: CALLS ->[_extract_resource_name_from_task] @@ -365,13 +353,12 @@ class ResourceService: 'active_count': len(active_tasks), 'recent_tasks': recent_tasks_formatted } - # [/DEF:get_activity_summary:Function] + # #endregion get_activity_summary - # [DEF:_get_git_status_for_dashboard:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get Git sync status for a dashboard - # @PRE: dashboard_id is a valid integer - # @POST: Returns git status or None if no repo exists + # #region _get_git_status_for_dashboard [C:3] [TYPE Function] + # @BRIEF Get Git sync status for a dashboard + # @PRE dashboard_id is a valid integer + # @POST Returns git status or None if no repo exists # @PARAM: dashboard_id (int) - The dashboard ID # @RETURN: Optional[Dict] - Git status with branch and sync_status # @RELATION: CALLS ->[get_repo] @@ -425,13 +412,12 @@ class ResourceService: 'has_repo': False, 'has_changes_for_commit': False } - # [/DEF:_get_git_status_for_dashboard:Function] + # #endregion _get_git_status_for_dashboard - # [DEF:_get_last_task_for_resource:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Get the most recent task for a specific resource - # @PRE: resource_id is a valid string - # @POST: Returns task summary or None if no tasks found + # #region _get_last_task_for_resource [C:3] [TYPE Function] + # @BRIEF Get the most recent task for a specific resource + # @PRE resource_id is a valid string + # @POST Returns task summary or None if no tasks found # @PARAM: resource_id (str) - The resource identifier (e.g., "dashboard-123") # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search # @RETURN: Optional[Dict] - Task summary with task_id and status @@ -464,32 +450,29 @@ class ResourceService: 'task_id': str(last_task.id), 'status': last_task.status } - # [/DEF:_get_last_task_for_resource:Function] + # #endregion _get_last_task_for_resource - # [DEF:_extract_resource_name_from_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract resource name from task params - # @PRE: task is a valid Task object - # @POST: Returns resource name or task ID + # #region _extract_resource_name_from_task [C:3] [TYPE Function] + # @BRIEF Extract resource name from task params + # @PRE task is a valid Task object + # @POST Returns resource name or task ID # @PARAM: task (Task) - The task to extract from # @RETURN: str - Resource name or fallback # @RELATION: USED_BY ->[get_activity_summary] def _extract_resource_name_from_task(self, task: Task) -> str: params = task.params or {} return params.get('resource_name', f"Task {task.id}") - # [/DEF:_extract_resource_name_from_task:Function] + # #endregion _extract_resource_name_from_task - # [DEF:_extract_resource_type_from_task:Function] - # @COMPLEXITY: 3 - # @PURPOSE: Extract resource type from task params - # @PRE: task is a valid Task object - # @POST: Returns resource type or 'unknown' + # #region _extract_resource_type_from_task [C:3] [TYPE Function] + # @BRIEF Extract resource type from task params + # @PRE task is a valid Task object + # @POST Returns resource type or 'unknown' # @PARAM: task (Task) - The task to extract from # @RETURN: str - Resource type # @RELATION: USED_BY ->[get_activity_summary] def _extract_resource_type_from_task(self, task: Task) -> str: params = task.params or {} return params.get('resource_type', 'unknown') - # [/DEF:_extract_resource_type_from_task:Function] -# [/DEF:ResourceService:Class] -# [/DEF:ResourceServiceModule:Module] + # #endregion _extract_resource_type_from_task +# #endregion ResourceService diff --git a/frontend/src/components/DashboardGrid.svelte b/frontend/src/components/DashboardGrid.svelte index 61686ad4..aa71df66 100644 --- a/frontend/src/components/DashboardGrid.svelte +++ b/frontend/src/components/DashboardGrid.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/DynamicForm.svelte b/frontend/src/components/DynamicForm.svelte index 2d9f0a56..e3e39e47 100755 --- a/frontend/src/components/DynamicForm.svelte +++ b/frontend/src/components/DynamicForm.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/EnvSelector.svelte b/frontend/src/components/EnvSelector.svelte index 5865685c..0ffdc59e 100644 --- a/frontend/src/components/EnvSelector.svelte +++ b/frontend/src/components/EnvSelector.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/Footer.svelte b/frontend/src/components/Footer.svelte index 0e75c78e..dd798ab0 100644 --- a/frontend/src/components/Footer.svelte +++ b/frontend/src/components/Footer.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/MappingTable.svelte b/frontend/src/components/MappingTable.svelte index 7e109f17..de3ba4ce 100644 --- a/frontend/src/components/MappingTable.svelte +++ b/frontend/src/components/MappingTable.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/MissingMappingModal.svelte b/frontend/src/components/MissingMappingModal.svelte index 29ca082c..ccbe3ef5 100644 --- a/frontend/src/components/MissingMappingModal.svelte +++ b/frontend/src/components/MissingMappingModal.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/Navbar.svelte b/frontend/src/components/Navbar.svelte index cdf4eecb..0d5eefa7 100644 --- a/frontend/src/components/Navbar.svelte +++ b/frontend/src/components/Navbar.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/PasswordPrompt.svelte b/frontend/src/components/PasswordPrompt.svelte index 82c52a9b..71e36925 100644 --- a/frontend/src/components/PasswordPrompt.svelte +++ b/frontend/src/components/PasswordPrompt.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/RepositoryDashboardGrid.svelte b/frontend/src/components/RepositoryDashboardGrid.svelte index 4e3c2a19..1a39f2d7 100644 --- a/frontend/src/components/RepositoryDashboardGrid.svelte +++ b/frontend/src/components/RepositoryDashboardGrid.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/StartupEnvironmentWizard.svelte b/frontend/src/components/StartupEnvironmentWizard.svelte index ca206cb8..66b1f1d2 100644 --- a/frontend/src/components/StartupEnvironmentWizard.svelte +++ b/frontend/src/components/StartupEnvironmentWizard.svelte @@ -1,4 +1,6 @@ - + + + {#if shouldShow} - - - - - - + + {#if inline}
{#if loading && logs.length === 0} @@ -189,17 +184,10 @@ /> {/if}
- + {:else} - - - - - - + +
{/if} {/if} - + - + diff --git a/frontend/src/components/TaskRunner.svelte b/frontend/src/components/TaskRunner.svelte index 59e56f58..ae433c62 100755 --- a/frontend/src/components/TaskRunner.svelte +++ b/frontend/src/components/TaskRunner.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/Toast.svelte b/frontend/src/components/Toast.svelte index c8a09fb9..cf4b18a0 100755 --- a/frontend/src/components/Toast.svelte +++ b/frontend/src/components/Toast.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/auth/ProtectedRoute.svelte b/frontend/src/components/auth/ProtectedRoute.svelte index 7dc26ba7..50b45e4a 100644 --- a/frontend/src/components/auth/ProtectedRoute.svelte +++ b/frontend/src/components/auth/ProtectedRoute.svelte @@ -1,4 +1,6 @@ - + + + - + - + diff --git a/frontend/src/components/backups/BackupList.svelte b/frontend/src/components/backups/BackupList.svelte index 89175e55..9bf3b16f 100644 --- a/frontend/src/components/backups/BackupList.svelte +++ b/frontend/src/components/backups/BackupList.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/backups/BackupManager.svelte b/frontend/src/components/backups/BackupManager.svelte index a6736c5a..1a32332b 100644 --- a/frontend/src/components/backups/BackupManager.svelte +++ b/frontend/src/components/backups/BackupManager.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/BranchSelector.svelte b/frontend/src/components/git/BranchSelector.svelte index 90fce144..f0873045 100644 --- a/frontend/src/components/git/BranchSelector.svelte +++ b/frontend/src/components/git/BranchSelector.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/CommitHistory.svelte b/frontend/src/components/git/CommitHistory.svelte index 2e6808b3..0b559d24 100644 --- a/frontend/src/components/git/CommitHistory.svelte +++ b/frontend/src/components/git/CommitHistory.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/CommitModal.svelte b/frontend/src/components/git/CommitModal.svelte index d5477887..2965e527 100644 --- a/frontend/src/components/git/CommitModal.svelte +++ b/frontend/src/components/git/CommitModal.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/ConflictResolver.svelte b/frontend/src/components/git/ConflictResolver.svelte index 019ef8ba..1954e3e7 100644 --- a/frontend/src/components/git/ConflictResolver.svelte +++ b/frontend/src/components/git/ConflictResolver.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/DeploymentModal.svelte b/frontend/src/components/git/DeploymentModal.svelte index 3f6ee744..a7adf00b 100644 --- a/frontend/src/components/git/DeploymentModal.svelte +++ b/frontend/src/components/git/DeploymentModal.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/git/GitManager.svelte b/frontend/src/components/git/GitManager.svelte index 779c1c56..8a3b6c86 100644 --- a/frontend/src/components/git/GitManager.svelte +++ b/frontend/src/components/git/GitManager.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/llm/DocPreview.svelte b/frontend/src/components/llm/DocPreview.svelte index 1f30f045..5bedb599 100644 --- a/frontend/src/components/llm/DocPreview.svelte +++ b/frontend/src/components/llm/DocPreview.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/llm/ProviderConfig.svelte b/frontend/src/components/llm/ProviderConfig.svelte index 17c21c30..2c953277 100644 --- a/frontend/src/components/llm/ProviderConfig.svelte +++ b/frontend/src/components/llm/ProviderConfig.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/llm/ValidationReport.svelte b/frontend/src/components/llm/ValidationReport.svelte index 2c325a8f..f79e0850 100644 --- a/frontend/src/components/llm/ValidationReport.svelte +++ b/frontend/src/components/llm/ValidationReport.svelte @@ -1,4 +1,6 @@ - + + + @@ -76,4 +78,4 @@
{/if} - + diff --git a/frontend/src/components/storage/FileList.svelte b/frontend/src/components/storage/FileList.svelte index e9b920f8..96c061bf 100644 --- a/frontend/src/components/storage/FileList.svelte +++ b/frontend/src/components/storage/FileList.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/storage/FileUpload.svelte b/frontend/src/components/storage/FileUpload.svelte index e2db5815..98323235 100644 --- a/frontend/src/components/storage/FileUpload.svelte +++ b/frontend/src/components/storage/FileUpload.svelte @@ -1,4 +1,6 @@ - + + + - + diff --git a/frontend/src/components/tasks/LogEntryRow.svelte b/frontend/src/components/tasks/LogEntryRow.svelte index ed837d14..df0ea3f9 100644 --- a/frontend/src/components/tasks/LogEntryRow.svelte +++ b/frontend/src/components/tasks/LogEntryRow.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/tasks/LogFilterBar.svelte b/frontend/src/components/tasks/LogFilterBar.svelte index 7dfba23f..9fe297b5 100644 --- a/frontend/src/components/tasks/LogFilterBar.svelte +++ b/frontend/src/components/tasks/LogFilterBar.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/tasks/TaskLogPanel.svelte b/frontend/src/components/tasks/TaskLogPanel.svelte index c2510a9f..2f87b256 100644 --- a/frontend/src/components/tasks/TaskLogPanel.svelte +++ b/frontend/src/components/tasks/TaskLogPanel.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/components/tasks/TaskResultPanel.svelte b/frontend/src/components/tasks/TaskResultPanel.svelte index a4dfa5e1..33d822de 100644 --- a/frontend/src/components/tasks/TaskResultPanel.svelte +++ b/frontend/src/components/tasks/TaskResultPanel.svelte @@ -1,3 +1,9 @@ + + + + + + - - -
+ } + } + // [/DEF:handleTestEnv:Function] + + // [DEF:editEnv:Function] + /** + * @purpose Sets the form to edit an existing environment. + * @pre env object is provided. + * @post newEnv is populated with env data and editingEnvId is set. + * @param {Object} env - The environment object to edit. + */ + function editEnv(env) { + newEnv = { ...env }; + editingEnvId = env.id; + } + // [/DEF:editEnv:Function] + + // [DEF:resetEnvForm:Function] + /** + * @purpose Resets the environment form. + * @pre None. + * @post newEnv is reset to initial state and editingEnvId is cleared. + */ + function resetEnvForm() { + newEnv = { + id: '', + name: '', + url: '', + username: '', + password: '', + is_default: false, + backup_schedule: { + enabled: false, + cron_expression: '0 0 * * *' + } + }; + editingEnvId = null; + } + // [/DEF:resetEnvForm:Function] + + onMount(loadSettings); + + + +

{$t.settings?.title}

- -
+ +

{$t.settings?.global_title}

- +

{$t.settings?.logging}

-
-
+
+
- -
-
+ +
+
-
-
+
+
- -
-
+ +
+
- -
-
-
+
+ -
-
- + +
+
+ -
- -
+ +
+ +

{$t.settings?.env_title}

- - {#if settings.environments.length === 0} -
+ + {#if settings.environments.length === 0} +

{$t.settings?.warning}

{$t.settings?.env_warning}

-
- {/if} - -
- - - + + {/if} + +
+
+ + - - - - {#each settings.environments as env} - - - - + + + + {#each settings.environments as env} + + + + - - - {/each} - -
{$t.settings?.name} {$t.settings?.env_url} {$t.settings?.username} {$t.settings?.default} {$t.settings?.env_actions}
{env.name}{env.url}{env.username}
{env.name}{env.url}{env.username} {env.is_default ? $t.common?.yes : $t.common?.no} + -
-
- -
+ + + {/each} + + +
+ +

{editingEnvId ? $t.settings?.env_edit : $t.settings?.env_add}

-
-
+
+
- -
-
+ +
+
- -
-
+ +
+
- -
-
+ +
+
- -
-
+ +
+
- -
-
- + +
+
+ -
-
- +
+
+

{$t.settings?.backup_schedule}

-
-
- +
+
+ -
-
+
+

{$t.settings?.cron_example}

-
-
- -
+
+
+ +
- {#if editingEnvId} + + {#if editingEnvId} - {/if} -
-
-
-
- - - + + {/if} +
+ + + + + + diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte index 2f818795..85fe7b93 100644 --- a/frontend/src/routes/+error.svelte +++ b/frontend/src/routes/+error.svelte @@ -1,18 +1,8 @@ - - + + + + @@ -28,4 +18,4 @@ Back to Dashboard
- + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 09b56c5c..2e3ef853 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,30 +1,24 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + -

{$t.auth?.login }

@@ -156,7 +142,5 @@
- - - + diff --git a/frontend/src/routes/migration/+page.svelte b/frontend/src/routes/migration/+page.svelte index 463dfb45..79f72e50 100644 --- a/frontend/src/routes/migration/+page.svelte +++ b/frontend/src/routes/migration/+page.svelte @@ -1,4 +1,4 @@ - + - + + + + + + + + + + + + + + + + + + +
- + - + - + - + - + {#if $selectedTask}
@@ -490,7 +507,7 @@
{:else} - + {#if loading}

{$t.migration?.loading_envs }

{:else if error} @@ -501,7 +518,7 @@ {/if} - +
- + - +

{$t.migration?.select_dashboards_title } @@ -535,9 +552,9 @@

{/if}

- + - +
- + {#if replaceDb}
@@ -623,7 +640,7 @@
- + {#if dryRunResult}

Pre-flight Diff

@@ -664,11 +681,11 @@
{/if} - + {/if} - + + - - + + diff --git a/frontend/src/routes/migration/mappings/+page.svelte b/frontend/src/routes/migration/mappings/+page.svelte index 89638e18..89ca00c4 100644 --- a/frontend/src/routes/migration/mappings/+page.svelte +++ b/frontend/src/routes/migration/mappings/+page.svelte @@ -1,4 +1,6 @@ - + + + +
@@ -226,6 +228,6 @@ {/if} {/if}
- + - + diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index b5fc64ba..500cd2f0 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -1,879 +1,83 @@ - - -
-
-

- {$t.profile?.title || "Profile"} -

-

- {$t.profile?.description || - "Manage your profile preferences, Git integration, and access view."} -

-
+
+ - {#if isPageLoading} -
-
-
-
+ {#if loading} +
+
+
+
{:else} -
-
-

- {$t.profile?.security_access || "Security & Access"} -

- - {$t.profile?.read_only || "Read-only"} - -
- -

- {$t.profile?.security_read_only_note || - "This section is read-only. Role changes are managed in Admin → Users."} -

- -
-
-

- {$t.profile?.current_role || "Current Role"} -

-

- {securitySummary.current_role || - $t.common?.not_available || - "N/A"} -

-
- -
-

- {$t.profile?.role_source || "Role Source"} -

-

- {securitySummary.role_source || - securitySummary.auth_source || - $t.common?.not_available || - "N/A"} -

-
-
- -
-

- {$t.profile?.permissions || "Permissions"} -

- {#if securitySummary.permissions.length > 0} -
- {#each securitySummary.permissions as permission (permission.key)} - - {permission.allowed ? "✔" : "✖"} - {permission.key} - - {/each} -
- {:else} -

- {$t.profile?.permission_none || "No permissions available"} -

- {/if} -
-
- -
-

- {$t.profile?.git_integration || "Git Integration (PAT)"} -

- -
- - - -
- -
-
- - {$t.profile?.git_token || "GitLab / GitHub Token"} - - -
- - - -

- {$t.profile?.git_token_hint || - "Token is never returned in plain text. Leave empty to keep current token."} -

- - {#if hasGitPersonalAccessToken} -

- {$t.profile?.git_token_masked_label || "Current token"}: - - {gitPersonalAccessTokenMasked || "***"} - -

- {:else} -

- {$t.profile?.git_token_not_set || "Token is not set"} -

- {/if} -
-
- -
-

- {$t.profile?.user_preferences || "User Preferences"} -

- -
-
- -
- - -
+ {/if} -
- - + + diff --git a/frontend/src/routes/reports/+page.svelte b/frontend/src/routes/reports/+page.svelte index 70759e96..36cbc175 100644 --- a/frontend/src/routes/reports/+page.svelte +++ b/frontend/src/routes/reports/+page.svelte @@ -1,42 +1,19 @@ - + + + + + + + + + + + + + + + - + diff --git a/frontend/src/routes/storage/backups/+page.svelte b/frontend/src/routes/storage/backups/+page.svelte index 9aa19d4f..c86e2305 100644 --- a/frontend/src/routes/storage/backups/+page.svelte +++ b/frontend/src/routes/storage/backups/+page.svelte @@ -1,4 +1,6 @@ - + + + + diff --git a/frontend/src/routes/storage/repos/+page.svelte b/frontend/src/routes/storage/repos/+page.svelte index 907aa11c..b62117e9 100644 --- a/frontend/src/routes/storage/repos/+page.svelte +++ b/frontend/src/routes/storage/repos/+page.svelte @@ -1,4 +1,6 @@ - + + + - + + + +

5B-3dnj%u%@3v6 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 DUtJUC delta 503076 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ℑI17V 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 z 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ಙ=__FGoAIi<*GV7=s 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 TrQ=FUS*sdU+ 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�HMiZBnN`N+bu11avAZ!mYmZNRXw+X;Vgn6Igr;Of@U zdC6&Hy45eR!D-))&&b$lO;ZyX<&yO`C-O&ixi6vnxi!y#8twophgN34)15+H5k_$OVVZh-#8 zP*An=cZH4si5s}LnI_V__$9v-2GqD|82pvhmX0H5lbk&p!n|~9f;;@1upQ(kyOn%w zHEQvs_Jn$g8$B>rZr+8Pktn>oZ?{@r{L)g@a67?|aFBR2D(Q^Kl&wMHt8&Y!a114zQ=m&5!Mnq$+;W;rS zGIqiZrzcvv&3Icaa_Xh?ALwjKIa)-a_L!3na3;OBatG-cc}7+3^1F(z%1Ks0xhg_o zif}Lp056HwWfT%kKZJDB5(WOqMxKvkXmn_1Yh_)r75-4*vEJ+OXM#}A?11npi)+s&YC%%@IlPz7|T1M;cl0H-gSLf2DQk%*)L-JB=v;atvxemj+S%v*zih+zB28r}$QF;C1 zs>L3e@N|h>N8{#}zVUG4T{-1XyuU?em{io>IGS?limjwCOv>Tu%w zI@vf8Pw_Lz#_u6yM*F}@Cu$MW2H(wKbK$57{JLbzjW7%&q7RN<*#CWz66;y`*@&K4<52%F>gQ@ zg-TfQk(s_v(*Im2E;Pq$q?Jv3b75&`D@9{B!yzH$j4|x_<#$i zOm+(Z2*Xpsmf5_UK;Eg z$~b-seTDxP@TGMlH-izb+CVKzsvg{HBWM{4e?IW|?=+#{^B$ppAFXCOd@h%>?h7Zo zbq~U4_9CuI;5ckC2$;Ofdepp@qW8+CvIxb-8DK9HYt6-src0?@=KB;9354=^J8LJq zy9FTl!DM&x74q@j$?h#l2^}brfasBVN+^V#7&Dj2n7NEhj4I_MwKA758UP5x%K?bx zw8yS(XUr(}56M)Qr4j|jzLLrc_Rd%Nvdd+Z0$fdcPr~cLLr7o7N3gT?#fZO%GUFOx z%+l6vFsWS(ru5=a>C)A;lii*_pwQ%%JeJ{BFBNrd9W-KV-l7qye&eo)q-j`H!bPZc zV={+^8+otskv(Yf_FKR-E1=-X9nbpDTj@R~9i8RnjMnI8;C-RN^)Pav#GbhurT=m` z3T~xSUd4JH-X{DE6@+3``BaXF4uqfJ4mw%D!=C{qOHo<*O<$Pc?jSRhxr@H*)2jV9Ao*!!Y~u^Qk0IqF2LVq{ z=XWjh+pS(iWrAmz9=s!lGtgvr!-skHoY<`5A{sQn82*|cB`=n^H&9T`E%NkLcZ3!Cv?h==fzI z@UoFQ8_WWV%uKMR)jfWY9L@X*sGfwJkTXz5u=#HIrG7RhN|y>O5Bq!IYjl^*T>9?AIbFisNS7?oqPnX zZnJCTqoUQPw0%GbItF*azoaX|YkuXI#<0_#B>WV8KT;N4ukx=Yb{J3tJnh>x+`B3l z@Jy=wFmE5s$ZAc2suHo(QabH2?s?c~32VvM*WqL-LD{ES>N>Cu{Fou!Rxjo57gjZsVVFiZu9M!dB~yk$zf z<#`E@S5A9Uhm9?u%@=^%R$NBqYjFX>Qp|I3IJ&A-KDjwaZUc?qyksh$rN-CAtL_LZ zLe!#ccTDx227-IiCelySKzp3*&OlDDV=#t00U$QYNJ!>;h8m7S2_ZAnx-*&PuozWO zv1hyOeHD?aAfnk7*Klp@fHN4~=Ejl8*DO8hRDu>1UQj0C9y z?Az)V2tYU!%FIEtvomS4#mX);S~4?j2cr{~B(2KK#%?Rt4c}}M*-@l&yetI+Y#sPyENJCg{1L-?nsfKNko+> zFfG)rV;Xvc^j4YJtST5sY~xA;ePvIu6xEgjGMR3O6}bFfCNQ-T4q^-d5YC$gz((Ry z0Tz+2rAdsAh2qpxtXgKJ4f;)(N-zFs<+c&6X3c7}n#`9$J(fsk%$NMgNROCbl;okD zsNP7KZHEC+wi*fX`sGpEE}Si6_Xt3Wc_J5f6_$?lBIb?8SByrucq|QxaaiAj#G!yp zCQs(3Dz$*Iag%g?Cjhae(rON;k4BFnGn45evs{vqulyf~a1reGf|N~1NTHZ_#MYf$ zVf>q;PM#;(Btv`d8R__=7E!NYd8>OtXk>f-2a89Ot_<-KDa&fUYk6l??*#YUPO>rR zZFPIz40h0Gtc3l>iau)SrBJeu)vR5>2a)Uoo;~J&6jqE1qabtauj)rD7wn21+(#g{ zBu4>qZ?}lPy0X<}Z-LrQAKeRk)YnVG`fn?ZDlNu zJ5AK})%5t}TAJV#XvEVrr_{7lPbEF(OT&->9=N0 z4Nq`KQ${B94KhnnI#p6B^BX3+wZcU9n*hj$XqXW>-~%`-z_&=pyejuy;0vFE-a;^f zW$JsP)t}$$J{IiuDkP;&m;H*X4jSo~`$CF+p15V=GBcUm zL5|H3o++h%Crdv7yq^ky?erp^72sC-UjXLb1prDE+75+J_4Cx8KmA$Hkhl|q$)7{e?R^&-XWPAeYK-itS%!NWS4U%b!+|0r ze!;*HwCw%BdTUC=rw{)70O|ffGa}NJK84O;SY?8T7zKho=V72WRn13)gR$sMry2*? z&3QnuFTbj@uNOI{tHo|^4>)fL9}_$pBks@xbB(7V5y;GBenn=qxXOtL1#6rB8j%k` zn0XR_!dM6ecoaz{0CS!Kpqx6>xZ(7OEx6&1n67TvsW8wqyzudlh z%<_4+{oTUuGtl!vzT&!8oWU3&Gn4r}nPrHgEh{(AF-8CgGfxBHt0Ce%~0+aNb~sX zf`+*-4b!G!ZYmJ^N(+i*?eP9ZsGP}GF>YtvsfGF)1n2^A$CWbyhsyU{2E9OF$odvV zPuG-=fC?sA$D(1gle9XV(qPaAdVWtvv8{d$Nu2d7=P>4q(kW&KLNeyNyhq zE}eW^jH5x}IiNpxUydcKUC2Z5X9QMhO|dmV^43~raOAHD1m3iDx*Vo>zX9*$RwwGs zk$4GLFf+O&M_MIuuS55$DTV$*Bv|pNS>yfI$8c0M2!gr4g8&nX#(q}ee)K724fd5` zDIA#$t?A~*cZ)DBQ64;x+X7zCj&eTCAC2oyU8z@LaMTa|y2pb+Svv>ugV}z>$Czf0 zp2O={snn~{#XhaokrMU)ATUYv@lcuKQO%(Lz?aAk9%e!5bGE*r;s`d9{}Dt=gBdC!z&jhTo)G7)=t{A zf;fZhS>!s@0j>hvt^c|8phZY#DS_u@Fq1a@bwq={dyT=J1`)}D>!IDjTusZ_;OQKB zf>U2_K#P1z9g@6|d}C5+tvQWxE@2allC%j)-g7FvwjeNGvbnz99MKVfBbG8A)$PBF z5QzQ%`Y|F_h#E?3DxI54P6)|Ljh)*81TjkA&{*OeM%58`X@;xjSW$0GYY`rwX9K!>1cpLg5g5}l^pXz~ zxf^N~1i@f;5FnN-MkPIh-bQ2R^w8L90kyBhm<&RQ*Y;f|drZ{z63?)QhC^R2a322MXyo@u!VRG6JY&3Sa zlM!2+l(BhEs)nFH@HINW6`3t@S+}74!?cnIqLNMQ`_&Sl7&OdSi=1L&4@T_cc;fWi zTT%XhYwV#cLhb%*pQ^WsKq@6QI5+luiP02e2fUR=8|uR5C`fAU;?UD zV}~Xx6}nBcQ`4zAlVAzju3>*F2r9c(V9MDTJFQ(qFbR00>y42`$oNedI~n#1sKd`wA?H!lVhq{#`5F`8lF-R4_ zI|eCo#&^PIM#`P5k;1$AoUYBfJ8Z2s9yWt))S(u_stP%GK3EG-hpzzBY9V4*;{z() zT@TMj>O3CL)|Gs@GKB4T7;uu z=2RK(===QlK-CZ!vKo;1e4Pwygi0y7)fA4m_;2yBi@|>{>h`dDmP=t%_h36#JTB|J z4@msRU|F+*%*R*kL$DI~XoEBc+}?@`9jsc~jQV<2OL{J- zoSqvE16u=NK}_uPAW0?Yc0LFk?Ts)q1Q!7Bs)aj1siJ1w_roI?qUWYEOE7_pu0e_;=MamoWwP80i5RKpy|FEk0gqT@MezRU@cCiZ0^8TcA97hDZC zF;+*#;Cm3B2LeO6munzuv8FDvtYBiV177O3pqmR_fuEYan3^^u{|?Eknn1TJL2!Kb zoi=(E@TyA@gNJ#KhcJTG{KsE|z)ES`9_KZN)+>XURexmBq;M_jdaoAYIuQJQH2W|r zAAzBsH^2k?C-L2gNXLt>G-X@3O*9YH&UgcwtR~>O3$(KFa&0HKpw?ek+++xLgAJQ$ z-GF*lROnWaA*mRZcQ!-ZhECMPxn21jctc&FP52opxpR+>_Yrt2NH}EOiNd;UV`vKr<*h(s0#x&cHQE-t%xSj2}-Y1B!##DQ0bmy<> ze&18qcxU%6kl;2L+&592agR8~!5(nk4{kmV^E^o&*p6ZjKE(zReayHIL+&_dyHrEz zBE6)VvxjG(U{dm?dL>VTrOABQc41@HQ&6*EDxs$k=s0kWjKU|}>_p9LhQ{mwftlus zggZib0(eIz5w@w@nNcSnrN)oJp(|#);OR2{Ex0+MXAqArewd>X>a$Q05CntIfxzXI z9LuYfm=UgU?|@z)5SJw*FxK+J;G5XHH8+j=F|#uTjuTnpb}~2k3fFvNLkHB*zXbJp zTT|-{*VOcr=43-7*Z5sf9|VR%XF`a)l}D!w)%=;*ufn#N*smd3e3}upUz1CdpN9Hq zZ0q$f)f<1O!Zwm;&^m`}JAVs-oXF|+HhvGmLC`TAO?vMDufrKE=jiSrecaDtIHL?a zGqk4b_Nyg&4|HaOQ?$!5@!zMPo%;a@Olj5maZ-wZj$)Fch2Y`Y4-zS9!cXgC=)gM= zQs`aa%)Lg@mTmYRfGOCgQv3u2PQPd}r~eY4N8gLUP`Cdg@c91PnHfI_yt!2n^8)7c zSF}4R#TTUi*G&C!)go@jDe`sLbq3seFy_?BHJr$2QB@PZF$G zFd%wgMHkl+wp`Efi|)-eqO)4c1P}y+#}FGn1ygtFtcY(9hK(k6JYqYWZgB#dv!B$& zPG~9jn%IdT(8EL{3gdFczlyPv#&(a8KOv=T%Wf$tAE=ak{|7sNm|b$1UG(Iq_}3uy zP3pF%BhY+dsQV7;v>`hVL>f+fG3>n{fc-4PzLTZ+))61!I%hd$w3JuDA()#90xYY= z+80xt0Pu383rx5tOMkXO_5;D7Qb#4DMfB~3N^VpYXwgz;eTXtyIW1)%FhMKSQr0LY zwztgV|{D_Vm-qF%$zK2)5<* zU{G#P1B1sL6aHZ z*BR@-J7)EgwEolF&Fgn=x02Q$zV@nKkJR-H47X%(WwPa!E{F0h-;O6g&9?@-y_MKF zs8Q$ci2p3#Ds_9GNOcg8?O60V2BU8GRJr<#e5=;FJx-22g$_`1zs$FGXW-Iz6LBTs zMQKv=6|yUS{6ERPbjJw5*DUX|=yf%L=XbRFCf|C?k}=QQmM^dn8_*P8O{l+RFoFX1 z$wc*UAydE0xAJz3!~fIcJpa+HE?beyz>X(>%(otOyECQfXuc($J#fc2G}_pDR&_U+ zOaGtqt)=c;5yk zXDzw*nAh^lZGYxlX;)rc=a(Zt<>UPoyf4j}&N$~;PF(EPGzFQuW9BiH7Y_B3i~h>D zj@dOVM}CW&))ZJZZtJE$3#>uzJ9Z3k7g(3O)9Wdjvj`jHBMSr`e0v5zu2-SeLQaY+ zuyU=}JTetO2HI1Z_Q-VE07OmB=|q(4cc^$~F;nHz!Bb$Zx3=W$*p43wZmS=vXJ0QX zBrQ&|Ex&aTAN|D_SQD%_JTfq$z?y0=Wo|37c5DFRkY(fSBg6FibXns?YBtX4Fo<30 zI}&V-oQ{of22%{R)%2I{sPq?D)7(}dYsdEF0&APwdq3QC4N8?2sRdSd_v9V-08Xh;oGBw)6j+xgwJ_ggxe{gPA3adl zeZPes%O42Z*z;`);OuxO&PUJ{jr#%cR$X*g?ka}Aue!7wO(1v2;hX|%wq@5r>KbED zx=hW5E(G>7<2{*;#}IYL#RxjVZDnT2y{!tYbxBskVvXdIlDq=cS6zzFN|O2bU16_< zQfkocX)@~%I16$Z=#6$eEFLTRjdZ!Db%8a~JCW7jRX)YLSfexKqBa_^$?H(wCU4_? ziM1_GHoVot%CpagD9i#8s$C2mOe@@#CvP^({ejth1~Un*Ya5}BuTf=K84FTrmbmGClpv0dzS0C zw`7err4NB_=#Bz- z7qHTgN+sSOv9Zz4$mz#qnNS2qG93R){70#*H)>(7{Y5kUBm#!Zok3(fa3WKis-6#@ z9c_9PSZUTk&zNzO!q-&2L^dxI?jd{?(CRln9M$zA@vkMxCTIW@i-^IfEv3psHf8Ov z7`Ju#oJ#mRW%45&btSnCCA0A5R^*XAJqxT(8LwhaU2z{1?#22lY*#R_AFH<-a1+ze z-%`}8zzS4!q5Q{E{z8TP5u~0W_~1)v@l2%tdMJ-L!d5U8apNc9TomU?B;H4u1h`=U zQnGBX-22CN*qmQ{1+Y2EcwMTm7jM9;Z0TKqw$iA1lHK_2&&Vc+9KxViRzi=1?vVTm zf=+>!49FwQ1(cl#3Z%CtsvTh)nc1hn%C;>EpwKH9y=+@~?ryRouNYPS1qAN%FQA+z zt1cLOVD&{dPb)??lTJdWdJZDP{)7Vv2LoDVO<^gxh7gZ)x!rOtnozF%IMcRT$$PLe zKHJNDj6puuC8H6KCH*+BZ0n;LTOto5!A|m4n_|Suxf!j_a~6nXW7}f1=(~0TVNFBk zO1Gm`zV{ol@102|qvbigx3lCr`^@G|Y=<*icNN|T5B!ZZaIlC+924H=S+U>dL& z5LNA!=aJbwDMXB1dDn~PP<()aXA>?b+zM#*i(1wd#Gm7pW7GDdfaei$E)m{~h`3Om z>xbqVxR~#k6Xs}+>X7k(eHl^z^2*5jA)GGjh`5Vz6rqaW8iviAe6E&O;GHYWPKN#K zyMQ_R1e+yHH_7U*@MARRSYTJME{g_u0&^Geyzldjg`l}d!fzs`6i&kYXB&yn$f!84I>5W9x5{Ap5&@*cMq_AZ=o8ij|7ZWZ4jLp+B z;+NXe@m6hE=M!-rpqzfAjZ9oZ)W1OUIpmRXF%i=eoH{LsmyIe3TB}i@k1)sE2ImWf zbK+V{>UD%y0B%5m1NP;_TxrYVu_&FqG7N%tUnOq?wl2Po!PgVUmeq1iGOrcN@Z45Uf+GH!&TTRqLnThdR zYiof|K1Lq~>wZNtSYw`JxeC{qet#2*&x*tkEGB9Rp%xck&>}VOa-zFggC=+3ykP^y4^6Jy=rla86% zW>05D3lMiH;ucce?33V9Ao|}*#BGFOHBcBgBswD7#PHh*?*Np&7$5}fJBhkG$>|~d zchOSBn@rnKfWW(K5a-gZxX(n_Ov_iYSr4Hx+=juwEPD(};<-!dwqC7< zq6_ltv!7sMPx|DdM^J!(jpA!&TM}j%acIR_FG6iEP#w7r<5!=(hf$x0j%rWlc^LVW z!KW(oFLXk}vrkj2J4a2!GEC3;>`9d0hd${YhbR@FlE(qUR1T6q!Mmrju3?F01u+lGX#C2I;=LtqHs z3kkcLh|sQp)!8rG&s8m73QB+A6jBT)9HEqT6`=GEB5I;MtW5iVP7PVhd4lKoJ`&De zTuoFy$u#DZSr4~k8b_Mp%>?YlWVysIi(gSiIFE?)35_U?J+a?mlj-G=j^D$_>iHGZ zT|vIRIGuGF;#!rp8r3|r>3j}Rp)W-;x%OQY;N30c8jN)F>@`epyfQ|0#(V$CZ&Ai$!M zqYaXbfpP1=S$yxvw4PAX*$W`3Kof&HGiZA>Xc>dLFleU+nGuKmB5ULszYN4QcV3Sd zNc%kD%Y=IYt$tCz;Vt6dmOnLT_CCJ9>!%+vgfjiWlBqvp>QSmshXLrhl?i5YLil0i zDXy|A;^-bBpHB%t1C+gmTEfFbeMP8M=QN_g`{epVY|vVm12`o_eS?g_T;#cbl~yNf zJ2?HP9`FNZoPk{Bm(es=P(k96t5^kzBNIDWKHjWMQsf>)?3B@xBF!cYf&k4r=xs|x zJBgpJy-hyf3zB8=d~lEeoRVCbkHA3wU(EjFRJ!jmwPcmoIm(Iu0~mQwvTVa7N1k^$ z5yRJ)CB|j8v4%{H(2iozXhIE;Bii3uGn3`mUAxhc(|-OG4H<{08Rf!tT*ix;gBa5M zS`RBL4*8KI$Qu1Ndm))sg2U2X==o}sW#vw_-(zysT%S%>J8>sW_e0v3GQzpZ@@hsg za(p2XH%KoyP@TLR_`X*Dg!*QC*Yo`vxe(#41Ml$tHo{v0Wp57@L;7E-zQ*Su^9LF@ z(>|BIogY!Kcv8D+XWrzH&>V-hoo z{~8+Wu(HdDnw{cIFWCu1l!p;E5%Y=gFC-!vP%i$xhn1VZi%sDd2K`0Y0iB=K5S~DH zx!Nb;-au_MKYvYu?P`x zKPoFg(#f*{BywPl8AIsGhWAd!xkKJof-al+9UMIA11+9fHL}aj2i53iTaw^@E##zk&LKBs`H=JzCkOm-sQfeeq%cU43w6I10?Q0B^J3`lG_45HjtaigY`s&w>8VG_xN zU1h*rG{)jgzGo1o6KWi*Uvzl)1+yARl?0R%3n-#yY{p6Yv7zP}Ua=wKnIKV(e7DRR7M<(s zNwQw4&RE+Tkt%DpsO9W(o0>-kk7clZDiI|pin>lRV?Iood$CjkcCyM-Wz%^mp=>ee z3V9f?bum6HcDg4*oDXT<=K@;z#47yYV+?~|kSg07v3g+TV9Ieb^xr>sL72wXB;%`) zmc5k3%Tgr>ZAV8%L~}T^7Z7nF5G|DAVj?afh5O$!V2d`d-N>dt|0)JwO}LKmN*jPlH}fm9pOI0y;)W-z?!xU*U4(%ceVgA4&x4nP=SrWxhv?WQOzhon&`cs&w0^s#k{n zW}5Z`6hYu&QanhgfiwwA*p8opb{doLJZaud;>^s8K*-+H)v9(WfyJ?2MXOt=&V*T( zw_jm|y%2*AUfpj<@*-58=Pivm7>br7FX|$dJ(FqeW4>%0VPrD;L8^>QL(aV)k=Oh3 zFj%y<4q!s87gp#}tYPJvY4iLyRla%Hs4rY#pFNbT17D`f(FCOL{f12HjkhK9YtUxo zf(N$a|62X+buoYDq+eSO;CT18swOfxBZ zItS@rtIcJbi~ww1=;oki27T3$Bp(52&8ZF-%5@6f<-j)EYLORLUJ+SsoIvvNge%e| zC4^M^pGw3e!U!Y$h#NIIvL-W?;inN!1C+Fzp~iL-c_UnykT+XP!I>bdq&dngpSbd7kSLE?XZ{1-&i<8WFgW5pE#VoH*(p zIG-ik_&-73TZy_wD(f-lVQ=R9ooTWder8_a9wP3Ijk~c8q~m{-s7C;uC1EeT3XdlI zgnYE!Mk#tzO|QUFb<1J*0 z`)#7$N|TP*u9auMOT>GKyu85bXnh#w@-agW5H2xXkU${tAEFKtE;B)h9`GL`>TE^5 z3skQEbE4`LwG&nx^XL@pmrkmA)?qA=oQ>{o!#Oa!fo~b(2;mQavJ-7Q;Qx-O?-lh+ zlFqfyK%6wspAj1S7t;I;O+{EoD=u9|j)HQ?%xAP$e@|T}OqmS8KDgZEF=wja&Erp( z2`do6UdH^TVl$3UjpV%?M*WCOb9LUy9?c4CkuHnPtXvyo>-2c-N9GC2;GM}UGA+>BW?x*iV8jN)$om7&p_ywDaVfMR)!($~RYctzb|DuMF~cK^eZ^SR zNSD0Z)NlG6Z2f_A8GJtBd4O^Rb94c_B?XWBL_0RC3@#!KuF^Eg{!3|_F+|24KgP14?I%x^nGJMBSM#o6=A-?z`ot7j;DX*k}3BnFnON&e}=U zMVPR*wx!EAFQRKn9X2AIJ=Y`3s{~l=IfUPn>BoI)CK08OBZ(-w`$-w_l5%-W3G}`* zy1D^ep=D@s|JL8)Sh&1t<;$u7k6{qw3k)O0JO4vz<0cyu`CSx2JueG4=d3wjo56n| zSTwShlA~%9z`YlS5EoDVJ+&Xv5MDLvb}PJc8&@lPli4!uU2I25T_iRUaCx&;c|w}0M)Pq~{|s4k5e#oJ4t+G)#uK3CIl!lZS&Q<(tdYgWn62=l3_zf>i6!r}JPG+H*Rh;0!UsTVnO{O1JB@l`c(1`2(})NYs$! zKDZ=uBp>RRfg_U)hrZ0w*?##3tb-Z7DWVhUc})t)gp0AyDq{&_EF;9Z24*>1Pn9vO zUe9#g^T2oXd~mRd3DBH6cE%ETJiq0m_RJEQRgK$)hN^fzPd$YEZe@gf2_FWOog0x~ zua|?RXCxlF(%QloMJq2s)!MI;79Y3-)8UcGD?lgt-zEBczzyBO!TyyPd?S~zTJ3Ed zCsD9dcpJKvJUfAioJ_d}JOA?RJR*M2kfWz+6d{j}nUbKBUh?FnNX+g^x{m_Ro-}(1 z5d$+F|HB?j#3`9{xJ!t54r_ZDn&;Vlh?th?%%|DYi5Q+DBPW3(;{`53lr!KK9Eb96 z)Qml!h#%u*WF~Tzv5<&Wgk_n`R}B$IsD|Xuajv>tF^PZi{YCHZwI75c?VH^~sl{Y-gYeLVX=M0^6B0FONT5E0LS;7J$3_hIc%(U9qxt(V6eI~q9EK^#4Ao0spx0Bd@kHLzl!(k-{Bn9gX9qn|6 zqy}W*T3D!zOd`_Rp0ff{1InPC&49FkJP#)#&(0yDML@Q1Qm-<=C@mRanrj^*2kbne zas!fZ1w_}c3lY6p2&D}_aT89P8vfmvbo~kYgtJpbL_t74#>!ltT};G@VZ=#9oQ(YG zXkQK^u;NE=4)~_83MO2_fhbfjyRp zo^H8j9ST-_DiIS1O9;mUS|!o>zNy5Y7GOi3M#O5YKB&pJrxS5H3sgLlh#7=o87Z`) zB6F27d=7-&1(^)AqII9ofZri>+4&$UCZn1tKQ@#qpW-Z;uu7*HyRb5AV-j-~W3xZ3*2IQAHqWQMXx8K8GI^k%%qC?)J^ zu0*?lfew0p)8WP7<=e5{em54I12P;|CCgbiwl49@8ocJpaUfN?{HM#iIanDIv${;DQwCe!S8Y!^=ip8cfwgz&$BQUsSZU>_ptV8GdAV{ahh zn}Bp23nLV^58o57N<-g&HBmn!oe;VYY~68`>URd*#J=}0yj#P@X;kxMlP;JgA7j9p zXNQQ$$&!xOVW{Qfds{$f^OSW$7EU%*{prZC&RHBx6%tX9C7ZBcG|%ozME5Ltr;X}s zFCuz|Df$v|a+b_(iJS)p5^*A-W|P`Kl&HbrrtYf^kIyun;aQFmagUUbU<$Cu1r5n( zk7w|>EN4nHP(sA1god}7B$qAM7g(J$rZHeD;pthj0X}Tde=8AVP@ys#O)s|}c2t=SF;&jN;oIW*~9N2L#J>d0;DJxPGP^(IfqyCPdtZ5=Tn;)-bmPx zCC6?+3l7X=L#t=N52^UO(5Fzgv5bmr3z7%)8E{2HqwjGj=Gk z)rS-_K48$b%;kDqgn+LZg8e^Xz_0(*YnCsH7?wk%J4pCBAZO{nLM6eVVc2%QYUj74 z_y&a&2%`VbM0`hhgzzUo?6<~cWzO>h@jnuN4=A(WRDu0Y)E`;w$$lf^Se9(lL;@~O zNY3X+IE_#kDMFVV@K`$UH~VWZHpb5bw`4C)BR3lEk**tI3cOQT=LvF}zGoznmyggJ zl$99&1Ov%L_z5-N(u|;h-GZp>pm7aa%hzC)XypXuD1PJx@)+Khur;6@&?zbVbSB>} z=y+ZIJCLS5A?0I$H6`N6xVkXBAcl_F=}J_7&>1fTx)JenOg+omThIgbU`QAqmzv8N zKn~k?%yzAwO99cBL~y~Y6`z$6jMSUi}tLkRyV5zHCrV}`fG z95LqEgBW3WP^OMUQ;}SZ_>-(rL1!|i_!NeZ{r@l?8^d^F(9xjc@r+RN|A{z0CgNm9 zv?nmal%ScB%=1nn;#!G6rNC+vmgDq)VmxeQq~xUxpB;3ja(Y$~G5?<;_KWsyXE8hx zYRomrMxvU6j{nrNfrusl#EaXH%VJ8m9CYM$5u;oXl(|^@#fEYsE{@V%M%2F{1D(?K zZXn_&*@NAPt^41^_jQCf(0*TwcdIhuN4c(I_*z1hjyQW~t|00fd@YD}oM&H0#O2JG zeFG6ULdiFwjXxN$#(O*d0`=OZtF4F0o!aG!@wm`4(BQNkMBlYD> z#v^!_G<+r)uyzEc278?`Uh1Ti7|)XO4Z^$C=#*ehJz%{Ylvly6qxBl1Wa$p(9B}aY z-y-Qd%G7-J;w9$8u*x4GVm~y_ECL=PVoDMRCP#_*85DOS%lW?%@eAQG!k+-G5s|M% zT)*R^^1F~L{{tj&>1H=)9XpP#7iQ87ZtOC4BMI4@x6dZU!>mkOBk%O8)9+_c3SmaJ z%)+O(!9Y3@$%JWu(roNBiHbd9#Vj_BE%gDPm4<`}Pm#`n8Y(!AN zK9Q)N*>a82x=)tXFm{-~Z>y7%c?=$ad%tx;#$Ly1t8oc(&2l7Wjmnm8Th!qkL(;L3 z8W4H*3zYCF*^ag9Kb{m53AGI2#~Kn@9&??_@RArh*@@5T0)a_HjpS~n)A8=?=?$F5 zpmDLW>b^qfX-^?)TDJ7otOaHeF_Z92Kxyu%0+~fr1*RD9(PESlF&o7&h`B^m;$CQ- zH3-xYQA?<0l9ImKg3ij8lzSnaKt0162$urNYinyz z0Dm*-S1P^pHO|FE{R=7$Pb^RVKnh8oe=CLg_R(HXiZg9aft<}&a2=R;)L!m!QtTpp zQX|*>j`eQ)UDy;YK|_vgx(`{;e~C1T3_sSy=nVf}hQCGuxnGw>XwaBpei$lby@?y? zx2l4?L%Mz0&K~Q)dqlh+lYk8F2$|&nho}oUgbt^x&@-nt@-^<~(0MQqW_*bV7Vhl} zKVKo>ykv;63xwm#I81(D6Ml!g-68Iv^#jzmy;?RS?!d1M|CvzPIaOKwJ5gt_g@;p8 zXpM<#?_Z?*Gu&j{{16K!!r95^AtFIKJb-@2_VPUm*U9OOjO`=BZsBZCw^DF194tgg z4xkbG-5K&V-mr;3Q+?GmQn$c$Ko96#RUQ$o2|M6MDa>jF1MP`uOV|!jnlbMekQpFI zlB-|>vYf2}R@W9%gJC;99Jo?bDQ29WEo2*LVN{6dMc7-J71i~EAss~20K&moV0{P~ zDE^fuVFUwCAsh~9O^)u3{e^U+a0&$?=DA199yHvRviH7T=(0vK+E_xZ0_PhAdje4t zvFjM>muH_!#3bbVDIomQi8vF`X)nbyh?+^L{KAhlHvF{}PIzE=X$$UsnN37l3+Y`3 z=H5CY+%f=Lidq*p^1Yrg%vE9RDq91?7q^h-A5|NEHWBCGn&77u(M-hopio5MA|n3a z_dCjR1HR7*1om@WaWyG+0ZN)1)tr&UIgjfZbQ>;3!&+A`u#t%C2$h?pg%HPnnW&eL zvUcVE`-ylGP#!({6qcXgCh7%686)I>pQxwMUK~{akBIt^P!kD1al@xHhJU}r>Bj$( z?l7jjWH1CAuurAy|0yMIe@2=Y@a2J4f&B#$uMlA!hWg`E1?;p%IEHo%e}mzk?|_iB z^RUfBMuWm{|453T!z_O$;+-(!HzGdp$z1(@-2Rh@oOl`OSH!O@f-8sFisQ81F?<{I zB=Q-siTItA36Vm2{Q9%aC_RM4$x#MrKMQ9-?}AWieHku9OO3MgTwUgR57Xd2yDceJ zQ}6B0M0CNuB08G0J97T7Ajc_0pgSo(=J;9@aK5DT_GHi~Ic+=i+S3p3Qi7(5rE`yV zTZ3@;!AfnRU2MF1#%TCpG(dhkW{)R>Q*!vl%}63nMZ_y~;%*5MO*t}hhvsh<5o2-8 z{t~5_L&UfoS&Y6q&z?)f*Vr>|Hcc-lVjDcF*{Fv8mk@D2;Yz|rz__yLF=}gwAJ3`B z{ix3kC#u3-$&gOi6sY=Tt;e+ySF6Xp8GCGO>pC1af2tPz#bOovdd}tDkt6R6S4Aji z*ZUC4X+DyAoD3h$k>F9C@py)a?Swl3Wosjvp8qYP-d2>;x4%c!yAXof0c#>wGb+r^ zYTd_cf6DMfIF`MgX5b(_w$E~8D!#AGv%e%o53HYPd-gm74V1Oxb+?ZB(69IhQe4Qw zs3L?PYj~uYx_+Yu9fb^x=lLfie&CmP=3yBzPN$TMnw4|kS5!O+ly6c|1eOzl>)}kRU4nNxaFrTY9FT8N7h;#nASijpTGC!kxCW<1gGtbT z2N5@Ewhv>zhEhrP{41dkH24KrW*MS{JZ zh$ll5)Gt}=T|_*Nqm&KBi$r9HWaL6n1ojZ|CgEN{=QB?GBceVG$x&TxvA-o^md$OA zzZ3Brs{RFS&MuBs-7RHP0-|`6h?pV|gP^sY!uQmca?uk&==pL72P{$DduJeTPH-E6KYu+~1u!=mzLa z6MKsE=T10P*p@dPI^XX+n$u#^Mid7>;)dXWem*BMyeDBV9CY}Ss&Sui=FY)ul64Ya z?UPB^FC6}g3Gd%h-ocNK)&R({Sj94wghOy}qp_r;i1-Gx5G$Z{fp0mKp2&bx0Huu$ z4YelWn8itELx^rQwL6kxO3U@1YjYm>;o!GWo&b$s+t?t-gRA_zb zwp+94)8P6R7osQ~1M&@iEbj6#+uPH$PA~EO3BF%N$KNNbPb{?N<&ULL^9(29vDeB} z{cOft$LYEX?4#;ghy%a2$ZEjW?sMcGz$Jy4@;4Dq6Dp;v*@od~;9UmyE{1i{ zqMalgaTwSMW6$CI=5D4FVE#@ao68x#nEBaBy5+=w#8JW6xb-*R>CTtILv&Z&l-`9_ zk)2Nupft^hR0e02!Nq?M6fNa2D3aWRcl-qSnme?e3V0uLJDIuf=y9TImfK2E-$JXf ze?GJJZ_@rmYd(Q%BhT{vR?-b8z75`GD-2AFD|aqxv12!w1>&spFreyRXl-cw0gc!X z%t|&D;#zhF-Q=bLg*dYAn>0xosC9mJ`i^e+G1c;>(AYhb1nbO}Z<2q7YVI%^3C*!h4w4s}fX9 z?PTXuy)2KrMn2iI+seG6ji=qGB>0T*Pr|PWzf6kCw~LG(R%qq6OXt#K24NN!1uQaClPjF_(8ru zNjQVB*5t39q>VUWod8eY5!uKvHD_4HVNlvnhn}ihgXPW<$WqQ^YU~xWBcgjZy z&u+JY#jD{wWfNP=9JY>^ji#d&jD(`PPnAW0|0=$R-1d_DH|(TSs9BFOd>{ugyL?7i z(9#^KgSVVJ?g-O8pL#HqV(mi?r5rs>=ljRx*op6#Qn%--5hx142xt@7aL~3q;vWrv zd{Q?yxMvu%h8kpwG7AShGsAFsuo&?d}+?~S-I|U@)T0Z&R@gf z#2L)bZXQXN&hjlMmodpk0#~&0%1QXTF4wz+4dgVLiT7-KDFr^nBd?l`Yil3hW%ZTU z5!kZ0l3Zr6s@Ks*y~2(emna)W_A%A&p7p!7xW~%;u}DhB<@vDv^S=z+&+;E4#NoOo z?wM>rhd?J=(Tlf`Hh*omdPv(7d{#^MKxuUf@|iJ&`8=5o`^vu&+0pTdW#w}tDSEo}A>I9y?gG9~C%;C*eKPYg-BYHy zK1p^?RC_XowA0x153vEvkJFl+E_FEOEms;#3avKwIi&qH%hZnH(^ifkEI0o_hHWFf zpYUPA2MD*uN=_b~q;X$l*e=4C2%jc=j&OHu+=Rrkc?)XlWFr(V(9`AmjYPjtmA+>M z#M8G~N6+M3YQk@+rVz#|l}tu9LK9h46G$hu{SK|CB4BWxZO}i_<_{yp_balHq-XTL^C_yoc~M zCqYxLjXz?n(e5xqJ|SGdHt_}D4`J&zl3LvEMJAO;qtKeLEnz$Czw6_(rptk;$XUC7 zME4@>OW2#RPfTgLY(O>T7FV(|){x{G8qmKe`VQ>XaI3D7=Ic&BYYLP0F$y!Zl*aH9 z8ozynUD$;rvFjK`ga0@S`#QUtRv{x|HN|Vsz;=;#1K9(dOa>zeXOi*BnR4iKNGJbe zO6LaVZ6)E0Oz~R2@5zj<#COktr~+MC?rgTMLcGg?OR<70N6sv?#>kkg9ahVBm$Pu2 z2>sOAc*3NZhSx=keCUhXoy~^ZOufIHv^O)U1#HM0kxp*A4Gf*g(CK{NNO;3>DU_U~ zDePkA`%z!^kajm=PK=6ok-alCom7fFlQ4@gNSNjrcw<7k$h+}KCzM708z{&xNZSD? z`OYk~Rxw z;V?j{yHz_!r`BF9MgQDx0fYZbMc74%OAL(?UlQ96WcF;$mN3rS%*N-G@%w}x;{o@e zeA>)L;|W!fX~PcX1~Y$$%Vtd4a18dz;5=j z8S^SXu7V8vuA}A7d}u@dV`R6D(8pvS( zor*F-o-Xl435iMBSw(z07J1vY!a`NhpIKCtliXR>g7(LBJ@-PVGq5^n;O%nt{6cG@ z`!4wuuzQd58NPyWKCLHqlo)q7f%r>^KTAebVHBV7B8|{5Eb@6yGv9FsLbKdMejxci z!biyFXTHD3*07WK{}R4KxC3Wszzyw(ZAm5@KM;QrUY%E-i~eecJ}{~^=P#CwTgplLFV-%-t1) zI|x&$;ipjZdpcd$4t3v-{O}Lp=0i!Juk3%I_py84kffp_Ytbs4?CaW{bXB`FcbaQ( zzRQ<{6Ky+ZUEw+Hic2rJt@G`Z3O8R>#Az*pG*`14tt3~otIcsC#*|^;=1Q~PzSG^s zb*^onaM$#9n?22wT;9QLN&;y^?iuVtG*{RM*q7@QkS7OU!Y;0u?z2>M zA6J83Qpa6qCPr-BccNNSB$i*Y-j-g;uS`JYa8=$j32i11UEfLp6EI+w(d$_kXfs;oL90px!1oU(dXy(5G;oI6V86v`8gmrhlM!0fmV}x}@r84$b;zrzV6?01C zl`rbe(+ucxd1cf|MONFS@0fMjc$r>BF)dLh41hYxjVB$n(pxn+M)VV;Sf!WfL~dMj zrQoyMm%yP*KfO)Uw;?Qcljx){VZzPC>(!gM=PZ0dr)fAhNY}mxt(L1CEpS!h-ZALi z+{9ZMr>45Ltis&J;i~9XS>ro}S%toq&RK@L!}TtXntA?l#5ayBTZWt4QMgLIjMcQD z0;)(wgKNa3@na@W9y8H{llApU#Pz*%tIEsdg}z1BfZXdri3_}Nk!<+3!BW$KWT>>; z^gC#^O?D>A4c|}QGNE4)K8kn_(Q3-)PsZJq<+vnT6ZR|j4`c!wkHAeTDK+PteTx4|`pQM){ zX#(*NlTO=)UTMrr)Zr`y{uuABYpa#QJhd)B^dtHuBOMU=4>wxH<+G-%9rZMON3m#Z{H2v1B6VQWLH+T&!!g2`_kMIfz25 zPKqYvT;}D9Hfoi7V`T}NOPSsY?0NopiOq+CRpTPsh{Tk>TyJ`WNr4J_o;#kt!MTS# zN)HL2R_&?_UvW4u9><33lL!?wKlsnHnfThK`tnMn38oaLbYDQPn+>72vZl1IX-;)j zg6!?z6OO}pxo{vvhyDzYW9lJ_X7lPTm7NG|k%k)9iwlj^uZ1vV<5*av_VW;J!#QT2YtjNHoAyUxN#m)>gX8mz5{txO9CI=QWk}nO8X(c`To=^=i6@ zYGkp*Xs>e-EeH*xh0&}xHI~Y6$bCC^m9!cRW9<7^G*B+T+>c|>yx}^KQc!eCG`&`R zqpZeTOY3T{Y{rjJ`?FEZ#zr?Qre9>Oy zc?9B`AW0mG8gsuWT>;zTeWJemBx=)S6)<^L!+epU2d!4_-SQL=*`60Pp^Xpj#9HKc zKxF#hS5pscNgY;%D`9LI1lTt3Tbl8vIi_ggf5r$u;Fec!@MEItT95|Z_sTsQ1yNrE z45p*)lH6#})nV$i;YC(jE1Z?1Pa-P1s=g z5eeBcaReA4%kuy`V0mq|zWX}GaJx@K#sndunu^M$CDBru%E^jK&LpQPhJWa-_m!vN z#zF^S+U#f2Y9!>_et}me}{D_e0gqE64fv}4cc4`Ba`f=m}K{9vM1v( z5DSf*GBYdScK@)NG&aJoa}os?(_V1kXE;UaGa4Maoq(E!wQVe|j7TpI8wm7CG&jA4 z{X^UtI>eSs5h)a$fMjt)9oRc!gm<1i)RxB)*v%Sd%a=;!Spe3OmWGXVk~^lzDr%37 z{^8W&J&g#^h^nN8<|H;O@nJrZBtD}nfDBqwzhm>II2R^dryP{&@_duxZzR~RV5d? zy}&9Lg6q)frJb%4fKUOVL=t^%`V6?#)`@u}lLDLuy>vQqe z95zTz@18Kh6G75y)n|w^zj9GogDcXq68h5kmeJXu4w-Ss1<@j!yyIUaV{miOI91H1 zB^sOhLbSR44?SFE`3kv6?zD{G%^34hR=;cl zouksJs7g@gtD||N?Th9O3~TF`MOuRo_b2ER7L9q)_wWxC1Qr2TjW(8Hgt5pdU|%OE zGePseEoCUKS)EeLjN@KOTw0<}qUnws(74rIttwC4$b?N*xJ=C}T>?H0(Fgs;L{we- z?P^WS>UBJ|kgdkZb9786^4hqb7@REvy>HbmWLpbo(lB!V#cvAZV=c|H(&RgdZ)lp+ zP+ng})o`WW%xu8*tkR~Bai?-J+J@mx9EK~k6;0t=MO|K8&tt+9W$#Dm`aKdpbXW*ps!Fyy|7v+D{|W00iC52fp=+6jLg&8tiV zrmgj;En}zPV#&!Zu_vXf5`#12eQ6{o>KJ6V9y2T94hVS93rwoAHg+0t(+1l6iPauA;WScs7y>}S$vj8re~jkDdE?K^_TRkggV-cbZ) z=V^t%Zj3d_>%YWL*$dV#DvPu|%x?H`5s3+JHo)gN zBA_>~_*daVb(g_)DXnj+DQ&Q}V33DIiYLDp$&;_F2K`IDt0@wxE~d$Xhmn6Dh^eWn zTCMP?CI}1dKM_X@>cei!C*99LtHXdbs$5mzhVkPbg}7z#j3TSE zHN+();D(k}aLDJI>67c|<&m>Jw?Rt%r3VzMz$DJQRuVoZkdFVbR7QVrd7V5E=1FGgC%U2klv zGFVLTD!eAjNA@dc-}Nz=)G`&q)iIc9ipg9T#h^QF(9yHpfskISu8f8-nP|Rr;8N4A z4#hT)(F{MDCYm~IjA=O zEM<5;KSJ4Tf}!D{?z;;6ePgN38qwtP`Uo|=5M4gO0u8WLb*5FrfW(oDiHv^{dXUFH zrtzmlO;9*2OfRF#VJT10O;61x%!i>6Y;b9#F=C2B!&RreRGL8=#1)i$Oji0t?1fM6 zLwI%$MjiD!RkFx1=p7TjAK`8Do~3vi>dU)@Spk{~sd33SxMn=)zF5YOOM=ZASAl*k zX+MWS(ro&sW<^@7U9u9dA<@@(IjzcTrCJ*-IrbPj?;h*1mB@JN(JJ@ivLAgCjj`^I z5wDS`Q_vzwt}L>KCE;QqLnCW@V_Mz4S{4DVcIR@1oq97^t3s#mbubH6Fh#{4qlh8d z2kHU7Nimq#a}ht(>$v!h-O6xTJB(?kt$5K44l8sWgfL1+XSe zc3~>3#A)5*YU3Uz)22G@1RBsEKwV1fjOSAjZ4#0?4_Rn)@A2wcT{*9;d|7EjSRrr+ z?oN!9eY#_+*~r2twet>I1+B1T5gkIA%HI`}$w$C^K!2<_hh?BysD^(YVO))oV*{q| zt6WQT{lsw4OL&$^!goJ@NpEsv{cENy6l z>NaTU8ywT!9r!V&PVa2WLXY4T;KPOc7KwG}(ghi*sl&opX<5bGhSDb3%=oJ~<|C!! zipYSfBCC(*Bgj?uKG)eQ3@*X8UlolGitC%obxOtazE2(p-C_&h9!_7XmU-L)C~XLz z5$cmzjfoUjP4%+UCG)EqE34yiBh%P(K$i0_Fw==jOh&8Use&>s<6XH246;YgMME`x zP=hPnP!m5U(?w3jV@|{@x*Da#v{+nIJr%@J_QE1-NT2Geg_XV; z>{GB*fvhZQC~d6O9>a|rDos`^8|om>N({;2ag|mjeTt&JsE*W);L#$u5ebAB1K|^> z5WM&R@+Aa@Rz0Grt0l#fp^6j?;}0>n4%bp>iCN%C_?-AMEdG|ml`bw(62*s!ew;UXm~D#Se>zl zHfKYZfJ?3n{Xg{(B2QJLI@;d|HCUo6&^q+5D=UX*Z<^PqF)ia~P`0&h!%@eJAQSg_ z(hIPA4#x7hSX5WZbr#FinCYN9$qIy*;=6~B0JC%cgTb}VzM)b~KL9N^ozz-Mu0i;$ z^B@DPJJ*F*(%{b-qu&!n(&2pBB1dsi77xC6HAO;IlvX&h)M;Epe<$jy2FoN>(Yi|h z%_pI~sB7#OzDX1F3ESc?^FHm~=I*RM}~MaJ84g zFU4q1XEdA+&6V^g@oEMm6+*Y;U{r+Ir%+!xw=&u(fiGjo z(rg0!M!v9GiDBZCYFt>QEM267bj;?PlAob%dGJ6?)2#Agj@9&k(cJiP*O)$u&g^Hz z1SY-A%vWN|1{zptvDFRz?cWO+NoW|+Bc7?>=wgD5f9tXx!!wu8oD*gYVBf;`(_hT=4v-1uG6 zrXI3`tuS55+fm_~aLsV|htrTEHEQT@27N_YUD)4MQbV5}QD48TM*C`qXTcZvb zi#kb|_V`3h0*`=tWN}24uJ{uo{IRqUQEtbCJQWkMdn3HnPF=#ul&g|0b5bd7FxW?9 zs4rJ)tI#hm;YY}KEGlmK7MS90rufc8_D;mBY(!g#)^Kufrqiraa`|Oq6KrVe*uP;{ zH66s-s9)~W&`!9OPan2Z^V_`Zzb|8IqkAT<{yMR=)U}Z7A*Q~*0fm!`(#raJOq5ku zC6D=AdV?0D9z%ygX}mT^IcuK>SlcdaEgwU2it=Ty%$~VcO&qR&vLb7iwa_oi@ne%!?U(5vYrE^e9J46aBEKxak8V~C1Y&+nN|OaUK#-%S;=lsU ztuC8~O-shfF&5(t`4fcMR>+c+Z`Fboo~`4VT5!T^$h}-<0+Aj3FI++6t27L$sTggB z{PAz3%cTem1@-g5S}eX{Bw!X}VB_NJ{PMU4dPZq4y>Yzm3pxx$3(pErOWLct5RrS1 zX1m@Ge^bka%iuQS$H+WfiDm?$GX~>o9oF+}8;xckbxFz!*vQ<?Pza`-VP_(wbj0yUNORpl;D+E*r?i)7O^wHfE4=t;QM z)5uJ=!rRBLEg*}mGB_^VaJ-A>cW8{%l_v3K!UJ!>&$4dEE!gM5tc?9vSZwvRn2oNf z!1_%MJbvHB#LshBqwm-KTMgEQ#G}K(B#4xshY|b{h!_*HrEuXIT=MOC2dy5~rGDwR0qX7UCj-uhaJpk#zdngIT8YMc)@rS@ zap8r*mo2P3RfUTUM!Ie)_aRm*&o0#Oh7iiw$`5b6AvV{O$1b1QV+UPr+;UrWS?lnu%H8oTjH)b&;+H8XTGVcP|oZ@2~ z{&G$%ES1D7m<~q)So&*H-!)+^8Wkdj+GEAmGJ}#uW!2SA1sIKeK5DzPE>h7&H?uS@pT>GRTWM9+>@K!-1JMiAt6XnKtTe*SP(@J zK~Yo`?8+r10Rm~HfS{6K?}8003-%6(U1@d^6zpOHdl!5A)c>8?_Z0m9??2B2lRMjI zXJ=<;XLmadzRoMWwzUpx*QV5z8Q8(CjN!Dld@&vTpsx*X%)2(nJc)$3<{F>*?i}fu z>vK%6bER8v0E2H8maE0jPQ}mb5QaM!R(7GSwD^tSUyZ*Q{EI&ZXFn83PZK*&MD|(1 zOkwsGOw}FeBH4OHH%Zy@sod`ed@XlnTN+c-8fc9cH~8u%>KipWBgSuMt7P+TtTi@D ztFiTtHfzoY6Gl!m?*R@RD`&xF&W8#MuR~p+sIg^d5Co0^sgPLF{+olE)x(b%%PzR% z!iTsOkGA2E>hm7)xUpFsp5RiT7M1)*iPv3B z+wIaWEoV9S0DH=<^+)=9D2o39cn&p?Lx^u@>SKdWM3Z$bYG-`mCIcfCXm|6Dj;yXS_%kB#Fmv)KZbUZQ0&Fc}< zwriPNl(~WJHJipk_PF1snQuGo<|dQtK{8#n+ubTLk7nbMl`k*GpgZYW=?EMI04u>3 zCEltS0{BXUV$^{do$pt!az=H1mB`vA@q15c{JT*|yCigSJb7puE&t2zeO z7Whcg1ICG!aK&%7b^%e_Toec4%yogPrz&rM2OHYKN)t{)m5$9cA*D4;`9*zBvRnIv zhjui6R!}wdZD>wS@v8%1^ojyZsz0EDAu4w+NVu()bzd=jCJWQ z6IhP0G=vQ=CrUhQ3Kfv*-f}N5BiHmh51XCk&+1s!Ro6(nhH=~&6M@SQ^4Hw(Pu#{< zm<5Gk^wBsh9cgMF4O_}MFMDlJ3=OKD8ChIFSw-iU{bfCbx!CgWpQcN0&ziGI%kFngk0_s=Wdq~vKbdWlvdIap| zC90J*o!FFgjhwR+@m7};yr$3&fSufHgxWTJBY}0mPTx)n@pPHdUtYNxXwySDC#r5l*RSt&QA5 zW>XQU9f%&pQb&9FP82-Z5xv~@?BrCX4*y5dvU)6^b?FZe6R@6YC0F2H2xS4R?5b>q z()VV;Y=EUth`Y@2{^0VGZ=|PGrLyVpe`J$45E>=Qp|Ep-;aL^y%opE}xd-g2<>wD2 z9upm0E^#)&vY^&7JB|zcz`Je3z`M3#R(7FG?YNNvPm2c$=v(J^kX0g;m8GH?cd(Aa z>vq`zKao^J^Z-^M)HfP`IPvH=lXf*u~fqW@V>MI{g!$tg=x#Y z4wNmAKt`s$#-4<=Xsp$gL2J<12J%-Xl2w4+DnwQh$v}mx8qOO;KbP=s2*eX z6>4LF7ixemH1MAbEfrP*pSiMM=K{={=aq(18ReBu=I}*ggNR2>UKuhj%|u~bzBRz6 z5BJAZYz!L*ZC?*Sl6kV$cQAJ$b#XR)OuQjrUY7j2V-Q%^O)c)TfQVKcn)GWvcKh{! zIj2+(@o4{j5JZ|&X+8bSk=H=c>9{QjX{bM3eS=IJGUyx6ZQ`j#vrJ;P6{8{$Y7GsI z(n9?rxRpmfoM_PHVigxB%4~-RIcp?|=ly}5Cv1U1BeRICAGm&!6I5Djbd@m^gx(eSRRtP2u&zEQqAF#lEsf4fDZMP*Kdu2rs~0Ho2v{VFzoC ztN_OC2hmk_3M>!pNC(2z@|-D740;%{VwVxf0t!0jhoh@2Z6wVCQN3C?<~QUvVB}QjF4mT!qILi>hg`zSUaPH{ z-=;)GKTpOw%;Q@~v|EX{wQqAqsm)xH$fx{zG77Z(I@L?4c&vo{HYodq+92F5K7z2! zi^sgy1AgVMo!f(INg)%12AStT!$eyJ7UcB}zMF6S+&qcrGb5~0>b|X=DFHjUc!LX; zc%#5OQY(?rRb$B1I%)#qq)Xt~pcc^k?dKOD71Fcd7O%ta`XIsoiZ5G}zM+o721k?9 z;4>jTS(}3CsbO}t_SUBXqcfkjU>J#M6*GZXfD-ODbX&V(a+`SA)8`#4cYXS50Ux5G zaBINMCfHGS?^CwmkN;Z(%q0d}aXr04o%H)iPnPNhn1>OsTic!iA6PTv?7&yVYHs4% ztxhLe&mU2)XSHi-#W3TrgNeasY&oG>85)k&74@)@`_0rI8)fMrjm{-*h1BSJ3-XY43fjF}E z>ISHG%_`xpM32=ts#?KCM%7joI8(z7wv_ODfEGkAe{Dw|{I;+oDM$0v04^lW*g{^` zBfBEjz=4=Rl^iu_VC4*$31`_e!r(w1L2wODBcaU8o6xWma}lK;_P@YtfkXF-AaA6- z#&Y2G=Pe?kDK=>XACd-IA201Kv_%`Z4CR;whd}zbVLJ}qAW$xGNCw;~PBzN)3E&l@ z<3@>)j%&E&i?K>t18B_Jq)yT#FoXjSVQ~pTJk1A5FbHLvQdB}=&L$PI&^6u0mN}9( zJ=HK9HiqVu+4~lZ^ZpA}wUm<5Q_a&FWlR zKaR(;#(f99aXUINxlEV=)oefsIA{!i7bpL_T(d6%i<2u5SSyPg>qjo9B(k^Xn&}ca zdZpy=hsi1k=ll6a=%haY=ijC}^Ft?HgGUQAF5n$vz|YOO#j9maurINet(Gl)E!)`D%B>QgJL z8&&Pk|II6H({!|N>+EP3LHK}OAf>K-!VKhKdTFfg^vsfaO zY?kssLMVSzq%&ctjs59Wb@{ERYRRjJ;t3y2ks{DC|Kmm3E>bXwd(|Q-IAhj}QxFQn zAkY5`OY9VSbu6xj!oIxj_poIxd4Djy*GA09a&t=N-(FMUifMh&WNmDn4}U}Z7Pp{A z$#;-rEhZlEiozg7;#T*cTyx^xV6`X?v*NNpIrQ$nA2q8d3DbR$A&|yx+nJLqRE%uKtfGr+j7z#|rbqJ3mxsr}(6#AiF;XYpPon1Am0*%2 zuJVk)7qz}wyhK`Sv4@t*b@R+0PfPUAqo#B+q9?yVtfp!kkNumUmS-+RU~v_$ixSln z=2O{hMc1PSU8qw-7q(0CCP$b;J0GjEG<65`rK6oR;DF@9M6Mp7*yNJ@)QJV2Pp(;B z31)FqG4uD~AWif%A=jYCFak<`K;7W`0(bH%(S@X=bMXSYd@-~3b?NAz5G52r)Sv83 zG_nfSZ`)?8FE^(+vR=lQmdo^VSBXs6bZ5}nV*uh;z%LMMkh+>vQE0s-{H>@a6hKen zH{m!yb_eDo%?+LX%yYB zIPv)^ZYSdFRXAX-5H3LnCW%jB*GD9jI^(CNXcH!*3A$qRY3<|{GJa!qy;)hao13W0 zLq)-kAr-_)i!xTb)Wym3Zu8M`Bbavi&&iXufqXt5T=Ow9+(A2UixO zT4{Dtk-DmOt>P+~ZeBu!WNByAMYn7@Wg+jJ64ph%v7H&%^MsFcQ$t3#JkxIm_%hlB ziA?*eG7l8tPlzb-fq@=^)lGGCd?%(i%Vnx)>xw8Gpkx{+yE~&;cYl>67$Mc^J-njr z(RgtxhV+}PLhLZKMI7*bHg2%KR3osw(Am_JspurUccEm0^3mDnG+=Ujd375u+DEdN zHecg<|E28=hN?p|NA^Y_xk5B!$tSi8)6c=dzR9%>ld;ht?$*)X#HYf#D27MsepxBj zq3SyZI61iSRlZmqv`!W9s;xTlzQnU+uUfG_#Cy@v^BCccnY$9N$>mIHg$fE42%wuN zyXH^SB75KJync);62+e7b)afnGJXTDK`Zr+ph_n-xv8Q)>jwIo;DWYYL9H?n1Z)hA zSC05O7`ctvyMtPdpjLKYq;J>S7MROc!ez~D>QLt9j79h0R2F#DPCv5xQx@C{nL$5l z4I68|;cSHs9y*I_hOTId_;8SKC^*U;l5Q@Y3GOw!KY;T}wvJAGV4j(KBwPui`ypqu z;8uybG1IKR7f0tx4+#2Fvy$8fF;|=nsGdFEI;R64Da$i=HK9q#5lDn<^#TKYU9~$n z&pamQA=*YFuoBWD#F08GAk_+ucMD@;4f4$%iSOG5LoOUoo@1Gf&T7M>1JM99Df|a zKTaGEs-P^XqvsFc4)e;Dvpw1nji4O5+&5C>Ri!X~eC;aPTN zeXmTso6dpmGByvpL%7KVZ5I(&TZ3zE+z~9%=OSrHvlBn)-}vT+%9@InI4(_=FNVe= z%Q2^*5q3@0q?t~suFe?kH6qL`H5IwHaESOPB4t`hH7Is}v+7|in%$ROQ$GjvN*)Z| z07r^2aBPR-X1m-mdFI4VQjZ zb$#SeFUneplB|(t&^{ou5)OFe9hPT`PeUI@W|+Er<$&6|kHEjOqz+N#3Mo`bb$R5O zKGw@^_-qWNXf=F{77qn^qNBwEU2wV-F%#*;ncilvZogSaV1x`rgOXUdV>RTrK-e++ zaEJ?}EIt%xKvYi-mLX1FK1;;9iF@s;S!Nwcyd9#}*`_vS#mK4Yrt}PrW~|4+*(IkTP*#Jm;&{#|YLdc;a!uo#XkYRSM5%($+6)KxK-g~2I105zU~zOl z0!?Fov_8^qRw2tm_jD+%KqPS<<{LpI&q5rew*KpIoNB76NIM$jeq7dTXGKh2n^f~$ z#IoGz_glvJJTo1E#Zg?sf|E=F(9l6l#uZV?^neC`R^m1v+^7T^26&SU1P)5m>z~N8_ z7Lm=Kb71;0UiYv45^*C}n~j>!BGe0rWbzuM!93#A6XPfMSV+$KXh`HbGhmg_gs-yZibzSDn&Y(OC+{?PMWC~M9C5)BOU#tMaO&fDL6>#eiQor zEW~whO*el_BKMYbbHv?o{9zE(jz5yOAsgi{nWM#Pq^>4@0+j1EDgBOgvsyAp-icUZ z6cft|-QDs}L_=0fzulc~cB+)T?vcEL(;ad{P0mS(b+$D3K9jpf8n6s~1VplYIb!2F zA573N64YY#2I8E|1`LLvw>B`8sp&AV2oT_-lpY4Nr*rpZAuXc+DFqqZ4^Xt9%>hfCAh&7scpJudRy1?#w@#rs5 zh{s5*lV@X*Fh_KQ{wWhn4;XDq;O6i59USI#NPP~`dO2i-yAi{`3$)GgUm}O8Vn21XPOulWWX$!5 zs9F$Ykv;#Bej4%Z+KG8;AZY>%>rebgytTiZH!j9rD~u*;jqjJ&P=B=Uk^Vb2W=f%a zkc*jgIoevX6uk={ciesfOQ%A6Ef3DQx@NmXNxp|DYGDK8R#(HTv$8h*Jc#TUAvB}w zW$JHSh|Y3p@t+7}OJUd4Pc0|!JLY{v zNVsIQo-~LQ+kzD(1nI5eyrHq!9B|uj8hu2E`o^&fyE`62(*>}>!yp6b1!C>n6-$~ z$<0DW$D*!P8Hg)OQcrxMn{p~yqjs&bO^84J4zYTq2Fib`!ed_u%QE??!c_i9Ti}(s zT&}3@p9ArvTiZD4Y5Ovj_vntCB77-yFjJqGHD*Ut4nkFSG;~QSnM%&vG3s;^(!c($ zCVd&F0_|sI1BvOR<5^MpV!S0!f8f9LtBL24XX|{LvX`^{Vm0`#h-YI-242d;YveDp z(+g1DGxIQ0hy*diUVzOg_lH3BZ<=GaU0aUB*khC_Re8PvJ=w1LklWkxtP0T_WN%qm z{FEIzLaoI871`ptCs>tn!<%&-%DI*Gw>SLa(l$(tI6$kM_)|~~t1E}9U*%I@S}|b# zAUBpMMdz_zwr9*J;Jau49^_+3XNE1*T#VozZT-+a|K^2rSS`9*YGd(V>|x?%t?lih z0iXK_@z}nCuv7b!q){fq{#6(S@!*sKobLO)x|I2udcsU@_EM$eTq+#-O<r3KJ1zG;wyk2a#1_MXerfZR3A~;BJTq{yxL4yWkahNIwxVyhgHx0L^)yw90RYeRZxvJxzVik5b zg#BoFzwO`mt?#;Hy}_KCqhBlUvWglA6!c{53n*YOgE+Zsf~GXKnEAl%+?GNC*jD!h zuuw*uF&eO4e*GUESC5N7tiPy6(D>-+w6J>sLew;N;s28lq=?xWoAimHxSHy}mZp2_ zx-A1^6Ay4t4{}wPvt&1|iT;<4hdAYnwJQ+Po0~S_l2faSn160P8COLw!{yIeJ3(#q zCV50tJ}dAAPQEGJj00Hx8$lu`+@DbYCF|4uO5}UKizf9g?)Djrk(SeeioG>v`y(^2 zBk>{YqKwR(PQ8B zvhU-@al6EI?2L&&Oy5^9-3~q2r?DU6vyA85i7V1&SSf>z51MCbTmn-AT9)uJFNVohbJU<@_t^)}t~%$@yKcEb}s9k6fVH zz)Xg{Z`uFGms!a8JKh26WZtO-;IdTW z?NKNfx5l=?b-=r-!-}Cpk}ym$yGFI9ZF*E|b4hARH^D1+Sk7 zK5JOM$$As4o`=U?DOB^3o#{0Jr_-w2fy9|ie}@1r)VU))Qey520-U^qh{vlhA$)V* zDB?ZWKwRG>B`cThh8i`QrL%FV{={AJce{iHvdHH|?5}7d4MzXr`A+#_7QE-yLY*ER1~5F=uj3OU`(YW;-;)i4G#ZT@pI^CjV_5 zjqYp@C;L36VU8Jvu2m8d+~9P0)xmaPZ%Ve= z;D4X@7w*{A9y|NRR;gPCtUlG6B>Q~E_dMU3QQ8IhW(!W` z7Pox_3X3mD@JIywEAbtMTIfM2%h`fT|5oXO39wcEaA-`4EPoWEboE-& zHM|C8UC+#{({W=(4JI9Y8CO7JeuB|m`nG7(GDh_dCAg&E+})K-#C>R7fTa3#=2f8A zXQ+X@C1UD#g*Yw!302Y5m>=oyn0_@=+k;u!PAM{NQNEeJFQ{Gmi%?tFQJycf6S;nNF&5I|93u%iAP@^fE$kdVC`!hW_H`PNE-`(27RqVoWc^+z9ZlK@t`nT2}FpU z9KFgqF+BJzU(9CjUr3m7H}T?E5R~mi@ldqJw;H=o-;8^SuWlNTAi0uT^UAxAc-V~E zj8#M#@?>dm9L7Eo2=Y1GC+>%LdX3{TXIWrQseGiF{=nCS+`DI)%+OrqxNCs7) zJ&jeHdYfhxx!=o3Yk0Uvkc^c(;yrq2Jg@^kXz_zP;B77Of5t<^%PC;bGeIi)@I(e+ zCEujC=UYnXWCrp0U;9(m!q`an_QU53Zpb)lz+|?Az+@RNe;+P{xIqn%y0=lI4z|qO4EQK(qZf zn2p;831>jq>-h+=pQL6eZQ$(g@g5eOV#o`w|FAbxsOes^)!xMU3s(%XzI6^N*{5Ee z73&i}<1^yxn`gIyjlnCb>^K>pGXTs@Qt3paOVfK%fEciO>Yo#EZB z&Bp6wk0~h)Gs7|VJ+3}5MK(Lxd0K?8lTjRnh_!YCl%26JqbzIwV~${$Z&_#YxUg#p zWe6oAX3(f!S15bSdhF7AG0oI1pJm8$m#@GB_KI0HY-+Wx^KK5BWv|XlVu>^bKFD%m z%}*?|I^^dB9a<-WXt1<{rlU{j`SidqT$hNm(X}vEO=-ydhxypIFjbg{gw1xSa^h_` zWXp9xc14qF1Oq@ovuO_0z@b^`zL6<2lNmR(j20J0 zPoU(+iLU_>MC^krWM4r?9X!n4CZ`eCMcGRjp_sZ^jIaApD#x`n?O+(=$hkHWLOcSP zf40I@lN^s~gESSD;--!JU2sFJ8_PFe4wm`w7!8C;J8)ZdGf5|~U&!=l6|93r#nJZ` z1F4o|^}IVPDMU1v6Og~YQK~i8|00EqUr|@vCMH-buptX6L zyc;RwYdpr6_79{}njUHj3P>+vd@Npr1NP|07&}Yvfx@khljZv%-TnR7Judc360XMW%z?20NV~UXRoOTf7i|2=&gQS?q z>JLhc!>+wMH)iHt^aS=j&%lOsPCxu{I*!I?K)oJ(D$2(j6Ovu?Y&|F0?C$Qh%l_t& zwBuus=q4lu#{iZSo6qpNh(eNltoj9{`KLlbKZi|;&ROVRqOs=GB&K}4D za5;nZCq?g8PqQ8;PnrQZTUY3w8#6C`f`X!$kKj7AME29qNA)|P`fV4WaGVi^i;JA% z&;_)PR_q6QvI6!Z+ne67;TgN}oVW7#U?uGS7%WP1KUZUg=ChX6_R<1I${By`;Un|y zwI`Te5fIfaBm^FBh&1-K8dQ%M5GrRr+FC0OA0NL`hzQ46&?| zo3%ZE{PcZM#ZJ;Ow<8e8DiAMnz1ja~*#o;lDln}D5K!|9r|~`y~U}>KER`FcqVCX&K*L$ zJ~-7{kHL?#91X4M!-D*v~wKcsPycLoACs3C;?g zT9b8Xftg#3{&ydanfnYzRof#%X%TxB_K+?*Blj>ShcIXk9oW=aXg^e2k2!ebheo8= zfY%d}+MoKLG91u-PBUo^)@H6dVF#hKpOuXrFC5C_1 zTGZK2nf}!5&gP6K=UG8c-7=82jAK&VerAt5iq_j?y*HmiKL0wip&8;;^E5MQshl}_ z1`_i#(5ZYe97Gz>wV~meIe~er@xlsRX-D>WuezJMV%V0fM==m8P`A9J3aPwfyeQk} zqean;h++oK*bOCah?&)wV3@cs#LTz9%b~xQ5D0G=aSUy(*J;qnKNjru2iVKq6f^Y? zN%YN-)DU=&=eFV|$-lOK)tx+A=80kWNqq{%HvS~C?~4UvGENycg3 ziI@}+h|`0R-aR1^q8}iZ*)DE*#qZ-!VNo^4Kr_X-)5-sB2#q_WzFQFi7vk7E5UoVd zKFORjHS%Q(5fWT{W)=OB15kyM|7l-p3K z`*F1X-FmiD4yMxZCK+aQC%ytJg}G4d zm4{kdbadYt)Xuu2#0$J@Tg={ngAUn^p%^J(dYAl-vi&Q=tc@t%$}-Gy95?N>pMy5h zO$OT=x~|cBu0Uq;fB@YWrF!lxCT$i!OLUIej#7FQpX${b*XF7nQpT-Qoi`iY2{*uE zHzU*J^^*6FPW}hiF?57`Io7I@ql&X5k+YqRN0*`}^>ugA(jnhkYf{;_F?V?cL!IyJ zu*pxOz7MBqZaP4pu4svz##yp77cGOSOAg0*_fxv3dzoRNK^{WN=w4>_83ks`?hxYT z1xN&jZw^^1wUT($HVx{BxHHk;z)73O%*@ulkZWxnkyOVThbP*X^C1_@)zH|z^XaL{ zwW@AoD+tS3yCuQr>va)##Q$b=pK~UR8|NiK4^(HL$10#wfA zIms5iE=I`=f2B0$gt0pWH_-0fwkt}NqyJhr`=2)P!60v*n+k?3ZkpHRGm1g{#SWC4H3Th_IDD$@864tn> z){Y$?&kINVb*z-I0Ucv?Y%cz3Q91$n3mvck9D#=`<|1lt1!r$4N5|Q>2rN%4sUS?! zph1O4clL5Nb6PdF%tVJ%Q1O?w;fW!NcaHAVRV>y7v?LxG$A^QO) z60wN2sqNY7s@Nzp3vb;}LC`S6$$79q{kYO14;7ewqh(OuZ<036NXecyo{5ChYIQ|s zF`VM?!9RIl(E<)Y!PO}_OzVHTkh?}^r{%}wVCtWdX}(3=WZQuf7t04+AmM%PqpYO5 z_XK>Ieh4dx%7+Jh{$lEqSkECJS^8>LN>SX1g(p{=tfMy8`>0da<6iz1H68ssfRIEh zxi21uT8ddEIR-U@8spdnisl%70-`e=R7f6%Ks_t%RT@KdRe^a-0!t4^Aa4B)Uv!kY znq~4mLN8ft4RYLHR#M}TXbw&>=u68sIBNtG)$4C%VEM~k2bE};=(fXMoP&S_pF9Dn zMKlN0a-vT$wLKY)C8Hh1s%#vK_)~GYus`Tq8gidxe2E6t8rgw$slp$Ct^NbhU&#}s zZQ8Sbb)rvW(T%`jvrEBXw`0%9Gt7t*nGTHh(!-a)K8x2tL5WN>b+IzHur!4n?H;0^ zW7>1f0pfWFcaG<=sH*^PM^>4QlDPa&c$Z_kkoO|-YN^gn@fUVzhb1NckF6)(^tc%_ zY}-iGM06~<+=5^1MMjxX)7WTt+B77#fq1!HY7ETkYOaEh*+$|)D7nIuSK2ulFA?uO zU_#Y9{}JM`*6C2Io|HNbS~I{Xvnms9YQvHD2$n+PXK6!cIQcI#p`6$BNC z5{@A_b4ul|jZ|iTN*UWQUwB0}Gk*On3Gwl1mw9W7+M2fTxXpNl2`Ct_VWYP6%vXv3 z_h!Ls#Q*zL?d!ygTU?0k^4}odKSSx5>4R6fS#6WV251*_l`JIordKGOZ?P=5`USe- zE#j?J|5Oo(B2IKB!2KrPq-lu}YBo(ro+pZu<9v)S$cw&uvV-?v=ybRz*2S%L$d?^} z4v&70AwL?N-_|a}LJRc@vHLTlkXyT=3S>@2f_)I0b1~37$xnBD1e@& zpC4a++eG_r$6B|PdJ)y@2)tF2_fiO8>6fWt%mGC+})He+N^0JJi-HTnfqRN`Vz zN0;_;2rgqg^Ra3NPODkFp|bWt5hw1|6p!NNLOH{0y173wi5dO`rrODi(J6Ytu(+=+BG-56)J;aV71{8LE>5cFooAz^_#F7sj9F4Hcz|ZOyHGcq(T?z$#T{6C7ld zkpg?I;{-n^tvB32;9kQQtZYuoCdVR!3cW$fBJqFpFP5x#%LP22{x{Zfr>}xOau=E> zLrb2*nyhrIOaI5Cz>*6f=||s#D3*8qdbkc%mT4kLaGwoDhH4l~z^-$20V2ADTD06q z!c0F3BfWe%GPBk5GmE&)44G(jx^^fp>;p za>p`>hxcl?K>Z?u971+WjFSf&p_cfCh(d|Y2ssBR4a*)u zT@7p>&N{t-Wts(ezOz#s9!0mVE^_T#L%qB^nBKvwu5||TlE)Lo+mft*E&`RMuv3x- z`=K}w*$3rjRcChc^ASzgKXsJek$A}Q0b;xMzF21lsD%hb%uBdZA}@Pz(Bamr%oJcQ`NAuweftq6n7t$R^A5}xYJt(}{K8zULN{DLXlgAQk zFh?|*WhlPe?jIs2j3WQpAZ&B$oHsAXNyeevX&N(|CO`=8;!JgN=LEDsrU`go3d;qL zD=e{M7G7rB=gv=u&JdW`mgS45)t7z*8AHqb|IS1jQUXY=6P# zn9u5AevxOGOljvbcc=b8Nb|fQr3tJ!n-q&jo#@q7 z=M$^l;xQeSSHGbI?81Vw9CFR}TD;*wJ_r+^xT(JMz-dO2Om|Lzlmu{b9w8X&w>6Kmq zvj3tBN)5}dN|-}yVK^(tEs|;WSQzRoO?B)(|Di!4UT%INrU2f}o!W5qpC>~rW%Rm8EQc#;bhZ}*xTaJ&~o} zG8c@u>@&SutzCgy4eo^~avwoxh-IC{DC#g1%x~$^6KA+)HS$iD^hQDmVcAHOJASa& zX$=f*DBTRlK5r-7Pi~HwDKp8gi0kk*Bz9-y9E^dOm}&dP&3*UD$=yQ9QQlzo-gm;t zIuCkD6B>oHLEjW(_2?F4o5esRs|F&rox!ZQN51a)thmZ_RzAm?vmp1e%)vOy>RDGqziWY%S>xgHT zy2=XpbQCERvoSP0>sF>~*Pr$W{`9NTR2z!T{N@H#*Hq#7{xZjgD7V5`E-NW>yLQI*SWQFL_5TXf)8bGSkE|_sdlqJJdT7@o5}3twJJ;lQ6S=xIs2n#A z5}p&2X}G%_kLVquJy=~4=#=*PmTgFBCY=t-k3Uo>FFKcKsaEY&t@DE*a|Gcb*3fl7 z9``W5QvQ%YoP;%iwn;%g@+7Zxu}^X95zr*p#9lT)QC>e?WY^viey!n8=d8)^_}alTZDya~bh zsz{m{kTho@ud-Uin$m=4e1kC^dyDmv`w%(4BqL?F1I>WfpMdabKpoB&f!2r&S{o(Y zFDC76sGA5ZhMNTxVYv|mGa#DM3~QT`qd9I~lGG(Fh+y}h;q4=uL`ze~7LOdm_W|9b zvk+zRObF3vU(76K+?_y{oC>7Os&bo46>fusikbhem#$ILCFdfFh%s+epoY2cK+^V- zY4CIeHsP&I1oFAmro8vO^ux!B$esrhor^kFwIQy$j^K({nuOx-Gp<_bN*g^URWW)I zx)fVpb6`}3&LaM*_9~hBfcy9aRG^WXNq3;mZD*q>c^F;D$uIJ`SCoAMR$R|y5XL@I zof@~@LFH3fZdYnbUz{(BINB**HcsRV<^#rumemB?p}sh$W2%#_n%FoJUPt+Z3JGee zFe;f!6JQnQ4MML3KYry5RMDwap@DsSAUFGv2>jW{@lF>LQ( za5DZJND|p4FE(LxGu7UWgF8083e~rW3u1LuhFQA{^cSI;%K!7Qkd_Ov@REtg5N?Fa zIGu-mhQI?=VSF%j($k2C01)TWCS2bk>u+VOXMLL3NNnTo+U#mv7=&Y>-lvU!DM-*z zZ!j)hmLIeB`yn>lnt5WYh?`7yJ0Y{leJ*gtJBVRV_+PE+xHY_|5IJ=vLLBB^5t^3BsLI}ZT4;*Oz~m}vY|`B zUR=6hbuvL#Zn1v06uc&*u1#K+vHrxjD<;!?02QI=1iOlM@&+)Jj#s+zg2ydXR6mYV zi1%|F$oM#3%MC`|zSJ@G5L#nTN<<$4Wz6ttxRTX<1cK{1Ob_nD_Grn5%TwBj;%GgF@Tl}>@cP6MZEZagvgfNj|+B~kmO+@ns@5*_U>S=>koMy z#S2hTvuS^6!HXyl3u<{45!XhjUrgRU%vYY@oKiN(vF0HsbKef;4@qDC0utf2V{w#C zO_pc*TACt5nF#Lgvg3+^_ml(0IYWpy!;gel!RQT0DsSsRX47$K@4WpO1?~~E276rv z1!#Zbv9E?+s%oxm(!lIt9n8LW!iE`r3z?Y(DYT>NRs2~4I|!Vw;829^-c*Hl>MOvj ziOliSXB@zMxwR+yC1k!`g};XQaKUJ${lu$os4lTyj>}a z9RV9b#gCx=y&WaEBnHQz!Sa{?rA=TyW(h7@C`!NzqD){TWE~5}xxF0EfXrw%D+WTw zsQh!XPViFos91}wT(B_(HPYpt#6V1cO@2XGm6v*nmRyWUe(292smw){L7a?}8Kq&e zl@l~@tI&Hj5PGIz5SAo8G-1}yp1DZGk9}J}ip@>oEv`YIfY}a|ZtNlV5+?tHGNCES z^osKonm&6H+ISyEa5>&T)=T|hXVVb;mwBpfa18q&vJGV~2WzsoA+M2qCoO}f?MAZ9 z>a`+kqFLrrV3MQLWce>|{la#o_e!Hi6>QzVH(@7xc)2>^K!1lO_gQ#!E=SFx2gtB* zT!|LA`B~=u~}TzIs$jQ6>*1ZGBNbv(A$~^7uzgBeyZ|>Kb}H$B^x_O7t9@Jz zTZOo})0p0j8!bh2LX@Ty_QWeOVrq4dFSmwKZ0$TU<0-NZ>UOb5WV%NSdn>sYBRa*y z*EfdG-)77}FRy$bb(|ic@5ZxK+V>XuKu8`#AAm`K_Gs>8W*K!|}_B<`LE;vhOvVK`+r!KG)CHo`S9rA?r zvk9GpDXrKYkY(mQgzCAaS>`2RlF5OHu1_iOFasKu*WAH8wx_hF3@m#)VvQb*Sh_UP z_a&71^IF&%Dyhw~(~d7^6!AN@45!G@3)jkOYcFKR+rzwy#@6Frn}>7U?q>d8pST^} zLn7wVM^MkweWhTr5;tK##>UP%l?C8w*jd=u^v-|tTRn}<$+9Gzai35l>cgq*<= zKwO>c?Bq5U6=RSV+I$+L#&qh~-|d(dxgPKF ztP-JiYL@v{0+Ms2hisnPnK?dawJHS4Mda%O$K!P1I!<7QPM&G}j8@BOj$IQk;Q(giA7j%Qm$ zPHF2e1Rm>rdwqeGUU|2$KK^^Rw&3WkV2fesiR2~!N0GN-;*!9~Wu~q?j)`jlBH_Zz z@RxQwa#-Kb?P|UmCUe^rh=N51Ot0|IvKIEdJHWetqrcp%(5r}>OkORqASJO3QXf$8 zR^FZHdmLUWF1ZGQo&kYHxp%StX4BFfJOr@x>rA&}@_Iz9$3?c&WGL2Fi1wCy4+y*g z@+b0^`34G0p}RQC)Nhb74Pwi=vQX{Q@9kjLBe2-Ti=%nd#HM%?Vu73L!m;iH9lnM* zExkpu(7SB?;Eajn@g2kr1k5rd+9)&3?UE?8*MOuM%R!OhLiRfmSXYPLh{-jz+%L*m z(E-<4OW@rIgv~AdrR`E+PLx3RUbJg6Y8?GaW%kP=vzH+OZcGckujs@d@F>@KkI0r4 zk|&m=Av(lXVj9u{S0WHjR^c!0L9}BN+7W$6#M+X}L>YZ3%N(&8G<6?^hN6Fb?V5Q9 zf+>?v*!;t_PJb9$0s@OiKaRlJ*gn;bHCsy7L#!Y6J!P%zm7V#Bhllyc$AWY#;a)## zSGPEQjZ{|eNf`Xj|0B?Aq2#8pKIHxH=~BR-xPSz=wk1nW0_`jV$uT!r80zg6p5F^i z$H{vnZO@|^d+6J%niE^ax_Q2W49Q$gyv+%s$3E?+GGjZkOjWeV!Dx@d@imN(lK{Bs zXuLOv<*g+iHjjL@@v5~Q+5s^b?yGjqivjc`Q{jXT&gr0Zn=0=q^x#3l zPcI9_{8s26?$gi?hwyv}?UgBl*_#mTo2xMR^yCVh-8Q}=xozClJoOr8l^f-^sz+^tz=*l6L!ZAfXUkebZZRsM^Z zrAL3_4t77zGK(cvb~I%BS_4c^_a&?uwulVb!~6wAVe$)P6xd~O_N2Z#>tzV*+fe!9 zZ?qJ+I*ET?qtM*yZeOE-cR-io&k+IaRe_b{sW|Ng^P4?x`z`c!b4JQbGPQ&Xnz%eFt@vN%iE^8@j>G zhEGd1sM|?}$v+fZOvzXpIM%)?=Y_bRfdP)f#1PHNhE3!`5iQx-X5Ln5R%W&-1tyuy zMVNIy@V0|fk?$pWt!e?Re$VzsQq5~2+u@e|jV^Qd zT-c+N+4yRf`{cuo<(+PZ8n>m~Er`EqH`<)H7WM4ucE4izoXq&;HXr8-v+npZw{PMR zZIt6YO8A&L>T6h#%GNX9i-cpX1>ZTGQ0f&$H+yV!gqe9Z99tHD`Z>zYVrJQzS)oP# z%F29VHn$?C*5-j6+rGC$xCe>b62kq$v>%6XFSBNwv?VsbkBK`b6z@~wjtk*>+Nsr9 z5W;=owKRnLlDNY|xc7)VIE4FvxG^U2uu%0~{vK^R*16bJd?j|dZNwiIO1qV~LqoXt zi8~~O`i5t&kVefL%5a1y%oYOC+_tSZUu2~ zgm9m++~Y&I&xt!Rg!_s0|1gC6jkr%kxX~oq!C@}G$8HWf_b!^*i%dL=beL_2z!mMM zXC^M!eI>`ag78Wc`2Y+sW;6+WA>l=Y7Zb{G7=U4gp{3s?#9vBy8Q}u6v>?|l9C9xi z`98vBgv$vflYz)jo%`_j3gYiK-`B|?TrxYe>>;-OVZuiUA0?DL%0ff^F_9*HC27$l zc0&c>WWq`_e;%eccXc*gIm_JAvS%6a9O3hXFAz!@I33`Nll7)&JLeE(NSxXTsY}^AR@DwM`Pb4QXRHwiC`LJd;q0hKbfE$63UW zGd*9)fqXfMMW0M~is>2I6X$tdxOAUb=Jx7*AQF3hp25Vi@SxtGL>gx2fugR=sf4f_ z;rV98Upa1J{9R`9UO1Dl$xYvoo6sGJCzVaq{auf;a<5*na53pH!B|%Djy}7WLGw0 zH^P2|!biu4_~=>UCokIEoziOsCDi?9;Y(nxqW+AsJK+GrQq%J|NSq?rSoIr9cn=L+ z_nMw>fKl9Qa?M?-GPjReo}24t7G1~wxSsGdQtEUwVi_bv(GA4kNO%UrSC|zWh3~E+ z{?rf!i=JnlULY(}6QhG^tL!OBlq9JNl~jF;^7*@f@CRM1InIw}^A^M}dYXkiL--TJ ze>Oe4qRk2Ws3W|n8D6E#Wx2&()a5u+LJapT<2|Q1r~~ZjA*}DdA#NK-+);|_8=uK8 zZQ79ws)(*8oMJX-V#Ft=^7k~t8co(c{u7IILv%|FVZHVx{A$s<95R%)A~&a#m6<1zAS}d zTEAln7m!mHT4>$`KPBRaapoKw=-r1Cbwq$lBMGA+62^ZeJN#zZp(ve^G6><0px>ic zESz2FUWdc|)iT&N9b1OMHk66?Bix^G-4#pc6c%k@FC0ni)6D7_35~pM79OxCRK9vF z$W#n&WhZX4jA+`s?dcW_xqz8mNO%!pGwUaLo)sb(+@$Kw}zmGduGZ!Y^69uR_(oI8^gQ58|uKBE<-h?+!b_i!Dj*^0|Zty%ajwEabc zda~>58L`37BQc$*8HBe;Qwp^*6+}%Ytn@2WG=-?Cgk{ugf471+ejxE9{et2L6LpA> zDjGx7p@d5*?r);FpGJI5kmMHj>aB#=G0F92^NW!9BUX^g_Y};q8$&(#Ao2Hc zy}QgT93|rSB68QogqK)0*8y}sXVwc$+4GQa@d@OtW5`)+Il%4?tvMDGXTpQ7=wi0w z62eOfFEiVA5}ut*9z9)ov~OYwb6HAw6DNgi#>->8z1XU~3HJ%+CHHc?c8(=NA8kql zGitQeqH0pqmHF*Mm}EXZ*)b!DpB=O?F`ZPHMVMGLq8C~BKt{hbP$%Qlh?~zUFJeuu zA-tAN`<+Gqp-t$nj5(cgW)RlexE;!UNQLp#4})R2$*mCh;YFlNeHWC8n6{EBT0>==NjkA8nyMcMr$ zZi*47SGV;j!dtI*W%?4rZe~Q`o^Ho(FCvb&9_!Aq9)w5O#oLG+{>~-LBg`k1(l;U> zZ$>B}K0(;Q^!yHFDaz#UIAIoHwrx|QCx7?y;t4ra3mj2HF*cphZu>cowHn*@Zw^(V zj`8XV8v^m?cE%1E41Jb(r?9MvAxSWjxP!cUa2@mtf4{16`__Cy!hcHm8R6%IQmr>b zeeebGUlM*rcv^@p-U{`{>BOI57G4SCUD0v;eLUgia!Ki^DMK+5|4)-_4~@7xr>fu&^W@Q2;Vi|KP$83bpD<}SW8$(DEWO9VuyO- z8weX!^r&7^bT@JL5dKIu{)uJHB)*BzzQqcipTDK7PeWz35?( zSt{$j#8~1FCp;oZ8^`9KerxTz#62a~96A@~rTF~}Sm|5-ie4mY1L3DE=QA@CGk#(G zROUa2BG^9L3Rg;U)-1E|SB%&ZKaxY|5#L7GPAC=H9_rHh#J|rz{=kg*68U63!tx%? zH8+77`@|n4;vw_BDEDR)3}x1$c0WT_554+3o)j zTGxBW8N_(fZ4vANdXqyL@q-EXAS@5X$FTKB?w-UqELz9;D&ex{(+J0!o=NH0Nies@ zJ;A>*5VA#CByu+4VzQ{QZ5-2`_#T8k340Mr8)Bh4?M(a%=5s&cO3U&^g$(aP_z=S% zw&CuF=wMiOOXFX#abMaFN_@cITM2Kovtm(4X4HwWGhxykm4i-b+l9Y-6ZRqOODGM< z3w6S-#P3GfkFb9zetsza?!*ruEF~OhVX@@z;ORKO*9I}dJB!v$ppC67+f-uPT68VP z`*nmTYbK6!$`vzDFHC$xRrXuL@2Hi_7=w?Ne$xqOn4Y`JI;1y$_aW?SW*!cFQBVHv zMYuEJE+$ch*(#15Wc|MIg<5;asClCAn`V9yM@rTA~=3KW8Y!c zO&4Wi1%Ka9xRMa^_A+!pWRuxeAOpA^)#_4INU7R|5byu!_pw>{7V1CP9%OKw$t<;! zu!>M3?*>A9s%AA_(>Yhf>2W07@r19CFt3{Le@Dh6;PIv3#e{I$((h72`LTPb2QMQY zep~u2Gzpo3B5O=pUzJDLM9!qK=M{4QtAwu+zD_6^3=9pRH;5l;HZRU~dtA}u&REez zuC`KylL#y9K3LHrlJy$G$qcVF>tGoxO!Q{o_94Xh0Krx1Ejp6K8%KB);nCJ2RP-bn z;VHre?39Jv8o@ah{WjW#SJ7hD!4SS?gPqrPbYDyCb%fW;FjEyN@jNranHv2zo9{pQ z9D{p3f8&6Te%M5n4THp&Y~)vjFd4ss)n;MVb_XFXNG$Xvt-&^de-m6*;i_Rs?3##Bg zjvF8@^42-ktW?AY4y4 z-HO}9GyMH5VVz}=-Tp&r{6+XT;Xj0u=>(Aoi`ou|3>0CVxRXNRF2cLbit=^XP+!g8 zYY5j8t|OGJjzd9%*iXP+d#|fVxTxEB(6C3&aCBm0=O1G=$KWU%% z58+>ge-lbwP6~C~KOUZ36;ZD0KH_e6_dSI765dBB`QSwdU*lLt{6bF4my^j4CD{)n zgtDm0LJ7<3Mz|BXS^R^ZW*0m|2HwQ9aKY2>5E62X*#?tYVSGE|f3NY?s4}9Kaq9@{ z2^$C{qpDE-8;O68^?#oA--+$&PT0dlV5jb0l*=;n2=fUuOyp%uW?8?Hy?@80npkP{ ziT_E&e?qppcz2fC!!GoS)=+O-OSq2kN%MVsNFC_N%G}S&uC$V-Xc@=Oa>9og{;=JT za7QN0mdmO06m4aVw-J6w__XQy0a{RWI?FzTa2{csnK?(Mv&UJ9CkR*hlSuq=Mtve! zRTZrwZZ+ZEltTAdYj{xxNf9H=B#fJ-x1s5Wu#}?-d79f9#9B*#wuZ)78S#S&_aH2{ zTgdS>B+&`fk@~T?{)8v8oRiG_o>1|Jv=a|^c>T^KJd03}xUA715AcB3Z?4tAMQ^@| zRk)cDR$HC_!SY4Rh+9tRInmCNEw;h85qmq~9fWrhN=avg8h;o1`6_Y2m=~(ieT@8= zS^AS~vM=UuL%4)+sYzhhys+#C{{E5hC&HfzrO^4I{C}~d&zr8^BLUtg{J;+Uq60YK zhZBw<{L8Z4m~$EbJi_w{FCdg+&j}TKp%-5Q@YT^CQE%3502{}jWk){eFC`KmuPEW@wsHp)5x}BoQ*Jhmk=i)^-H&UMA7?{pC1tBldTKL);m*+-i5F?VIQ+_D5#vk zi30tuB)p1?CY&MAZxJDm4d{0*%YB`{;i|0P(NqY>TZOQwFU#4L@K}Z)M}_ev_TtNd z%p5Y4dEu~&e$9j}go3<0MEX|ZXUX=7TGrjox#o{+Az{oQ$dJsUg{1K1gjWz=X-8F2 zK65A_Ob~Xk+TfVZ{GB9>GJQIs)MycEsEWW~=*{5&MGL?kjQg<#kfMwJ_jh z#$P8RykX7hgTH4nKM?*%2;XD>yB@`P6ZwUB*ZX=IXE%iEa49K!nNqfI{8=*ib9RQa z>eb*Eh+R+kBH;!?DbIw8+L&+ly#YlXvm}Jv%*d}0zDoF7D3hg#@0np=CmwId>vsm> zb~{#z-eCBfgg-LjPc}U9gNG;Vvj|2Z)jQh0B#FKv{F)H{}l`sGVndiF6#ao`Pyvj?=NW;mH3G9 zJ|2!Ayd;;%F&iye?nm_vA8XsE3W(c^sLFCe^- z@FJ7I>tTh7tC;`Qgo~)8%1rnh>-Ib0AG+meSHhPtpGyfZBV1sO74grQ=5ub)ijdw; z-1me(5Mo7@g}O}MooyLFSV}mMQ2P3@P}|OAnP(B6O?VEW#D6^0*XL&Ax(qRK{KA5N zC2S`*%%@JZmH2Ih9}<3KD_S&&6)GbfOt=SEtzRe=nN-__us2~JLaES3RLGl&`Vzk@VauYjqhMlN{#}k6@4~$ACcKC6 zUbC(f*fqX|(^3a+1|~?H4ul;EGdb7a$ujOD#1y694VLRhbS55WI`k_f>_RBWS3`9# zBEFdLu|=MkFN#x&W)Wr+=2*4bJsj_F-=-7nq+B=NMppTQ@K3`35K6&sqF`@6{fqc3 znd?=AR})J7w?gq35q}NgwS?CZnl6{ZlPuAd?ITOnI3*kG2 z?^>;-Xg#xkk#GayMmx2*)mdiC?Xth{5EDM^JN*`|VcfNZ>j-b=*6kDgy^3%(7YQOZ zA0_TF!pDO!_aR*Vc83st5WGb*6LF4*EW&KN50>>eN$^khqL~xZb`r;$#CODPC*0d? z-Ue4JwNK9{tDQ-B7U9`usVox43}Tux!oh@l5K51IjvgaNI_1RQPTTJtgmjByJ-9X2M(SEN;e~j>9%xhRu9&8zb>#y{~jad4vBE$_>{`Yr?pqV5{oG{Cd|1q~Sem3zqzo%a= zq5i<*#-7;P!1`?@e2MU7w)H9gewy%^psjAdY_tA8PxpJ81w3Po5;Z?_wEaT(E8%a1 zQjzpfMSds#55hkQ{}YOzf%x7e^%wDfhjh(&ipWXf^-9rR6lHr8?n5|)n#unBJuKM7 zD<^7CU-~6>W!}3H_OoTEf8PR9ec_@C*=d#`-$;7QCOkDTF?Hz3rrw5??ldp&V&;OLYu=4&5Z%SPD z2u`9GEa~C?jR#x{^a{Vf&)EKOMR})}T+{wIZdi=J;n*9M=Pjy;rFAr$R>MEUy(n%T zLukLOm3Tx~@_it~Jap@7cPEqcM48)?X>40#X89HCaAm^pOFOway>W(LqRwIx`P=)_ zoRJgc?_1;M6=cva-kFigP5LUF@`_#!!LWFCF7mqIkGPv{#sZrex&DgzjU9VOor>xv zBwoovB_mwqs`c94W&O-0NYUVa7%_k2k74eb40Gv-!FW#Tcr>U44$iHB2SkUU9JA^6 z=TXp|Kx9YuH1Dnc#BI*Zvm6yOlYWWgqV7T8yzl1hJ{1>&NNLrCI18q~@fa!IW5aVY z`0~Akjg5OsuHW=S^*5~f#N8)v0M3m|l6kJ)E!nVaK&=a|uJKXH$UZ$W(| zN?SnvoJf&bg;*uHjatviF6kw?Z9J?BSm*ejObMgbvyLo1A zdX}4IP6lS5=rwZYeiMk<$=n0PsgbKp?SEoZA^b!e`B$$Jbiiq_`OW*V?Fa z@nuJT$93~wht2%HmK$#a7~GHAmIhH~?Gvc!@U1wH&B<81^7yRg1q=BFPo44>dp{(7 z1Dy%@D$f+!{=^=84GlVC6gW5I@}S)>uuPk(g%VHhvB&tn!5|tq4N>D*7GxZ6K@0g>VXkY@39wn zR?(RVhXyt)7wTyn-uw3-rsNONuuaL2f%)EG$kU{ zuQw1fQ((Y3k^iTVhW%lWJ-*^DeCC5II96rp%K}OvVY%lF+V+QW_L}nVmCg?3(m&|b zXZP4kJZowFv*YY%l?2-Q+#dU7Pd_^Ic~s8+#GQ_y z3w!LD4&q_E_t>p;&o|s*O!BRW`Kr84TW6&5YXhx+5p0hP($>Q&kW>uA6~2)DBI@dy zK_3HTdXb$q7sAohm$0>in(@CqjH#aJkY|ve%D%;=V}XABISD+YsQ8b2>|+AoE03O{ zL;ncYP-}`?|9zxIJk7Kp;X(G_Lsawf9(zcU#~ADW&A(>#GvQ@)Q2TukZN`uKfJbG; zrjG%9ko|Fpg0J9)3eUOJ_my$>F+O~30pI^F2_ow{M1(v;Xd3{i{0pUNq+U+_cRKY| zh)>|>TJ0vhLuu5XhmOLu=RdYJS6;(mx(Yz`82L@aHZzd5pv0yDLFmel0J7Bc zIL&=+oPEFjtwjy5qh5jiIxaoLV*t&5eVjes{szsBAGm9ro&(TO`+JL?N5nxi;ZJ+) z>At^#akLM3B7sj;oF0|zMZ-sOdjse)TtuMBZ&NxQiKtQKKlZR5|)7O@4EaeSh#F1$(f7=8YYRA-MLf zJ$AGGGcO(WJy-oayw0bsl^`y1xk}C_d96ab-{R3C^7bBka(?C4B4RCV{dWL*dG0%V z08)+rL+OEf@G8=!nEI-?9t|pJ!++2_hF`5cqF6!R)~iI;p-|hsfX1@Up^q71#R5&o z9s=ReB3#S`-BKBOTe~_2Wmkr1?4cv=$bxOk-NTi;(^MRP2~gj(88e$z-tN$>=P51c zYIrFG-=6P`1eSHamr8zI3~q^(2ga*#4VH#|AJqy+E>UXWX3e8!DAaNAH;Q$$hweK( zXxB%u_fe{Hv^M&w$V+9{Fzjubbi0?H#OPaZ--x2O@3M#ZZbAxefC%)PZCi%4_q0QB zZEn_#S~Cf}){HFw_(vYxEeeOx5(9hyWsZ7FqwdxaTQY^bClK2DNr`_teQ)2O5Ja); zgtsBg_tB<3EJN}(?*YwrzlH}=a~>9mTti%>KW*L zxclqP&`5Nan=arXWLtoLKL{pM>L^Gs59VXwHf(mx^FwB;Zc z9&9@mPTuOFC#Rsa#t#+dJq@#}ewQG$Q)vgKb(yArpQ4+G|?&o#b4)d$RZss1+ZsyaY0()ZCfsq-w%D@s*#Pf)?2&PO8< z-6P)LV}~oM6%9U^Cur^}q!g-G>8YU(0F0qa&ndPen+Kwm>;XP1d5jzRPe|!wHy6@( zO+mY+q%BY4hWKa}^mToBkLrfuD1dq&jM%mISCET_9$ zj_}d*3xhC2!e&N&QM8m^?}w6o*zAMLzJ@dc^grbc`c@&mHFpPOX61)_aQji~2jjrF zH_;5L_F_{<4&{~e7ZGfp7^^NMqc5R}Wn$C=~E#$(p(P7uvi`gW2 z@J!GgP`KGX`lPcMVmyaN-@y9tgnwYP4DVFAs{ndhihdHor-EoRNWlaujEZ;`)0_W* z1)&q8O{$LZaRXug0&Uw9QH-7F7BcwXDR`%S!*_s;n=n9yMkSPvr5d`B_~8h_Lk|6A zmmZ4pPeQ9ygp+w0XbW&fJoR)c?CXQ*ijPnQ>iRKr?}{Q28#!FbIbLPsJtkxKT_f$j zUjDW3;X@%t!`1>cc&6e#%V*TJr zKDyxg4?xLqpbXjP_$YPmqoDkX`+%6Ak`aVtHtncv8du)7+g~Ed|Q=*4IfhFU)miB+bCWFk@!`CS&b|iXf>mfnR zU2mY#5>s#O-v;I0l=&tNvul$Qj4QCD2do5_sP|aL__2>pn+0t^=RFJlNB*f0 z|JIOm8wn0v~YHb01$A$Rn z06nvi~K-4>t)F3O(eyL;U*ZaB@WbZ5F_3_ z3JvHPN^hcoTKa(JCq8^z#!F+r+l~^ANib4#3@e}~(T(eA5DKoP6F!F;I`~d4?n`aD zMgZqG-AxdVZ?;1?CP2tTo+IeZ&!KI6Q@QkaPGW1nm>uc^W+jTtg$v)h3TA z7nd>}(P7|%58n^NrHyCcUL#tIfi)aVD4JHZ#o9+PhAgL#8+a&ds%6=^NKy3(&|72n zBDs?~zGTLncodTDE{Mr0w!4OZg)umIwjw(N$W~(5_VsYd?~Gv%TyYBb*_##OTn*1* zfZgM`hpjK5=lAn?{60gL@3%{V*cXRfoKL!gX%+;tD zH9Sk1fyGRZH{QbTOL<<^^iNbwUZMT}Vc*;9jA|IvN3HKq{B!A69-{W0hU!&Zt$DDY zRI2%Ra5Bc#c~29b*RX+g(1veNo$3=*Mn-8l4I0j)?MSbp8T~OB(w|rc?eD>!Z(zkm zyb9#gkSV66P$-dbK%@K!sSPv7IuGtS?Xv^zd1V}ivt3+B4UniBdi`6JZ&wu3kY%iR zf=YCyBB){ncU+8C3{-2>p~g@6`l=7W>#umWTy{KL41rrvd#B)o3aK7m=KA1p&Gj#+ zwggIO9IX*$8cx#iP4qxJo-};N|1larq>z?Bh;ADkrxE`~Kfd!&SbpEAvN;S1H1z@W zIQwvzYqNLo=)fK$dtxE&sO4eJsM#rs;iy6?1e@vub2XxsKL3uJbqCjG-x2IyNn%(G z9e2C}EYi@W4nG|GZI4-~lCF!5gVN>)z%5_tzMeqq;RF~Oj4RwSR1SWe`fxkr!&8rr zP3J;vYlG8NCf@bXRDK0=s=4G zd$qKm7E&dT7Swh3q0l2mel$VgmzvmUq~@#lN3dtd(}?AQ3+M@i{UaAE$OlUOF_1xO zPQyMN5mvSQnT9tg==BKgWnh{To3+^@;U>2UZBilTj+SZ_U8NN1rbFxiw*`f7!I6l)a_uX>u}MK$QEXI&vePa(;Bslu1B_^6q&{ic^<})P~9#N7JF4HK+28$NX!53E{eBLisefOg-uqwBL9+=L`d*A}&tWtchcr zG<~R#8qi(peaDJ3XND*Wkqy7<%}61Av6dl3N&PJ#vX&ch31`X7Ar%^bxtp)P> zx;EhEqnR%cp)SE8-)UV&|G(t>TPb}FB1V9>qenomIy3u!C_dU*xEJ;K0FJnjXCiG2 zga4k%^zj_7kIbY!N6~(Xok|}UH=9tx+B6Tf$@hhLiVVq&)(R4F?w}LsG*~H}P_h>`$6jsn zU#TKFpC*?!Vp-t#TJA}C+@Q`fkA?Vw*Zv>LcPt&N$YKDDEAET72iaCny zx2n1a7SX(OG4xj~(+qJ9(|+Uq7}cr~Uur1LrvIF77&SZIuzHL6GuF}|aN+Qi+KelZ zk!FWsi3E?+nczSly;=&3F7jhVx=zEh7DhiNd>*Xx{DO+h8|^Wb=PN9Z zCb=USh6}kehlh(In0(>j#hUsvWM9ucXro44?$<%#N{zVQPnUm%E=7yr!l|PG2ANPW ztBJQFdqtx?WW?qnPyzKRzV?hhL7(&5Pdi=WeKOus}#eP4};XlN>D{^wV5EsUoyyIb&W;AuP1_M%LKDIJFEip-SHjuuhF z2P5p-P0wJ(-8UFmHxCcmgCaeOf24*1gtYT+m>!YQ8g;h%%+BQ`Gx>NPz%t!5clM9(9kavY9XO2UyEHY$Jz4q3ak%dOREIukmCMpr@SPdZfLwrIZb z`hFNfZ#v_eI1Sp1XE~EY)A^#PMrPQ^bG?{89ma zL5YK)DT3?u{emKT6&5nYT2N@6!H+EqB^#YvE4xCC2-Bk z62kSsLd{om2f0BJ;%uTIzLAsefV(pi+^SJGp-sh}+6D;%z9GSzblAG5$SAYmaxLIt zR2D;bz1;{$Hg?Lu@eo1VgdbB>k23Z)*vz%|?~AD9SL_Uba55C82dFVuILL!R3MT|{ z1GS&F-Vxzd(&Khu#CZrF&0xg~ie{3AFBZ{Ro6$w>moP-%%e<8-V*SgS{)&doE$6}R z6So92YnlV|79FF;W}V~|E;^~cOY8N<|3Z{cDh9`J@Lh%S7tvwY!hE#fFQU_;V3_^W z0Hv@_Q%PM=b!9#r*n&LdgL)ri7H`e>- zNFDb%=HfK=*PRIQ3HhR2;L?+Lg1_#16j<+5E2B_DhdUf%H&7Xes%uAEN`CyCgSym%ANgFg=Vjmr-I%Dz|crvEvjC_UI-&K!%Y>z z>e1+dJfzfVQf+_=AwBhx{u&Y0kjXPMG0#96U60P+SI4@um0=7qBtTEXO<8Xb$CqQT z<0Yu3CsFBH;}px-JZuclk7F!coQLO-{{@%FocAH>Vf(s5I;|hOC&alJoTQnK01=PD zR1WC-;Q_iFzLomm6pc6x-wcOQQ}273S=rqCC`O{KXK-IQnob=IcKH6rt+u6$hp1gK zBt(wUtTQxhLP)(sxF65asKt5q05wklckPAvR{N9eY(D|7VJPDLz&^%z5@$AZ(H8!> zvyKarK|hevA)=Me#4=j{$Qs4|V-2x=6Rv#Twi9UAp(skv0dy$Mi0`obV#|XoLbHdU z)Ozdz75(x{4Gq`L|NMSuXgrtJ4+S#&R{@$QYUD(a9-`G-96~DdfYlY(se;8eZ9()11dwOW5$wyBh=(*}>{M36&GRCf z=Xq9@;##+#2vA8YzdrrNcJ$oW82M5}z)b)p9ID4j8Nu|5>R^XX4xuJ;n-~B8`%P=H zKO=0v7ogJ~eS!bY)Esg2`okCw>v3D4KwYo7aEqXu>;JM^^g8U= za1gh<35L%CW^xhyk4AhIpyjfmno+6c}<8*-eG+ppJ$ z_6rthR0xih~qacWrg_128G!h!bQGbaj!FC)m#tznqfkB$}2ucdz{I5WS zY1100k6@L)7YAuIdU|~T*KP{Hee@d3l<4%byP}DCw(k{kXaEK!YF*p_*(aOQ8nte); zUY&v32jh-)i5kY(+-y>a*+HtrHsAWlT#YzZ!yh4}JD}zw;l&!Ygrh_^1#xwf)Z;|d zADy+H-dM$hky$#!&VDgyVnJGrO#}78r3!tg-wXlaUpv+>E7^URr_|cZz_9)BnzIAd z=`OVIW)@)_W7_-^HcF(g1=|$ceLfoUEXww*q%+%rf+pZC6vRavLRuwhgK0X{G*IB*qeHrXl@`%3{;wCz)%l`@e+4CTpAz;Si)0H#ye_8kUCbl83hRqtf2KjCvIz`!$_ zJ4-Vm3H>9_>ihE=K8Mh3rwG2RQGX257nmv52jA3)LZ2>d{9Pmd7Ni>r0b##~kMClI z7TkOaD=eN5>ABNk<6wEpVVn(n=>O~AG_@A)t$JK!D2?%ecA*2BE|gd5|J`r20W@qd zsMxWMcr!FheGt3vrMOYhhi9_2HCpu{?pl!qTowUE0G$@`;XEbWW(90f4sYf*#DPkI z+3(%>*PCI$U3thb`vvNm)M(#}w?ij4+8cvas#mWI(5r~3hd1>2$$RZJ6G z!}a|`eaFO7kWbV2YiZL_jdsj)3GGK%RqoaJ8yw#R4W817g9v-E$bujBfhQHw4%*HM zqedtocBhNJcu>P7sz0!4O$uBn-4vn%Luk?xEEI2@3b76B)M>ye^c>!+8gFyx)G@e3 z>G_?aIa%XzIz`ZK^K86kt7imF#o>rSfeY2Wei?P(y()OAzBgmT6qaIYDEj>ldmQ}< zi3bISY2sUS>a@M~g#%C%sR1@q3hI>h(;w;keA;$&qkYcNTa?7X8b4A=!kG`rSEujq zC>+-OC4Q5>U#TSTnlK9e_TlM`c9a%v$7X2Sck(E^i6+c|KiISJk?qsjYwTG(pf~i?uro3!ZZ%Vg@w{7$k zd#BTeS$pkgE2b;ZKBacAN6<;54cJ^40?BV2v)A6@nMQR@jW}r%j<>WfZ)@+Mi#`n6 zeSTz()QmXNF6=DUaFnuZp0ej~jsH@^yOsS|CzO0%e3AWFKJha4u83W9QM8P9Za>ua zc~bP(=XTkBE{ghU*C;F2YWPbHe}a%U0BSb4(2t^nKs9!;zX^0z75laP=^9?(+4$(K zo&|Kmk06i7fm$(qUsY5uX&6!}x9Iz88eXemK-1sW_h&VHgP!{vHgz%iWi#Ia^TJpI zD608FE4fL*uJEKx*7Vo7`|f<{P`jBr zj{%xF6)J8wYE9l&@!X{uAJf9$)c1@c+M(}j6ds!#yqmFswI4Ut=68d(kHWLzrAEz- z9Gs*XpV9X&MZ8@ReW?^r)AU0%{7};;@@imND-@3iLvJ4zh9$IVK7?V!J*q}dQEK*S zJ6xfKmTUat0_g=8MFVt16RIEhok~wLO>SmQQ>s*5?15{O2iOe{J!(&&7Xf@wIGWVg&O`^!x<_|euewUr!ZfPe&iC)IXr%d<_RHp=&iHjUS}%ztQ)<7P;)$eb^|w(382Syx%1o z))pLGqE$OctJ15z{!x8Ti!6>a>cm$VZ|S7?tI!)4MGNTs1$)soW@{N;TIMEwUue^5 zSdhkg#ZjZ|Y2J>`3|;h^$ytkC7Q&humHf?`=PHCW7W*m9pfkI`A8%RimgpOGjl#{+ z@IDQ(aX|1rrs1C$&%Vj*KsSTe&iUxewKx+g9c`Ot`${W&6^sEoe!=aU;WfiY!;M0Je8tLCCv8C!MH(#Y9R)f{8F8+@m+07kr-oSW@X|X~A#}|< zevfT7_1p3mCu+3@-)iR39{S|19rglNNWF_7UV}bY2<%gq%G9Vh9i|=oYJ{|}h*uSj zQrhwv?9LH~Dp;e2Un@1^_5BbHyAK#YP_^2|5em9WL7rEc*{T)2NCo)>O&?%nMn<%< zw@~<`cNFqg?LfOV{G)c2t2F03+7-&QgqOH3I~&jhQkP-0;l#am?ZLRC8h-AV{$bjccAkVnYX74&7bx;I%7t!(v|%S3 zMrQo~&&e47Yx*kq?FzP9i{387)k(Akwn8m^^tT=8GIKQTN`-w;!~65ZYdAF@0nHa) zplM%2AZY0lo_&0j*C&V5>jeO-`B3F0qcy_a!NQ_{VqX-t;bMR~tpnTzm&Ge8ttAD zH)wiTd*aplzF9+@Q53SzvnVx@4M0}Q6ANY^JGXfOmH!p$17^wR+RR(E5L^x|nTnOL zYxVtQxFe5bBt{1`$05u$UuZl1NSpOzW!yJDnm2QleG=Dy>j3V~kr8xfqR~Ef@X)apHNN2mV+;L7Rei_mGh}RF zKwU7@uQJ?c?AYq!VH927Xg^$z0bV*~P|sF1(#MD&S`M>G;?cqq9z}y!uo--TzE^0t zO~XO7eL2jj_`6#FYgBfiiX}hh0TObk?i9riC6kysZ&#-f15xI6ed-Xy+;VefoZhiq8^_U#;Ov4HwhLZKzJ; zR3!s9ED0XCJtX{21>_>dvs#<>Qa_z_A$s^DOIN*Pzu}>0SHWKFfz4?ur{PhW>u-t; z$8sh8V2%G)-$&~Eb^5+h!|f^(IQwX*(QvXcC)(>7wjh3X@6JI@0|s1s_GjNa&;2XMM^Zjo zeCT+)vZoc+{lsZA7Fb=+6P@jXsKY5hu%wnJ zR~Uji^9lf>>Fr4jfBI+vC^g;M-JXk=3J9LoWNW;u;B4K-t?>BDx-W%OQItAtyj|b( z2Q+$TvLV}*&cs(Ftgb}1A+!yRgU6rf>Pn_oWE)nX3gAc)N1hDf%cQ$I=f*obk!U5d z=@7^BsmiW3>CCE`nM^ukb!S=<5|3?1_KL*xbgC;8Z|TZf(^})wgNFc5dUc{A))G$% zov4OvT3ttVhhrOPp9>ga+d|Nluv+j9^=Ma`jzP-Mx&T+#%A`ARLxp*|lB<(l>nsMP zUjs%y9wLgM4aHFN)M0kyWE`aS#xqt&+}*!#dCybctcdf+=jWzdRtZ8ud=OKZ>6(`5 zN-hWKR%*q9u-?n)?-ekU89%B25##N;vRZU7O_9c;Ifx$N17?fPz|Z;~y(8PtwaQun zBQus&c;$g@)RI_`$abbvZd<2TyPtaJ#t8jJ;X6CWp!fjuBikHbA>9}C_O=Lp!U6te zV~pmf($C+0kEgn(x5c}xWFi&j*tIlz0ysXIPlLu&iFPZ~nvJbVcD2Q_BAhsxWhdm= zN&vAfZScDLFxd0sFe2GPf4zJsuQX)S?cK~vBH*l!;lQrIWmnC}#MdC_f<#L?(<&lx zB+5&r*MRNPpj-hFnHxZGuVf1>GUb{x2ppAI8(X$6*4>?KwUVi$<5V?qygls5Y{GOu z&ulF&1$k|WgDV#QS%JHh+-@fx6i%Gg9k>(UkN}37(3o7)5g}%#YcjT#r>fCdr>HgO}5jxBj2}2;~I__(k2f) z165A8XRSm=E8UWoMk2S$74*XHvDjVTaB)P(uIl(Hy#AM(j!#ipWAWT+Y>F^T4 z!*=n>d(jmY14~*5NOCb~yvjwcp)`I)M(B_I5FTQ(npbb=g-Pf+O)sH@bi06ZXP*e} z6g{UK)-vllrE|tpXn~9+yaq*fVs$dH#`0yEd^}RZoAA|+x5xBU1K)~S$@Z>9=4k2j zU9hn5ECnxCbOX;qN0Kd3I=smqoIuq~AprGZ`IH}jnspiaqoLR+e%!z6a3n5JfYZ=Q zw3Bw1v>hctdR8w3U(f*jcrVm^ z35ylG%L3C&b3+i#pMsKy!TND%9D`v2je?^g`t(W|$Yxp^GITY-=k#2j&&NfOPy;H} zpN>joI+9s#L{-Z&$~+08SQ+yWv(R*I-z5$nXDDg!7}J?aODs$sB`KJ+)-1s=gzcQ> z30S@IBbcS#=vkH6@poax;g z1&N(@zk|Ek9^fJP2X#$nE%Y3;Lm@R!8;{*BgMzdYKN>xMpiKy?Dz5@@(oQ26fAbb?l)6E;fXcqlR`_snV>5W~|5#F>N>-voMBNc=d&C)yJZ zJc9dub`>R(bQvRyl+?cWLFDgMV`k3M_Tox-cHH3wWHmka09mh;6NJ==E#UMoHi*$CZaH2C zJ08U3Y!<0&7z^b5_~TviW$`QqDDwocV9t*}-p&?>bQFFUapj2>#Dx~l8gG|Z;>Ip7 z8sOwE*z8TMr;Zuf8TZx&=?o;+jF^E$V`8u<$s;mz3-iHfNQhuZXIBI5K!#(Z`r!e8 z2DU_-Lyb<{Dxvgf-)&(#avDzcc~B@Uz*t*XS7)p(o%J>=$=q8ltEfGlUe(Ahy3P$LdT7u1Lh=NMcZUDiB@0#a=PjmXdD~&nLL9Zv+A&AfFFNXf;*^7m9*^h zGtie_SL7Zx7Td2*&@8LIt zRMQ0hVI$R{2uC);tssDGVz{;$*s{_oq&*#PwVG0$-Cd)i)8mD!f^;!54ytzLP)6E2 zQQcm_6r>lBu2;AUO+TW~5i_P|D7c)?bO@CG^Zg{R_`C!kb{6mhe=Q~{xdh*8jqhT! zD)$uN^dvw2MN!tRW1JySMvFT@TUXker(z+1-rF5SUFp=Qs9QfVtrj8!tb-`G$?QZN z{c)66zYpQ+odi8vi;Ec0I>vyj|Iv7HDMNgu0Q{3yoC#d>;xha?@gnM55l_+I?!<18 zGm*8l?8@AjmhFmV(sVVVe?(*Iv0bHcrc#SB19L_9@sd*up16sQm$V>uU^LEQNxLOP z%kX1(gyTc}552|iI94^O-GfeP@kU5D;V(4@BC=RkkK%Q z&2Z_l(()(J{uZE6BF%Ld|p#lHg(q*tcIunO>u)xf)UYP8MDuFpZroFu*22%^}E&_-%W5$OKH>_FE9WG)WlWzz?KU2V+Rgo=q7#C0TChm( zb@pfB5@^FjL2!T^{k#gMErQgM2I+4dRImVioMm7GxVl1Mx>qa$+bj$SNuGE3vAap) zdnE{_;HT(>ndEXv<0%bfQApk^p-MzwQ}&?^DD3kVv0hwxZ6 z^+LEf=vzdL2*yQcVL{4Ay{pICk;*?~a@(4KeuTxZQ-y+CaL@N5WViq6p$qV%vXbwW zZ8r1&I7Z=#)7#^PufYI7VolQ!B+kY7xtfg;BWofIcQM8B_I2q-sdRCheOb5je~$Z`CbhLoBn5HE9Gt2^0_qTt&SAvz4@cf;`D$zDFb@pLBR z3*ovJm#>7F!M+!PiFQvz{pk%v96U~{&ZG!J<{@`Nw%iq=u&oMg>qQzkOk)-Ubr3z$ zRG^UVSj^}iURh)pjl)TAuK_gf3*>MSn;1Wc6;_EH{*E>=I^PkXl*$&fU*TCq1pBxm zCJdc`7#L__r9%wbqI2%iXa zmCx{eP1$8Yt-yc~O+;eNK;E8UIAuJN0PyQ4^Hc+eXbm98gyUHqs|&{j>Dbps!2H-r z&mpO*61TD1W`NHTF*rnDpZLB#sMKY!r2Uvip9E}?FWo*Ba=R^+p?X#H8m0lI2UPVo zWGNYl9S}A&i3~Zz_z&4Mu$S4CY`{!`ouwT~xHvowKgc`fWYncPBr`^+ zb+?Hf+-aoO^+|O#gw-x`8AM1KRk}Q#iM2pC#WB109uD1t*qX{FPz~JxffP?L!dT&T zB@{dbil<612lpo-*3j7vE$xYTDwd)7NFCIZYE7hCl8A=;P()~MP9VBs-7>*(sf3Rw z(-lj>G>f+<&q#PDYwgsDYpF3+G*#nacPcl8b)rd6f{3PB=`_Tkl5YcWsPk-WVA@b z=!{Q43h8iz{7a!LJrNp>uxAcf7^_fjG#V0j(si2@d}(tuys7 zrpgr%t7*+mQRBZxo3(cqpBSQtkhr==Kgq^=aZD%NWGs-wF>O92M90Q4`>{_2ljX;0 z5PAHV9O{ZfG$GLbr6D?a8OwRAa?{9#;{mm31ybv(SD?;}d0i}FhLJ?4?96DRO-LWv z7}MNsiI!Chy15nQDQeO9q~nD&@qq+X4>C4}l0e)mk*XQIp;Q;W2>sI68-lz0uP4E` zL_IBFLTF_k?Cpq&{z%3)Ui5TkqT)Da`$x;Q*pY0?|V_^^$uRajAC2Z}z%esNz#*s-GGLqCS^!;LRI zFGN2}K)2HkKw8q&fdNK*mFR&mk9oK+I$H^1d#!buCH^DWp1!o2Y=)Q6ZwVP~Ma$ex z`+=_t7Xb8Kj&`@t57E*1F=&vQTaJGLO=1Mh9Sn*tfDa9zHAmiuM3!ZlK&~Fgb4ck6 z_affVIL2a61`OSz4L@Mo+rTiu-Vr2w1xm#MqM--}*q1`m@uMMnxx%aa-Qk+hek|}) z*%cVwhJMUiW|%`Q^;@WZ z_>p3aP9M>lzo)vDEHSsEsqmwwdJw2IR-p1RM0U>tjaseVo{G*ruSs|H);|G35|tYr;fF;N+jw zVjZmqOjWfXm6KuI$|k0DCV4$haL=IaoNk|L(^(+5hW6n#P;IaTPB^g9Gs!qrbif85 ze;))g+l5XQo7M@R0zX0h@B@2xSpDewVN95fRU7!>&R~1?CqU4ziO>E*1kBjfh0!A$ z^**nq!QTK9EjBuM{P*jZBmAsdvyRr##v4O#0$)`%n!;v7E@NSx`)G9Lg$b$OiBz0= zALHz5nw$b$?Zbd;Wb9^A&xA`O@?Krq-G#o2$?KWm#6h$L86q{~FtB%GW|>$VNA;J% z7DQx^8nOu{@DU6n>)dW{l+g@)TZu z!0a_drJYa?Js%w~Sr4(Zlc&e75>GC}YJ5(Dy!wqIKdc?79yV=eC)zs`qGG;4?Rup! zQxtx>cv@B^Qmt#6lbwlntadIE3BZJT7}%lqoMbfJ?FRIB_CYudKoY$Ma_i6uoS`~Q z-y^2VgLUH5QIS&pn37sQVLAq}gG2rtNwfwrlZ&nahSAsob;*FU`_K!B>WkC)dk~Hp z*aSf^-No(g!ur{W?-laGbm190q2uOd#@qgrV%;Jq-+)?U!XBtZ8jvlP)n+tCr)boP zIdloxRuRi>@NkN`9R*DGyhR@)tn$5!5@^c@Jg=Dbj6%w~OzYB{l8S&BG~5L8h@iX;csALv>a}X2EPugwho)y_)jx+C z;zh#lOoI0Zac`8yTS9Lly~@WM&}rWo*52VY;0&%~rf1e;wojAr8aM$swX6dDxTQY@ zQ$XW5DA12x`q#CjJ6ANiDSrLPQu=Wt`*o(gDWWU{1azQ$J^6;mTJA=2}f+M zyT%}7^k|9E{JT*@_ z4PA$r)9!R*43R+Rs?+8s-Y*6xgLFvaVH}JmF=Fj%)Z%WfA8#9(BTHFAR;U*3j6w#~ zCgB%`Qk6#VcKDg1GTU~zlUBNxLappD2gq30salSrhp?wU8&ncsCNVZW-` zgO33r@vE{2`%S24RXQ{`S-5Oioo?asYDGG%5=NoHC*ucbyI>!os0~Q~7 ze;fNp#rsgGU&i!feI^^@saPzr7L}2l)pX`hxT#OhH6IaT28U3XD;86358Ks+P^$1{ z&?iW)_rNYW6B%d0y<*GCd2C(0qdmq=CvN}m`M8VhA2ch>pwP3};f!br_?6nbJFwfL z^tWMJ31khW*D63SR|w*GBVq^l;G~5eUA8XT)s~2M#FMFL95)H#83_^U5cdl2$%~@n zN2O>#RyPu>A#d4+=py7X!*2!Y!?Re8l_BxKdUgula;obAud6GNE~8%*#Df*sA0z2~XfCH$Mv%@XW`pH* z7*e=3OlKgmreQa@XqJ63J2ABvgJ2i9h?WCCF*z``_uYz2zt{z5=sWbLlw1SM@WkVuJ?q{ggoO8INLAGLT=(}8KqNVr_vE;GL&&ny*fzg*&rQ;=cYJ6-p{HCq3 zOgF~#jsz~Q#&bLAH+0 z7lDMV1qqv&Set0!%`;{&yAWa0j3`_JKf;6=x(nNBvj&_so?+Vf7c~2Ad34fnr!`$0l6nQQ)_575p3w_6_HXZ<&ve6nUEH zIYd=e9*v5!X9sqh!vNnSCG@qxV=JB)x4?|BBcXZjrp&OR%L{R${@vmyUvLb2riwzBQAW0-1&M`;g4RoHwLmSW^e~Ilwvz9^Z+9J)wPBB4U!e`1Fjo3#PS|qyPP;m4>YTzcgABHM1x6kq0~i8m z8eCp|dtlBm`(n$5c&>M^RKA!Oq*AM5xUy6qjc?IcHy#~OL3v~`eWjmDkD;y;s|4}j zScm(k^XjtI8coC8IW`j&M8~)$dhxob=yVg$g#paW*o+W_rX0}Kiorm|(xC8?6cI2|K}Hkcw}T6GY|DMZ6H6Xowz<>0NYTcfkZDm9`vMkI>Z8$r_)t5{0u;CZ8(hC3`WPn6nvUDi$JM6{y2lfxiUto$cLy zjWxxdglsG@#UZly*HHY`{3XeZX$@2}wIUifIN@$ZdR65{r1N~G!NSIT!|TGS#q?(+ z)*Ksz&CxK1=V3ZwZRiv=`Zg-uDZW&fnWbeqaY!Y*VliHIHD@BE--JC>(g03{e(MfV z#S}XqW^*;6eBM))_lV-&OPi2h6+uy|t4zHB=ciprs;$KsVQ1ovR(njZd+NUvllcCxyi(oAceXrsFO{ts##aFpV>5!MOk+=0CykXlRDz8J24oVkueE#+b#9w8m-+n&H;zFx~v(T`n|Ck3E2pruN_5$U7}El7X>>c@>w4hJbm(=q z*SI>dwUve5Wr;H)*=D}z(&)KDJDlMXt510fuhRH5OjqZC5Ev=#c{mt|gA%3KdEg@Q zUtLVW8zEuCpMiv7YS?a_q3ac`iGt0FtykcS(wuqCxgQ6w)rZ7zRu; z+oo8R@h&Gp_y{;{JVFk7BMFZqp65u zAw&S0p};xeofNdwH09FqcJZLGyvhT*u;IX3Z1xTa{6BN>NhR!+^Zt099XY<2}iRidj&V*OS#f;OE;pb2Qy~H{UhMc@(Kae zzdll?&qHTzf^9N~FThF0R$*RbIon$cpVCfmc93Gehm(GIWXQtJ8@x2ceg>$@?v7YH zZnMDCjChJZ0}W3zu6*A(0By|E(+{QSIY^@7+!1x&!flkZBzIhs3aGitgS~opMa{Ve1sd= z5Ep~@mN{Q~dkL-l4cnK~kZ7vuGDs?VG1$+WCK9@>nCZAn%j=b5%nN?H44b!j=7NRO z7IAaH^_d7ipzx+ENhbbRxKZP=C^HoAZo#4n=Ru4XHZ5E66cen7|>9NOQdh2nserKpjl+z~(UkO>#DKG*3}d?Y*BI?=5n2C@Sz@4xBF zvazCEb{GCdDdgRac^s9gAb<7~@q=*%fNE&0pmM>>a|N?nieV!mzCwZ(i`u3>XgcRW zGlSw0je+l~+siC$hQ+}r^aVJ%TkK(KglE$1aTRYQ!%J+0Hevf_)pSf%yvF=;J7VE3 zZ9Vrms5!DBHJOESdKrra)ngWO=}Ta$^Q@r07;*+Ql{jZ>U0yjZUG8Xjiav69;<$P> z{u7Pwm3LJ=ff8eTVVf@jeVNc-f);pg+vRCr5E|HKnf-j}6@L zMH(-8li4s+^qr#1tiyK6TvJFR4h4Ux8N$+UtlkFUV3+D;+;P`T-H_-OPrX|(fGuIM zfr4%^n~FGhFaI+~Sonv_r&J5u#qKQ#r6G|LoX(cTEPq!Y@VUqr8Tm8hV~b9QWk%pzq^dvv@EPLiDdeadI1$=$ZA(H9 zs;jJsB%$Zqy`0}FCI7Y9h!8MjBZ8*jy{`6GD9bX$8%`wCo`bY*X3jr`*GFX4Nld+B zH;tI^B#2LtBsgrped zgZIL*rSxay9Xd~vt-v@1?@U{&P^2htxB8NM1+FaCJm>ghn&$oRq7{=FAT<9v?r~eF z17TIqH6V=b0KU#eWk5i$MLg^ZIdZDgNPGMN1Y5E~)4gJaD>FC|gM$k>U2xa|@TF$8 zJx^vq@hni}lI99C#x=8!BUQ4z6BH3XrEAIhj*0h8aSFJ^b8{6fy`J0X77z}&QBlkX zWm{FoEW_PO>ycj7!{5A3m}HqTq8|&E+YpZf3!`+;3_BbPj#m6;sm&CM|5D*M+}>N7 zip5)(XJg$i{aN>>6v%Xw5~49P4w}bB~o|{r#6hU52O06 zAcvlOWvtz2CZDyCbxgKdt1FODgX6{M8ie~E*V~egVvy&J{X2dxU5E_5aq~&MxUaM6%8QlJ7U#Ic1;-qzQSo<|t zt9}9}#dkec>c2uEJ^X!Z-Mz~&tZ73xD6;Mj*T=v+Fk~FTjc5Pm{=HG*#nR#fPjKHu zCZt_|ff}0jlLqH~IzshNfT!28xJ#Sd_~8mjOWw8u)Km(_eBO&)umE!(=~x)=y?i&3 z%$Uv0#dp6F)GMHBal}hIWOs0$S47jD&gzz*Za<)hgr7bDUse{~anYB{euF1Lk6efo zaEG~r{SVUR5(=1K*&oE42Bz!KX`{=ohYa$L9--X)6G`zPUW;)yX#D_L=fNd#{Qhjo)8?M(Rd5Gg>;`RR=5R#KM;=XF}z;pbefXSwf@G1t6apOb+KA# zI$dy#5g{ys_QFt)(EsKIe#yo1J7+ArLrC*OHjsy)0`^EZbf+XiNo|zL`Q*VSy&-c23hrQ!)RyPQ2Kc z79Z{B3?ddqEj}0`xpIaOY?;{daG7GKvLi6uj<08y zW1U5u>oGov*yVDQ!%{ADsapVAG<$l|OZlaHGs*^Q;j)w^n zr}uGHVi|pU@Hi}<4F(2xyf!GLmSyzjT`+D!SQS#i)7l2hoFxE1Mel~0>ls6n5mr@? z1S;{6WZf_P#2fDygr7*WqL^4R<#D!GYk{ZLJFBG*( zogjJ`3DmqFUI-r+TdD*Shv~Pu$*eeC8 zv;>r=Xr~?^sjK}3kjNohbHImTg*(4woTSgB7bxrNbAP%!Qxm-y-yu$Tkxaw2C1J^^%dGwTG zf;hMmr*b?zUC%QEiq@6Uu}G|`#>yA2hLn-j;_YhaGD6-pbSBcPstJN&l(Snc5r9dw zjnivZa5}urc_J*8{@$Xu08klP{6BP^T(;slQ1=~^*UlRsL_7DKi<)5bMgHTxN;%Y) z?#2eOR2sXvc*p=>2{u%8(ex_@6mpP*8Vllfe-B&@nfwRi}Uy+MZ z(O#y(DqE%c)=&Su{uECYO~ExvLAvvII1n9rFHfRw-;PEvDpPTy??6p`=&?M2FA+1? zcP|$*)j!UT4C9M!g*Q@={FB~#S1yz6exsi}4C`g!@yN-2r3tGDx;`TEbaxJ1-9GOF zKGOaq;tgiEnlhDqItT9*>;hn0XgI@A$&)IQhv z9Ts-(s-ok|6Q&GX*(Y{20y|QI1=DyoZF3Y zDp`2Ej*jMh#euDG7Ba|XDmg^Lp)(QBt6AvX&V0sqa5;^DZ)}ja97oEJ!>>v@2MVwF zlx_g$Uj_qQ(($5*hZzSY;jqaJTYVv6#>p;FzRsYKlGj zKJ@VKQ35Vy$e^j<NK$D&ydXhW?(1yH~+x-YA(@Lcwaqw7k7L+m_o16#BV#^GrgvK*n^$69e}j zpo}j_;(nuRvn6X4k2!Ld?BWwLv1OUUbq=MSY5I57IJ>;|Gvw|SvdX!kne#4t%&v{r zlTZceOS#@4eM-J;lZ@w^k)HKhfZGOG(LwVETQ%Oe`P6OA^FfWLeYfD2g8F>^ndt|< zTI2MJHrnzyOqd?7X*_t1=48LHyTKM~LpNzWZ^SY2;7bg981V2V>WL0qaj6NdiLTIf z4j#T%OXU}!qEOBRbIfL3x2qQ1*rOSG8KY%h@WbU=s>NVOG2DkFCoBV4Rq(tDHq7V~ zso>3uC(~|XTa0U3;9vf4sd4cy(P~NYPQkBo8!a=4_(T-Mp=g^^GmX~_MYsL$3Q*~j z7z-A*nG;YfVZwvc&|9$xjag_{8b^N_iSKPps_^)GB`8m0{BDQqz$H=RB^CeZ9AK{N z<8fy*<*+cN)6xB$((^5AoaM<>GK*tW*mD6+dEQms#e=l;c7si5x)ilFYkp1rfm!_Iqeji`)PEP4W8)*A^L{?UuJnbGb#qMsi}l6$CD?OhdtNK2ud8vArUw@= zdH9kZtQ|pojFT9 z{IOp6nQ7p8BcNCMf(pKM8?EO+NogIRV)I%UH7CW_t;r-;v^jpOz=z8BL@Lg-IZweq zoc55B%55j<&~<2FIHkq?YsOT||4BJL^fdIuoLZ2;TfF5|lR+ng$#X0{4IJSj^X}6^ z%pN$GsKVRTUqw;WDpDEE)ykpI|F3d7_8Fe69gIZvYniSbHCyA0*`~YWeB`ek1dxRX zu1brTmx$TNAU#6>BI~7U7vj>H;?I@3Hmqns`^koKlrIboBP(zJG$l7+IccHqONW)6 z7*8ESA@g5pJTIEb@GhOAA@CoK@4&Dy{SR0gsv89=u#BHu)NGjU`9`7E(lo&Isl__3 z2m2B8De`=+FjRKoPJ3KWN{*2v)))&&nrtMR3>|<4NS#QnQV$s3xK5aFdvnbt5i9Js z@|sb@HMk24RR8EcNarRz>7s{$D(B`hXS;^CP&4q!EUaNT-Yb8J#-~>}QNk@D6dSs< zU^&{#357)pk4u|d7N+8ze25h}MM&xa4@H^A(6??zQl{U-gd+a1Uw7)i3%J-FdMnJb zD=v*UPhrrZYZ*tlVk&guI0$Z|a=MLV5X+=5t$&uS@1s#1P8LMja?@3%zZ?9#IMqCB#Q1LBD z%Q6rW@KiV#g}d=)uG4c1w1S~QZxjTtYn-z9Ty~Co;e#|Do*AAg;Id$g&qjs%YrMfD z}}UltIhc3NJ?@^WjZJuq?uUuZAX}>n6ZE{X>;+(P$~Ek?#d^ z4`MS3(&iZdoC2JJU4p204rY|A_~Uim$=K>f&SV}b3*Sg^vO4i@Rtz7!^VE4n0RQp- z0^cqwJwTfloAY}8b8i;~hWor{!Qv?ZrGg~6tjtu;og%Bb;Lu6Fc2 zs2;o!pEkjE3SGoBjp1og4BBzDK+FGQjZZGe*3P3<$dfI_PaWVnsGv13vODE;V7}z1 zYa_m`D3rEf^(ErF1)~;4Z{y(~%a*!d(ZP=etqS@Yd24#!L6*#cyF5f~yp^M0_E&Xv zn;n^q!bk=h)Udd4ol={>ss^y|85%E}*qo)9h9=?Wk3kfeL6^LQ@uM0hgh%dP%q>)k zOg&HAkzN&gAxDjI?;Nfpr{2biuNvWb08qRnllO={_q)8@Q~n5!HJy#}FvH~e1NM=? zR@HOQBVbwfO7;q8W0WfuKk)hreGRk?_yziya$>ZJIeKKg>(|o|6{dilnvJm+HD)u|Hv^s5r~d7M}B!3mma4ec7SV z^|}i2(!#IgVP5kZFneOO31L;}i@cOa5VPhH{=ziAE7&=eYBu>v8 z99nOTHAi&3OB_oaM^t&kIj{(^e&b1hPm;4K!yjXC5EHMQdvcntxs}K zUp6$J&;wI_0h_{J% zH2x&;)2AJGNxw(DPqbc_WM)5h8^b!_&C=Pc^S_YN6G z6UR%=O;mhV4huPx_%U%4Q5C1WR-B8Z|3&mNBYY$?@JrE*iciOyKWnDtaKPZ^kM|hl zG_>>iCzC6Om`gl7ec5v);u3;nSfW_ApIusHqlAtROzZ|{08YkL>DpG!s4bi zs%sVt@yVcbc+-!63Qatfc!oGwd_B`J7R=`L+}!R-XhN#nn8o2@Ht_@E#p&~h#yK~N zvl9jy_A$O+Xqr(p_Bgu4NnW2Ko+h3lDvfJvCH$TAFKM}AEa4fV;;#dKd)zxq`p?AX zjKCYS)lVYN!zDb$DJ?!yQb9;@Z=F0nnax!FdNd6h!!tEn`aW;%@z z*ZitXnNFsfx)H)jjMRz6%>|jmQC13+>zMPLEu`(Eb9H7yYirah(-SC=-PPY?{1>?e z%1>b={~?MvOvHz!&0m(}96|SG6ogB3A?`%Rw$}V+JmtI>DJ>D1Ao=L(Lfw@}uA}Qc zn5}Y%HLP0IjIUsuzXO)Lpe8-vlk@}nN%RjXo(&}I!ABqz!Tkj5xK(a)GTkncJj;vR z?htGJNYPx*^D8Sd4Bv->sQB1US}pgWj0qk|{SAdte&zXwd2I@l&LF1v=2Pqen))?y zo<=X$Vm9e>h;xY_5LGds)2d(|=@sc@m58Y_a3}2TeuAx}HK(&mnV(oUN5iEhno5GI zINwS#enadp`{7ZE?m1NOL%G&h`H5so8?jti@1)qzGh7s?` z{!n!1o)BIa^U7SzlTv&qnD8WVAih=&MSL&Pgw~l;hvQXMZE>dV-5J%Jct9GfA;H~; z*ZriRcbZYb^D?h{;oZ_#P;cd5(=6Gak^AZ0xI<(9ew2be%94#4-&AsDK$!0$4OK{t zmF}q7ME7FW{})9*w1}k7i%1Jd`nZVnDM_Cdkv<`5p)9}bMFIYx4p;H9hpWo*ut^x` zUjAXEfmewkGC>A>i&o^BNM}v-$XqkcTUw!qGtD%I>@w5rLs>)JXPM?gU;uJ|MA8OX zzEizdwZ`3iBgz-eS}#fLAp^^xR*lJS8FfuH^S6Z zw>!Rhq~pEJ_D?3x$K4>0%M77D)|?Ncftw`8zLZXt)*JKd@cw=Wy4X zZSF0--^(Y{b~0_@HReiKcvf%^V4^`*BFRSOM0s8&_0=eua2=)#pQshBKUwo<6&lgG z%WfMX?w82l%ZlPG!*iGo^rr}k|66sa-!u1p%zZ1bck+50uXpkKTV7)vG_@_@^$zpe z9Fjbbvafm!uSvN9T}#Y>uC~>iGO}6G80PV@IY#57Zb*>g9>BVKot;2?UQfXSM@fvG ztvSMc7WLyONyRKWzRVm%+T(Y1impw3xM;TOX-lb3p=&|HlzREB C+oj8lAD*HpN zEJFGMy{hW{MMt?u+|73OJXKIq=4nzUlAddI_pZJ|jvzShl8QhEqTUu?hvPBdWYVfdIRY*i3pz zl=Oyx_%aJM=^I$Ojl_Ib05moFN6Qu@C|lw)B(NRibuRqyiHJAR9{dF?@fPBD#5^gO zswUnp@`n`-Y<@3kC5d)fgRBwO?JCHRp2-DfSs4Cxg zth-&r-IDWo0ebptwI}(*0 z*K7Pwk^VIC8Db~yl}%thO(aew&agHyS_HE@3?UXHh7whN82%uvIkUYT_EpB8~lwk0+KQmL@7C3LwvJuVqM2ASRl#HFNXO zlTQN_pe_HSn&P)eXt%7bjNC@-BG(SWtNmZlDocni@dy)Psu{ZHGjsFn%ZMW`2dS7BcjSr%wI<`iA#^Gpr-^f4<`%%m4 zC|QpYj}uQ=b!FTLmp@gatRq^YJDiBzCEg?6C#v!s)%YKf{*N8cT5=K<|1t2}t<4}k zhn+VL=JXuG>r5MJRQflHa`tXAxnWO{=5B(W7yDfP3)-;WG zt~;i(X8+kj$ec%Uuj>-)5i^NOpWmU6?Q{>5UZ2>HU1fhMz|fBG>d|Gq#6gUI-Rf#GXK*SvlQ@f* zOH}#&p;g9g(&t3VuwogQLA3o!O+4yTd;?-b;_udw*CK+~k;EvXkEpWw6WQ3F9!>h6 z976E4tNFXmNyS+f_vdKI#12J#tLvJM=NWl{_$%=uQAxtb2m%gMNctt>Z)`TpSa-{b zE3CUbSuaw#9>ktR>#H@EqPH}CUS|9 qW#i7LfyjlVbPuMztY`&t-p=5PlIY-{x+ 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 ConnectionsRouter\n\nimport os\nimport sys\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\nfrom sqlalchemy import create_engine, inspect\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\n\n# Force SQLite in-memory for database module imports.\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n\n\n# [DEF:test_list_connections_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure listing connections auto-creates missing table and returns empty payload.\ndef test_list_connections_bootstraps_missing_table(db_session):\n from src.api.routes.connections import list_connections\n\n result = asyncio.run(list_connections(db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert result == []\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_list_connections_bootstraps_missing_table:Function]\n\n\n# [DEF:test_create_connection_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure connection creation bootstraps table and persists returned connection fields.\ndef test_create_connection_bootstraps_missing_table(db_session):\n from src.api.routes.connections import ConnectionCreate, create_connection\n\n payload = ConnectionCreate(\n name=\"Analytics Warehouse\",\n type=\"postgres\",\n host=\"warehouse.internal\",\n port=5432,\n database=\"analytics\",\n username=\"reporter\",\n password=\"secret\",\n )\n\n created = asyncio.run(create_connection(connection=payload, db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert created.name == \"Analytics Warehouse\"\n assert created.host == \"warehouse.internal\"\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_create_connection_bootstraps_missing_table:Function]\n# [/DEF:ConnectionsRoutesTests:Module]\n" }, @@ -2477,7 +2509,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Unit tests for dashboards API endpoints." }, @@ -2489,22 +2521,7 @@ "target_ref": "[DashboardsApi]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:DashboardsApiTests:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for dashboards API endpoints.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardsApi]\n\nimport pytest\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime, timezone\nfrom fastapi.testclient import TestClient\nfrom src.app import app\nfrom src.api.routes.dashboards import DashboardsResponse\nfrom src.dependencies import (\n get_current_user,\n has_permission,\n get_config_manager,\n get_task_manager,\n get_resource_service,\n get_mapping_service,\n)\nfrom src.core.database import get_db\nfrom src.services.profile_service import ProfileService as DomainProfileService\n\n# Global mock user for get_current_user dependency overrides\nmock_user = MagicMock()\nmock_user.id = \"u-1\"\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n config_manager = MagicMock()\n task_manager = MagicMock()\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n db = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_db] = lambda: db\n\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n \"db\": db,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns a populated response that satisfies the schema contract.\n# @TEST: GET /api/dashboards returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DashboardsResponse schema\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod\")\n\n assert response.status_code == 200\n data = response.json()\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert \"page\" in data\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing applies the search filter and returns only matching rows.\n# @TEST: GET /api/dashboards filters by search term\n# @PRE: search parameter provided\n# @POST: Only matching dashboards returned\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n async def mock_get_dashboards(env, tasks, include_git_status=False):\n return [\n {\n \"id\": 1,\n \"title\": \"Sales Report\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing Dashboard\",\n \"slug\": \"marketing\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n ]\n\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=mock_get_dashboards\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n\n assert response.status_code == 200\n data = response.json()\n # @POST: Filtered result count must match search\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n\n\n# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns an empty payload for an environment without dashboards.\n# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n\n\n# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n\n\n# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/dashboards returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/dashboards returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Invalid page\n response = client.get(\"/api/dashboards?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n # Invalid page_size\n response = client.get(\"/api/dashboards?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n\n\n# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns charts and datasets for an existing dashboard.\n# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_client.get_dashboard_detail.return_value = {\n \"id\": 42,\n \"title\": \"Revenue Dashboard\",\n \"slug\": \"revenue-dashboard\",\n \"url\": \"/superset/dashboard/42/\",\n \"description\": \"Overview\",\n \"last_modified\": \"2026-02-20T10:00:00+00:00\",\n \"published\": True,\n \"charts\": [\n {\n \"id\": 100,\n \"title\": \"Revenue by Month\",\n \"viz_type\": \"line\",\n \"dataset_id\": 7,\n \"last_modified\": \"2026-02-19T10:00:00+00:00\",\n \"overview\": \"line\",\n }\n ],\n \"datasets\": [\n {\n \"id\": 7,\n \"table_name\": \"fact_revenue\",\n \"schema\": \"mart\",\n \"database\": \"Analytics\",\n \"last_modified\": \"2026-02-18T10:00:00+00:00\",\n \"overview\": \"mart.fact_revenue\",\n }\n ],\n \"chart_count\": 1,\n \"dataset_count\": 1,\n }\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"id\"] == 42\n assert payload[\"chart_count\"] == 1\n assert payload[\"dataset_count\"] == 1\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n\n\n# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns 404 when the requested environment is missing.\n# @TEST: GET /api/dashboards/{id} returns 404 for missing environment\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n\n\n# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate creates migration task\n# @PRE: Valid source_env_id, target_env_id, dashboard_ids\n# @PURPOSE: Validate dashboard migration request creates an async task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_migrate_dashboards_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"source\"\n mock_target = MagicMock()\n mock_target.id = \"target\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_task = MagicMock()\n mock_task.id = \"task-migrate-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [1, 2, 3],\n \"db_mappings\": {\"old_db\": \"new_db\"},\n },\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n\n\n# [DEF:test_migrate_dashboards_no_ids:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids\n# @PRE: dashboard_ids is empty\n# @PURPOSE: Validate dashboard migration rejects empty dashboard identifier lists.\n# @POST: Returns 400 error\ndef test_migrate_dashboards_no_ids(mock_deps):\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [],\n },\n )\n\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_no_ids:Function]\n\n\n# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate migration creation returns 404 when the source environment cannot be resolved.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/backup creates backup task\n# @PRE: Valid env_id, dashboard_ids\n# @PURPOSE: Validate dashboard backup request creates an async backup task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_task = MagicMock()\n mock_task.id = \"task-backup-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1, 2, 3], \"schedule\": \"0 0 * * *\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n\n\n# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate backup task creation returns 404 when the target environment is missing.\n# @PRE: env_id is a valid environment ID\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions\n# @PRE: Valid source_env_id, target_env_id\n# @PURPOSE: Validate database mapping suggestions are returned for valid source and target environments.\n# @POST: Returns list of database mappings\ndef test_get_database_mappings_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"prod\"\n mock_target = MagicMock()\n mock_target.id = \"staging\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[\n {\n \"source_db\": \"old_sales\",\n \"target_db\": \"new_sales\",\n \"source_db_uuid\": \"uuid-1\",\n \"target_db_uuid\": \"uuid-2\",\n \"confidence\": 0.95,\n }\n ]\n )\n\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=prod&target_env_id=staging\"\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"mappings\" in data\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.95\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n\n\n# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate database mapping suggestions return 404 when either environment is missing.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_get_database_mappings_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_filters_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard task history returns only related backup and LLM tasks.\n# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard\ndef test_get_dashboard_tasks_history_filters_success(mock_deps):\n now = datetime.now(timezone.utc)\n\n llm_task = MagicMock()\n llm_task.id = \"task-llm-1\"\n llm_task.plugin_id = \"llm_dashboard_validation\"\n llm_task.status = \"SUCCESS\"\n llm_task.started_at = now\n llm_task.finished_at = now\n llm_task.params = {\"dashboard_id\": \"42\", \"environment_id\": \"prod\"}\n llm_task.result = {\"summary\": \"LLM validation complete\"}\n\n backup_task = MagicMock()\n backup_task.id = \"task-backup-1\"\n backup_task.plugin_id = \"superset-backup\"\n backup_task.status = \"RUNNING\"\n backup_task.started_at = now\n backup_task.finished_at = None\n backup_task.params = {\"env\": \"prod\", \"dashboards\": [42]}\n backup_task.result = {}\n\n other_task = MagicMock()\n other_task.id = \"task-other\"\n other_task.plugin_id = \"superset-backup\"\n other_task.status = \"SUCCESS\"\n other_task.started_at = now\n other_task.finished_at = now\n other_task.params = {\"env\": \"prod\", \"dashboards\": [777]}\n other_task.result = {}\n\n mock_deps[\"task\"].get_all_tasks.return_value = [other_task, llm_task, backup_task]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod&limit=10\")\n\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 2\n assert {item[\"plugin_id\"] for item in data[\"items\"]} == {\n \"llm_dashboard_validation\",\n \"superset-backup\",\n }\n\n\n# [/DEF:test_get_dashboard_tasks_history_filters_success:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset.\n# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_response = MagicMock()\n mock_response.status_code = 200\n mock_response.content = b\"fake-image-bytes\"\n mock_response.headers = {\"Content-Type\": \"image/png\"}\n\n def _network_request(method, endpoint, **kwargs):\n if method == \"POST\":\n return {\"image_url\": \"/api/v1/dashboard/42/screenshot/abc123/\"}\n return mock_response\n\n mock_client.network.request.side_effect = _network_request\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n\n assert response.status_code == 200\n assert response.content == b\"fake-image-bytes\"\n assert response.headers[\"content-type\"].startswith(\"image/png\")\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n\n\n# [DEF:_build_profile_preference_stub:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Creates profile preference payload stub for dashboards filter contract tests.\n# @PRE: username can be empty; enabled indicates profile-default toggle state.\n# @POST: Returns object compatible with ProfileService.get_my_preference contract.\ndef _build_profile_preference_stub(username: str, enabled: bool):\n preference = MagicMock()\n preference.superset_username = username\n preference.superset_username_normalized = (\n str(username or \"\").strip().lower() or None\n )\n preference.show_only_my_dashboards = bool(enabled)\n\n payload = MagicMock()\n payload.preference = preference\n return payload\n\n\n# [/DEF:_build_profile_preference_stub:Function]\n\n\n# [DEF:_matches_actor_case_insensitive:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.\n# @PRE: owners can be None or list-like values.\n# @POST: Returns True when bound username matches any owner or modified_by.\ndef _matches_actor_case_insensitive(bound_username, owners, modified_by):\n normalized_bound = str(bound_username or \"\").strip().lower()\n if not normalized_bound:\n return False\n\n owner_tokens = []\n for owner in owners or []:\n token = str(owner or \"\").strip().lower()\n if token:\n owner_tokens.append(token)\n\n modified_token = str(modified_by or \"\").strip().lower()\n return normalized_bound in owner_tokens or bool(\n modified_token and modified_token == normalized_bound\n )\n\n\n# [/DEF:_matches_actor_case_insensitive:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.\n# @PURPOSE: Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.\n# @PRE: Current user has enabled profile-default preference and bound username.\n# @POST: Response includes only matching dashboards and effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Owner Match\",\n \"slug\": \"owner-match\",\n \"owners\": [\" John_Doe \"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 2,\n \"title\": \"Modifier Match\",\n \"slug\": \"modifier-match\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \" JOHN_DOE \",\n },\n {\n \"id\": 3,\n \"title\": \"No Match\",\n \"slug\": \"no-match\",\n \"owners\": [\"another-user\"],\n \"modified_by\": \"nobody\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\" JOHN_DOE \",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n\n\n# [DEF:test_get_dashboards_override_show_all_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.\n# @PURPOSE: Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.\n# @PRE: Profile-default preference exists but override_show_all=true query is provided.\n# @POST: Response remains unfiltered and effective_profile_filter.applied is false.\ndef test_get_dashboards_override_show_all_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true&override_show_all=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is True\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_override_show_all_contract:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.\n# @PURPOSE: Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.\n# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.\n# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 101,\n \"title\": \"Team Dashboard\",\n \"slug\": \"team-dashboard\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 102,\n \"title\": \"Ops Dashboard\",\n \"slug\": \"ops-dashboard\",\n \"owners\": [\"ops-user\"],\n \"modified_by\": \"ops-user\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 0\n assert payload[\"dashboards\"] == []\n assert payload[\"page\"] == 1\n assert payload[\"page_size\"] == 10\n assert payload[\"total_pages\"] == 1\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n\n\n# [DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.\n# @PURPOSE: Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.\n# @PRE: Profile-default preference exists but page_context=other query is provided.\n# @POST: Response remains unfiltered and metadata reflects source_page=other.\ndef test_get_dashboards_page_context_other_disables_profile_default(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=other&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"other\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.\n# @PURPOSE: Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.\n# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.\n# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.\ndef test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(\n mock_deps,\n):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 5,\n \"title\": \"Alias Match\",\n \"slug\": \"alias-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Superset Admin\",\n },\n {\n \"id\": 6,\n \"title\": \"Alias No Match\",\n \"slug\": \"alias-no-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Other User\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\"src.api.routes.dashboards._projection.SupersetClient\") as superset_client_cls,\n patch(\n \"src.api.routes.dashboards._projection.SupersetAccountLookupAdapter\"\n ) as lookup_adapter_cls,\n ):\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"admin\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n superset_client = MagicMock()\n superset_client_cls.return_value = superset_client\n\n lookup_adapter = MagicMock()\n lookup_adapter.get_users_page.return_value = {\n \"items\": [\n {\n \"environment_id\": \"prod\",\n \"username\": \"admin\",\n \"display_name\": \"Superset Admin\",\n \"email\": \"admin@example.com\",\n \"is_active\": True,\n }\n ],\n \"total\": 1,\n }\n lookup_adapter_cls.return_value = lookup_adapter\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {5}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n lookup_adapter.get_users_page.assert_called_once()\n superset_client.get_dashboard.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.\n# @PURPOSE: Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.\n# @PRE: Profile-default preference is enabled and owners list contains dict payloads.\n# @POST: Response keeps dashboards where owner object resolves to bound username alias.\ndef test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 701,\n \"title\": \"Featured Charts\",\n \"slug\": \"featured-charts\",\n \"owners\": [\n {\n \"id\": 11,\n \"first_name\": \"user\",\n \"last_name\": \"1\",\n \"username\": None,\n \"email\": \"user_1@example.local\",\n }\n ],\n \"modified_by\": \"another_user\",\n },\n {\n \"id\": 702,\n \"title\": \"Other Dashboard\",\n \"slug\": \"other-dashboard\",\n \"owners\": [\n {\n \"id\": 12,\n \"first_name\": \"other\",\n \"last_name\": \"user\",\n \"username\": None,\n \"email\": \"other@example.local\",\n }\n ],\n \"modified_by\": \"other_user\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\n \"src.api.routes.dashboards._projection._resolve_profile_actor_aliases\",\n return_value=[\"user_1\"],\n ),\n ):\n profile_service = MagicMock(spec=DomainProfileService)\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"user_1\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n lambda bound_username, owners, modified_by: any(\n str(owner.get(\"email\", \"\")).split(\"@\", 1)[0].strip().lower()\n == str(bound_username).strip().lower()\n for owner in (owners or [])\n if isinstance(owner, dict)\n )\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {701}\n assert payload[\"dashboards\"][0][\"title\"] == \"Featured Charts\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n\n\n# [/DEF:DashboardsApiTests:Module]\n" }, @@ -2926,7 +2943,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Returns task_id and create_task was called", + "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", "PRE": "Valid source_env_id, target_env_id, dashboard_ids", "PURPOSE": "Validate dashboard migration request creates an async task and returns its identifier.", "TEST": "POST /api/dashboards/migrate creates migration task" @@ -3089,7 +3106,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Returns task_id and create_task was called", + "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", "PRE": "Valid env_id, dashboard_ids", "PURPOSE": "Validate dashboard backup request creates an async backup task and returns its identifier.", "TEST": "POST /api/dashboards/backup creates backup task" @@ -3406,6 +3423,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -3459,6 +3485,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -3841,7 +3876,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts.", "SEMANTICS": [ @@ -3868,20 +3903,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -3894,6 +3915,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -3911,6 +3933,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -3938,15 +3961,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -3978,15 +3992,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4018,15 +4023,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4058,15 +4054,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4098,15 +4085,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4138,15 +4116,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4178,15 +4147,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -4918,7 +4878,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Endpoint contracts remain stable for success and validation failure paths.", "LAYER": "API", "PURPOSE": "Unit tests for datasets API endpoints.", @@ -4948,20 +4908,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -5560,7 +5506,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Supports only the SQLAlchemy-like operations exercised by this test module.", "PURPOSE": "In-memory session double for git route tests with minimal query/filter persistence semantics." }, @@ -5736,7 +5682,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain (Tests)", "PURPOSE": "Validate status endpoint behavior for missing and error repository states.", "SEMANTICS": [ @@ -5755,22 +5701,7 @@ "target_ref": "[GitApi]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestGitStatusRoute:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, git, api, status, no_repo\n# @PURPOSE: Validate status endpoint behavior for missing and error repository states.\n# @LAYER: Domain (Tests)\n# @RELATION: VERIFIES -> [GitApi]\n\nfrom fastapi import HTTPException\nimport pytest\nimport asyncio\nfrom unittest.mock import MagicMock\n\nfrom src.api.routes import git as git_routes\n\n\n# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error.\n# @PRE: GitService.get_status raises HTTPException(404).\n# @POST: Route returns a deterministic NO_REPO status payload.\ndef test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):\n class MissingRepoGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/missing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise AssertionError(\"get_status must not be called when repository path is missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", MissingRepoGitService())\n\n response = asyncio.run(git_routes.get_repository_status(34))\n\n assert response[\"sync_status\"] == \"NO_REPO\"\n assert response[\"sync_state\"] == \"NO_REPO\"\n assert response[\"has_repo\"] is False\n assert response[\"current_branch\"] is None\n# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n\n\n# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked.\n# @PRE: GitService.get_status raises HTTPException with non-404 status.\n# @POST: Raised exception preserves original status and detail.\ndef test_get_repository_status_propagates_non_404_http_exception(monkeypatch):\n class ConflictGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/existing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise HTTPException(status_code=409, detail=\"Conflict\")\n\n monkeypatch.setattr(git_routes, \"git_service\", ConflictGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_status(34))\n\n assert exc_info.value.status_code == 409\n assert exc_info.value.detail == \"Conflict\"\n# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n\n\n# [DEF:test_get_repository_diff_propagates_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService.\n# @PRE: GitService.get_diff raises HTTPException.\n# @POST: Endpoint raises same HTTPException values.\ndef test_get_repository_diff_propagates_http_exception(monkeypatch):\n class DiffGitService:\n def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:\n raise HTTPException(status_code=404, detail=\"Repository missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", DiffGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_diff(12))\n\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Repository missing\"\n# [/DEF:test_get_repository_diff_propagates_http_exception:Function]\n\n\n# [DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.\n# @PRE: GitService.get_commit_history raises ValueError.\n# @POST: Endpoint returns HTTPException with status 500 and route context.\ndef test_get_history_wraps_unexpected_error_as_500(monkeypatch):\n class HistoryGitService:\n def get_commit_history(self, dashboard_id: int, limit: int = 50):\n raise ValueError(\"broken parser\")\n\n monkeypatch.setattr(git_routes, \"git_service\", HistoryGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_history(12))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"get_history failed: broken parser\"\n# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n\n\n# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400.\n# @PRE: GitService.commit_changes raises RuntimeError.\n# @POST: Endpoint raises HTTPException(500) with route context.\ndef test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):\n class CommitGitService:\n def commit_changes(self, dashboard_id: int, message: str, files):\n raise RuntimeError(\"index lock\")\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n monkeypatch.setattr(git_routes, \"git_service\", CommitGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.commit_changes(12, CommitPayload()))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"commit_changes failed: index lock\"\n# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n\n\n# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response.\n# @PRE: Some repositories are missing and some are initialized.\n# @POST: Returned map includes resolved status for each requested dashboard ID.\ndef test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):\n class BatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n if dashboard_id == 2:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n raise HTTPException(status_code=404, detail=\"not found\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda path: path.endswith(\"/repo-2\"))\n\n class BatchRequest:\n dashboard_ids = [1, 2]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"1\"][\"sync_status\"] == \"NO_REPO\"\n assert response.statuses[\"2\"][\"sync_state\"] == \"SYNCED\"\n# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n\n\n# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request.\n# @PRE: GitService raises non-HTTP exception for one dashboard.\n# @POST: Failed dashboard status is marked as ERROR.\ndef test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):\n class BatchErrorGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise RuntimeError(\"boom\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchErrorGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [9]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"9\"][\"sync_status\"] == \"ERROR\"\n assert response.statuses[\"9\"][\"sync_state\"] == \"ERROR\"\n# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n\n\n# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint protects server from oversized payloads.\n# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.\n# @POST: Result contains unique IDs up to configured cap.\ndef test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):\n class SafeBatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", SafeBatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [1, 1] + list(range(2, 90))\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH\n assert \"1\" in response.statuses\n# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n\n\n# [DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit route configures repository identity from profile preferences before commit call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and commit proceeds.\ndef test_commit_changes_applies_profile_identity_before_commit(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.commit_payload = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def commit_changes(self, dashboard_id: int, message: str, files):\n self.commit_payload = (dashboard_id, message, files)\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.commit_changes(\n \"dashboard-12\",\n CommitPayload(),\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.commit_payload == (12, \"test\", [\"dashboards/a.yaml\"])\n# [/DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n\n\n# [DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure pull route configures repository identity from profile preferences before pull call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and pull proceeds.\ndef test_pull_changes_applies_profile_identity_before_pull(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.pulled_dashboard_id = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def pull_changes(self, dashboard_id: int):\n self.pulled_dashboard_id = dashboard_id\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.pull_changes(\n \"dashboard-12\",\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.pulled_dashboard_id == 12\n# [/DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n\n\n# [DEF:test_get_merge_status_returns_service_payload:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge status route returns service payload as-is.\n# @PRE: git_service.get_merge_status returns unfinished merge payload.\n# @POST: Route response contains has_unfinished_merge=True.\ndef test_get_merge_status_returns_service_payload(monkeypatch):\n class MergeStatusGitService:\n def get_merge_status(self, dashboard_id: int) -> dict:\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": \"/tmp/repo-12\",\n \"git_dir\": \"/tmp/repo-12/.git\",\n \"current_branch\": \"dev\",\n \"merge_head\": \"abc\",\n \"merge_message_preview\": \"merge msg\",\n \"conflicts_count\": 2,\n }\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeStatusGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.get_merge_status(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"has_unfinished_merge\"] is True\n assert response[\"conflicts_count\"] == 2\n# [/DEF:test_get_merge_status_returns_service_payload:Function]\n\n\n# [DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge resolve route forwards parsed resolutions to service.\n# @PRE: resolve_data has one file strategy.\n# @POST: Service receives normalized list and route returns resolved files.\ndef test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):\n captured = {}\n\n class MergeResolveGitService:\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions):\n captured[\"dashboard_id\"] = dashboard_id\n captured[\"resolutions\"] = resolutions\n return [\"dashboards/a.yaml\"]\n\n class ResolveData:\n class _Resolution:\n def dict(self):\n return {\"file_path\": \"dashboards/a.yaml\", \"resolution\": \"mine\", \"content\": None}\n\n resolutions = [_Resolution()]\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeResolveGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.resolve_merge_conflicts(\n \"dashboard-12\",\n ResolveData(),\n config_manager=MagicMock(),\n )\n )\n\n assert captured[\"dashboard_id\"] == 12\n assert captured[\"resolutions\"][0][\"resolution\"] == \"mine\"\n assert response[\"resolved_files\"] == [\"dashboards/a.yaml\"]\n# [/DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n\n\n# [DEF:test_abort_merge_calls_service_and_returns_result:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure abort route delegates to service.\n# @PRE: Service abort_merge returns aborted status.\n# @POST: Route returns aborted status.\ndef test_abort_merge_calls_service_and_returns_result(monkeypatch):\n class AbortGitService:\n def abort_merge(self, dashboard_id: int):\n assert dashboard_id == 12\n return {\"status\": \"aborted\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", AbortGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.abort_merge(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"aborted\"\n# [/DEF:test_abort_merge_calls_service_and_returns_result:Function]\n\n\n# [DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure continue route passes commit message to service.\n# @PRE: continue_data.message is provided.\n# @POST: Route returns committed status and hash.\ndef test_continue_merge_passes_message_and_returns_commit(monkeypatch):\n class ContinueGitService:\n def continue_merge(self, dashboard_id: int, message: str):\n assert dashboard_id == 12\n assert message == \"Resolve all conflicts\"\n return {\"status\": \"committed\", \"commit_hash\": \"abc123\"}\n\n class ContinueData:\n message = \"Resolve all conflicts\"\n\n monkeypatch.setattr(git_routes, \"git_service\", ContinueGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.continue_merge(\n \"dashboard-12\",\n ContinueData(),\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"committed\"\n assert response[\"commit_hash\"] == \"abc123\"\n# [/DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n\n\n# [/DEF:TestGitStatusRoute:Module]\n" }, @@ -6525,7 +6456,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Unit tests for migration API route handlers." }, @@ -6537,22 +6468,7 @@ "target_ref": "backend.src.api.routes.migration" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestMigrationRoutes:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for migration API route handlers.\n# @LAYER: API\n# @RELATION: VERIFIES -> backend.src.api.routes.migration\n#\nimport pytest\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, AsyncMock, patch\nfrom datetime import datetime, timezone\n\n# Add backend directory to sys.path\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nimport os\n\n# Force SQLite in-memory for all database connections BEFORE importing any application code\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\n\n\nfrom fastapi import HTTPException\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\n\n# Patch the get_db dependency if `src.api.routes.migration` imports it\nfrom unittest.mock import patch\n\npatch(\"src.core.database.get_db\").start()\n\n# --- Fixtures ---\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"In-memory SQLite session for testing.\"\"\"\n from sqlalchemy.pool import StaticPool\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n# [DEF:_make_config_manager:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _make_config_manager(cron=\"0 2 * * *\"):\n \"\"\"Creates a mock config manager with a realistic AppConfig-like object.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = cron\n config = MagicMock()\n config.settings = settings\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.save_config = MagicMock()\n return cm\n\n\n# --- get_migration_settings tests ---\n\n# [/DEF:_make_config_manager:Function]\n\n\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_default_cron():\n \"\"\"Verify the settings endpoint returns the stored cron string.\"\"\"\n from src.api.routes.migration import get_migration_settings\n\n cm = _make_config_manager(cron=\"0 3 * * *\")\n\n # Call the handler directly, bypassing Depends\n result = await get_migration_settings(config_manager=cm, _=None)\n\n assert result == {\"cron\": \"0 3 * * *\"}\n cm.get_config.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_fallback_when_no_cron():\n \"\"\"When migration_sync_cron uses the default, should return '0 2 * * *'.\"\"\"\n from src.api.routes.migration import get_migration_settings\n\n # Use the default cron value (simulating a fresh config)\n cm = _make_config_manager()\n\n result = await get_migration_settings(config_manager=cm, _=None)\n\n assert result == {\"cron\": \"0 2 * * *\"}\n\n\n# --- update_migration_settings tests ---\n\n\n@pytest.mark.asyncio\nasync def test_update_migration_settings_saves_cron():\n \"\"\"Verify that a valid cron update saves to config.\"\"\"\n from src.api.routes.migration import update_migration_settings\n\n cm = _make_config_manager()\n\n result = await update_migration_settings(\n payload={\"cron\": \"0 4 * * *\"}, config_manager=cm, _=None\n )\n\n assert result[\"cron\"] == \"0 4 * * *\"\n assert result[\"status\"] == \"updated\"\n cm.save_config.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_update_migration_settings_rejects_missing_cron():\n \"\"\"Verify 400 error when 'cron' key is missing from payload.\"\"\"\n from src.api.routes.migration import update_migration_settings\n\n cm = _make_config_manager()\n\n with pytest.raises(HTTPException) as exc_info:\n await update_migration_settings(\n payload={\"interval\": \"daily\"}, config_manager=cm, _=None\n )\n\n assert exc_info.value.status_code == 400\n assert \"cron\" in exc_info.value.detail.lower()\n\n\n# --- get_resource_mappings tests ---\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_returns_formatted_list(db_session):\n \"\"\"Verify mappings are returned as formatted dicts with correct keys.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n # Populate test data\n m1 = ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"uuid-1\",\n remote_integer_id=\"42\",\n resource_name=\"Sales Chart\",\n last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),\n )\n db_session.add(m1)\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n\n assert result[\"total\"] == 1\n assert len(result[\"items\"]) == 1\n assert result[\"items\"][0][\"environment_id\"] == \"prod\"\n assert result[\"items\"][0][\"resource_type\"] == \"chart\"\n assert result[\"items\"][0][\"uuid\"] == \"uuid-1\"\n assert result[\"items\"][0][\"remote_id\"] == \"42\"\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n assert result[\"items\"][0][\"last_synced_at\"] is not None\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_respects_pagination(db_session):\n \"\"\"Verify skip and limit parameters work correctly.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n for i in range(5):\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=f\"uuid-{i}\",\n remote_integer_id=str(i),\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=2,\n limit=2,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n\n assert result[\"total\"] == 5\n assert len(result[\"items\"]) == 2\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_search_by_name(db_session):\n \"\"\"Verify search filters by resource_name.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Sales Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Revenue Dashboard\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=\"sales\",\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_env(db_session):\n \"\"\"Verify env_id filter returns only matching environment.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"ss1\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Chart A\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"ss2\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Chart B\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=\"ss2\",\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"environment_id\"] == \"ss2\"\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_type(db_session):\n \"\"\"Verify resource_type filter returns only matching type.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"My Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"My Dataset\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=\"dataset\",\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_type\"] == \"dataset\"\n\n\n# --- trigger_sync_now tests ---\n\n\n@pytest.fixture\n# [DEF:_mock_env:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n\n\n# [/DEF:_mock_env:Function]\n\n\n# [DEF:_make_sync_config_manager:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n\n\n# [/DEF:_make_sync_config_manager:Function]\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):\n \"\"\"Verify that trigger_sync_now creates an Environment row in DB before syncing,\n preventing FK constraint violations on resource_mappings inserts.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n\n cm = _make_sync_config_manager([_mock_env])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_client_instance = MagicMock()\n MockClient.return_value = mock_client_instance\n mock_service_instance = MagicMock()\n MockService.return_value = mock_service_instance\n\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n # Environment row must exist in DB\n env_row = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").first()\n assert env_row is not None\n assert env_row.name == \"Test Env\"\n assert env_row.url == \"http://superset.test\"\n\n # Sync must have been called\n mock_service_instance.sync_environment.assert_called_once_with(\n \"test-env-1\", mock_client_instance\n )\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 0\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_rejects_empty_environments(db_session):\n \"\"\"Verify 400 error when no environments are configured.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n\n cm = _make_sync_config_manager([])\n\n with pytest.raises(HTTPException) as exc_info:\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n assert exc_info.value.status_code == 400\n assert \"No environments\" in exc_info.value.detail\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):\n \"\"\"Verify that if sync_environment raises for one env, it's captured in failed list.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n\n env2 = MagicMock()\n env2.id = \"test-env-2\"\n env2.name = \"Failing Env\"\n env2.url = \"http://fail.test\"\n env2.username = \"admin\"\n env2.password = \"admin\"\n env2.verify_ssl = False\n env2.timeout = 30\n\n cm = _make_sync_config_manager([_mock_env, env2])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_service_instance = MagicMock()\n mock_service_instance.sync_environment.side_effect = [\n None,\n RuntimeError(\"Connection refused\"),\n ]\n MockService.return_value = mock_service_instance\n MockClient.return_value = MagicMock()\n\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 1\n assert result[\"details\"][\"failed\"][0][\"env_id\"] == \"test-env-2\"\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):\n \"\"\"Verify that calling sync twice doesn't duplicate the Environment row.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n\n cm = _make_sync_config_manager([_mock_env])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\"),\n patch(\"src.api.routes.migration.IdMappingService\"),\n ):\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n env_count = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").count()\n assert env_count == 1\n\n\n# --- get_dashboards tests ---\n\n\n@pytest.mark.asyncio\nasync def test_get_dashboards_success(_mock_env):\n from src.api.routes.migration import get_dashboards\n\n cm = _make_sync_config_manager([_mock_env])\n\n with patch(\"src.api.routes.migration.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [{\"id\": 1, \"title\": \"Test\"}]\n MockClient.return_value = mock_client\n\n result = await get_dashboards(env_id=\"test-env-1\", config_manager=cm, _=None)\n assert len(result) == 1\n assert result[0][\"id\"] == 1\n\n\n@pytest.mark.asyncio\nasync def test_get_dashboards_invalid_env_raises_404(_mock_env):\n from src.api.routes.migration import get_dashboards\n\n cm = _make_sync_config_manager([_mock_env])\n\n with pytest.raises(HTTPException) as exc:\n await get_dashboards(env_id=\"wrong-env\", config_manager=cm, _=None)\n assert exc.value.status_code == 404\n\n\n# --- execute_migration tests ---\n\n\n@pytest.mark.asyncio\nasync def test_execute_migration_success(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n\n cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target\n tm = MagicMock()\n tm.create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"test-env-1\", selected_ids=[1, 2]\n )\n\n result = await execute_migration(\n selection=selection, config_manager=cm, task_manager=tm, _=None\n )\n assert result[\"task_id\"] == \"task-123\"\n tm.create_task.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_execute_migration_invalid_env_raises_400(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n\n cm = _make_sync_config_manager([_mock_env])\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"non-existent\", selected_ids=[1]\n )\n\n with pytest.raises(HTTPException) as exc:\n await execute_migration(\n selection=selection, config_manager=cm, task_manager=MagicMock(), _=None\n )\n assert exc.value.status_code == 400\n\n\n@pytest.mark.asyncio\nasync def test_dry_run_migration_returns_diff_and_risk(db_session):\n # @TEST_EDGE: missing_target_datasource -> validates high risk item generation\n # @TEST_EDGE: breaking_reference -> validates high risk on missing dataset link\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n\n env_source = MagicMock()\n env_source.id = \"src\"\n env_source.name = \"Source\"\n env_source.url = \"http://source\"\n env_source.username = \"admin\"\n env_source.password = \"admin\"\n env_source.verify_ssl = False\n env_source.timeout = 30\n\n env_target = MagicMock()\n env_target.id = \"tgt\"\n env_target.name = \"Target\"\n env_target.url = \"http://target\"\n env_target.username = \"admin\"\n env_target.password = \"admin\"\n env_target.verify_ssl = False\n env_target.timeout = 30\n\n cm = _make_sync_config_manager([env_source, env_target])\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.MigrationDryRunService\") as MockService,\n ):\n source_client = MagicMock()\n target_client = MagicMock()\n MockClient.side_effect = [source_client, target_client]\n\n service_instance = MagicMock()\n service_payload = {\n \"generated_at\": \"2026-02-27T00:00:00+00:00\",\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": [\"Sales\"],\n \"diff\": {\n \"dashboards\": {\n \"create\": [],\n \"update\": [{\"uuid\": \"dash-1\"}],\n \"delete\": [],\n },\n \"charts\": {\"create\": [{\"uuid\": \"chart-1\"}], \"update\": [], \"delete\": []},\n \"datasets\": {\n \"create\": [{\"uuid\": \"dataset-1\"}],\n \"update\": [],\n \"delete\": [],\n },\n },\n \"summary\": {\n \"dashboards\": {\"create\": 0, \"update\": 1, \"delete\": 0},\n \"charts\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"datasets\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"selected_dashboards\": 1,\n },\n \"risk\": {\n \"score\": 75,\n \"level\": \"high\",\n \"items\": [\n {\"code\": \"missing_datasource\"},\n {\"code\": \"breaking_reference\"},\n ],\n },\n }\n service_instance.run.return_value = service_payload\n MockService.return_value = service_instance\n\n result = await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n\n assert result[\"summary\"][\"dashboards\"][\"update\"] == 1\n assert result[\"summary\"][\"charts\"][\"create\"] == 1\n assert result[\"summary\"][\"datasets\"][\"create\"] == 1\n assert result[\"risk\"][\"score\"] > 0\n assert any(item[\"code\"] == \"missing_datasource\" for item in result[\"risk\"][\"items\"])\n assert any(item[\"code\"] == \"breaking_reference\" for item in result[\"risk\"][\"items\"])\n\n\n@pytest.mark.asyncio\nasync def test_dry_run_migration_rejects_same_environment(db_session):\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n\n env = MagicMock()\n env.id = \"same\"\n env.name = \"Same\"\n env.url = \"http://same\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n\n cm = _make_sync_config_manager([env])\n selection = DashboardSelection(\n selected_ids=[1], source_env_id=\"same\", target_env_id=\"same\"\n )\n\n with pytest.raises(HTTPException) as exc:\n await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert exc.value.status_code == 400\n\n\n# [/DEF:TestMigrationRoutes:Module]\n" }, @@ -6574,15 +6490,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -6614,15 +6521,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -6645,7 +6543,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Verifies profile API route contracts for preference read/update and Superset account lookup.", "SEMANTICS": [ @@ -6666,20 +6564,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -6692,6 +6576,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -6741,6 +6626,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -6794,6 +6688,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -6847,6 +6750,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -7187,7 +7099,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DEBT": "Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().", "INVARIANT": "API response contract contains {items,total,page,page_size,has_next,applied_filters}.", "LAYER": "Domain (Tests)", @@ -7225,20 +7137,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -7251,6 +7149,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -7280,6 +7179,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -7434,7 +7342,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DEBT": "Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().", "INVARIANT": "Detail endpoint tests must keep deterministic assertions for success and not-found contracts.", "LAYER": "Domain (Tests)", @@ -7471,20 +7379,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -7497,6 +7391,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -7581,7 +7476,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "List and detail payloads include required contract keys.", "LAYER": "Domain (Tests)", "PURPOSE": "Validate implemented reports payload shape against OpenAPI-required top-level contract fields.", @@ -7610,20 +7505,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -7636,6 +7517,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -7654,7 +7536,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "get_all_tasks returns seeded tasks unchanged.", "PURPOSE": "Minimal task-manager fake exposing static task list for OpenAPI conformance checks." }, @@ -7676,6 +7558,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -7709,6 +7600,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -7742,6 +7642,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -7830,7 +7739,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain (Tests)", "PURPOSE": "Contract testing for task logs API endpoints.", "SEMANTICS": [ @@ -7852,20 +7761,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Domain (Tests)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Domain (Tests)" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_FIXTURE", @@ -8041,7 +7936,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All endpoints in this module require 'Admin' role or 'admin' scope.", "LAYER": "API", "PURPOSE": "Admin API endpoints for user and role management.", @@ -8083,20 +7978,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -8109,6 +7990,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8126,6 +8008,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8143,6 +8026,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8161,7 +8045,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "Returns a list of UserSchema objects.", "PRE": "Current user has 'Admin' role.", @@ -8220,7 +8104,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "New user is created in the database.", "PRE": "Current user has 'Admin' role.", @@ -8278,6 +8162,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -8296,7 +8181,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "User record is updated in the database.", "PRE": "Current user has 'Admin' role.", @@ -8355,7 +8240,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "User record is removed from the database.", "PRE": "Current user has 'Admin' role.", @@ -8414,7 +8299,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Lists all available roles.", "RETURN": "List[RoleSchema] - List of roles." }, @@ -8445,6 +8330,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -8463,7 +8349,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "New Role record is created in auth.db.", "PRE": "Role name must be unique.", @@ -8531,6 +8417,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -8549,7 +8436,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "Role record is updated in auth.db.", "PRE": "role_id must be a valid existing role UUID.", @@ -8617,6 +8504,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -8635,7 +8523,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "Role record is removed from auth.db.", "PRE": "role_id must be a valid existing role UUID.", @@ -8703,6 +8591,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -8721,7 +8610,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Lists all AD Group to Role mappings." }, "relations": [ @@ -8745,7 +8634,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Creates a new AD Group mapping." }, "relations": [ @@ -8790,6 +8679,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8807,6 +8697,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8824,6 +8715,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -8842,7 +8734,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Audit endpoint requires tasks:READ permission.", "LAYER": "API", "PURPOSE": "FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit." @@ -8868,20 +8760,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -8909,6 +8787,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -8931,7 +8827,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Conversations are grouped by conversation_id sorted by latest activity descending.", "PRE": "Authenticated user context and valid pagination params.", "PURPOSE": "Return paginated conversation list for current user with archived flag and last message preview.", @@ -8976,7 +8872,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Conversation records are removed from DB and CONVERSATIONS cache.", "PRE": "conversation_id belongs to current_user.", "PURPOSE": "Soft-delete or hard-delete a conversation and clear its in-memory trace." @@ -9039,6 +8935,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -9083,6 +8988,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -9102,7 +9016,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.", "LAYER": "API", "PURPOSE": "Deterministic RU/EN command text parser that converts user messages into intent payloads." @@ -9125,20 +9039,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "POST", @@ -9179,7 +9079,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]", "INVARIANT": "every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.", "POST": "Returns intent dict with domain/operation/entities/confidence/risk fields.", @@ -9233,7 +9133,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Dataset review operations are always scoped to the owner's session.", "LAYER": "API", "PURPOSE": "Dataset review context loading and intent planning for the assistant API." @@ -9268,20 +9168,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "POST", @@ -9322,7 +9208,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns a serializable dictionary containing the complete review context.", "PRE": "session_id is a valid active review session identifier.", "PURPOSE": "Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.", @@ -9349,7 +9235,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns a loaded context object with session data and findings.", "PRE": "session_id is a valid active review session identifier.", "PURPOSE": "Load owner-scoped dataset-review context for assistant planning and grounded response generation.", @@ -9376,7 +9262,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract structured dataset-review focus target hints embedded in assistant prompts." }, "relations": [], @@ -9393,7 +9279,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve one semantic field from assistant-visible context by id or user-visible label." }, "relations": [], @@ -9410,7 +9296,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract one quoted assistant command segment after a label token." }, "relations": [], @@ -9427,7 +9313,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing." }, "relations": [ @@ -9451,7 +9337,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Dataset review dispatch requires valid session version for write operations.", "LAYER": "API", "PURPOSE": "Dispatch and confirmation handling for dataset-review assistant intents." @@ -9486,20 +9372,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "POST", @@ -9540,7 +9412,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics." }, "relations": [], @@ -9557,7 +9429,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns a structured response with planned actions and confirmations.", "PRE": "context contains valid session data and user intent.", "PURPOSE": "Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.", @@ -9584,7 +9456,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Unsupported operations are rejected via HTTPException(400).", "LAYER": "API", "PURPOSE": "Intent dispatch engine, confirmation summary, and clarification text for the assistant API." @@ -9616,20 +9488,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -9657,6 +9515,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -9679,7 +9555,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returned text is human-readable and actionable for target operation.", "PRE": "state was classified as needs_clarification for current intent/error combination.", "PURPOSE": "Convert technical missing-parameter errors into user-facing clarification prompts." @@ -9717,7 +9593,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Returns a formatted summary string suitable for display to the user.", "PRE": "actions is a non-empty list of planned review actions.", "PURPOSE": "Build human-readable confirmation prompt for an intent before execution.", @@ -9747,7 +9623,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]", "INVARIANT": "unsupported operations are rejected via HTTPException(400).", "POST": "Returns response text, optional task id, and UI actions for follow-up.", @@ -9781,7 +9657,26 @@ "target_ref": "[GitService]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_dispatch_intent:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services.\n# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]\n# @RELATION: DEPENDS_ON -> [_check_any_permission]\n# @RELATION: DEPENDS_ON -> [_resolve_dashboard_id_entity]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [GitService]\n# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.\n# @PRE: intent operation is known and actor permissions are validated per operation.\n# @POST: Returns response text, optional task id, and UI actions for follow-up.\n# @INVARIANT: unsupported operations are rejected via HTTPException(400).\nasync def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]:\n with belief_scope('_dispatch_intent'):\n logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')\n operation = intent.get('operation')\n entities = intent.get('entities', {})\n if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context':\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db)\n if operation == 'show_capabilities':\n from ._llm_planner import _build_tool_catalog\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'}\n available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels]\n if not available:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Сейчас нет доступных для вас операций ассистента.', None, [])\n commands = '\\n'.join((f'- {item}' for item in available))\n text = f'Вот что я могу сделать для вас:\\n{commands}\\n\\nПример: `запусти миграцию с dev на prod для дашборда 42`.'\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, [])\n if operation == 'get_health_summary':\n from src.services.health_service import HealthService\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=env_id)\n env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений'\n text = f'Сводка здоровья дашбордов для {env_name}:\\n- ✅ Прошли проверку: {summary.pass_count}\\n- ⚠️ С предупреждениями: {summary.warn_count}\\n- ❌ Ошибки валидации: {summary.fail_count}\\n- ❓ Неизвестно: {summary.unknown_count}'\n actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')]\n if summary.fail_count > 0:\n text += '\\n\\nОбнаружены ошибки в следующих дашбордах:'\n for item in summary.items:\n if item.status == 'FAIL':\n text += f\"\\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}\"\n actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}'))\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, actions[:5])\n if operation == 'get_task_status':\n _check_any_permission(current_user, [('tasks', 'READ')])\n task_id = entities.get('task_id')\n if not task_id:\n recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id]\n if not recent:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('У вас пока нет задач в истории.', None, [])\n task = recent[0]\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(status_code=404, detail=f'Task {task_id} not found')\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Статус задачи {task.id}: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n if operation == 'create_branch':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n branch_name = entities.get('branch_name')\n if not dashboard_id or not branch_name:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name')\n git_service.create_branch(dashboard_id, branch_name, 'main')\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, [])\n if operation == 'commit_changes':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n commit_message = entities.get('message')\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n git_service.commit_changes(dashboard_id, commit_message, None)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Коммит выполнен успешно.', None, [])\n if operation == 'deploy_dashboard':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id or not env_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment')\n task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'execute_migration':\n _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')])\n src_token = entities.get('source_env')\n dashboard_ref = entities.get('dashboard_ref')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n src = _resolve_env_id(src_token, config_manager)\n tgt = _resolve_env_id(entities.get('target_env'), config_manager)\n if not src or not tgt:\n raise HTTPException(status_code=422, detail='Missing source_env/target_env')\n if not dashboard_id and (not dashboard_ref):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n migration_params: Dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}\n if dashboard_id:\n migration_params['selected_ids'] = [dashboard_id]\n else:\n migration_params['dashboard_regex'] = str(dashboard_ref)\n task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])])\n if operation == 'run_backup':\n _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n if not env_id:\n raise HTTPException(status_code=400, detail='Missing or unknown environment')\n params: Dict[str, Any] = {'environment_id': env_id}\n if entities.get('dashboard_id') or entities.get('dashboard_ref'):\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n params['dashboard_ids'] = [dashboard_id]\n task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])])\n if operation == 'run_llm_validation':\n _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation')\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.')\n provider = LLMProviderService(db).get_provider(provider_id)\n provider_model = provider.default_model if provider else ''\n if not is_multimodal_model(provider_model, provider.provider_type if provider else None):\n raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).')\n task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'run_llm_documentation':\n _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')])\n dataset_id = entities.get('dataset_id')\n env_id = _resolve_env_id(entities.get('environment'), config_manager)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation')\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider')\n task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n raise HTTPException(status_code=400, detail='Unsupported operation')\n\n\n# [/DEF:_dispatch_intent:Function]\n" }, @@ -9794,7 +9689,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Failed persistence attempts always rollback before returning.", "LAYER": "API", "PURPOSE": "Conversation history, audit trail, and confirmation persistence helpers for the assistant API." @@ -9817,20 +9712,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -9853,7 +9734,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]", "INVARIANT": "every appended entry includes generated message_id and created_at timestamp.", "POST": "Message entry is appended to CONVERSATIONS key list.", @@ -9936,6 +9817,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "UPDATES" @@ -9954,7 +9836,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]", "INVARIANT": "failed persistence attempts always rollback before returning.", "POST": "Message row is committed or persistence failure is logged.", @@ -10038,7 +9920,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[user_id,payload:Dict[str,Any]] -> Output[None]", "INVARIANT": "persisted in-memory audit entry always contains created_at in ISO format.", "POST": "ASSISTANT_AUDIT list for user contains new timestamped entry.", @@ -10121,6 +10003,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "UPDATES" @@ -10139,7 +10022,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Audit row is committed or failure is logged with rollback.", "PRE": "db session is writable and payload is JSON-serializable.", "PURPOSE": "Persist structured assistant audit payload in database." @@ -10177,7 +10060,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Confirmation row exists in persistent storage.", "PRE": "record contains id/user/intent/dispatch/expiry fields.", "PURPOSE": "Persist confirmation token record to database." @@ -10215,7 +10098,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "State and consumed_at fields are updated when applicable.", "PRE": "confirmation_id references existing row.", "PURPOSE": "Update persistent confirmation token lifecycle state." @@ -10253,7 +10136,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns ConfirmationRecord when found, otherwise None.", "PRE": "confirmation_id may or may not exist in storage.", "PURPOSE": "Load confirmation token from database into in-memory model." @@ -10291,7 +10174,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.", "PRE": "user_id identifies current actor.", "PURPOSE": "Resolve active conversation id in memory or create a new one." @@ -10329,7 +10212,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.", "PRE": "user_id and db session are available.", "PURPOSE": "Resolve active conversation using explicit id, memory cache, or persisted history." @@ -10367,7 +10250,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.", "PRE": "db session is available and user_id references current actor scope.", "PURPOSE": "Enforce assistant message retention window by deleting expired rows and in-memory records." @@ -10405,7 +10288,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns True when conversation inactivity exceeds archive threshold.", "PRE": "updated_at can be null for empty conversations.", "PURPOSE": "Determine archived state for a conversation based on last update timestamp." @@ -10443,7 +10326,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns deterministic boolean flag.", "PRE": "value may be bool, string, or FastAPI Query metadata object.", "PURPOSE": "Normalize bool-like query values for compatibility in direct handler invocations/tests." @@ -10481,7 +10364,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Tool catalog is filtered by user permissions before being sent to LLM.", "LAYER": "API", "PURPOSE": "LLM-based intent planning, tool catalog construction, and authorization for the assistant API." @@ -10515,20 +10398,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -10543,7 +10412,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns on first successful permission; raises 403-like HTTPException otherwise.", "PRE": "checks list contains resource-action tuples.", "PURPOSE": "Validate user against alternative permission checks (logical OR)." @@ -10581,7 +10450,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns True when at least one permission check passes.", "PRE": "current_user and checks list are valid.", "PURPOSE": "Check whether user has at least one permission tuple from the provided list." @@ -10619,7 +10488,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns list of executable tools filtered by permission and runtime availability.", "PRE": "current_user is authenticated; config/db are available.", "PURPOSE": "Build current-user tool catalog for LLM planner with operation contracts and defaults." @@ -10664,7 +10533,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returned intent has numeric ids coerced where possible and string values stripped.", "PRE": "intent contains entities dict or missing entities.", "PURPOSE": "Normalize intent entity value types from LLM output to route-compatible values." @@ -10702,7 +10571,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Production deployments always require confirmation.", "LAYER": "API", "PURPOSE": "LLM-based intent planning and authorization for the assistant API — separated from tool catalog." @@ -10730,20 +10599,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -10758,7 +10613,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns normalized intent dict when planning succeeds; otherwise None.", "PRE": "tools list contains allowed operations for current user.", "PURPOSE": "Use active LLM provider to select best tool/operation from dynamic catalog." @@ -10796,7 +10651,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns if authorized; raises HTTPException(403) when denied.", "PRE": "intent.operation is present for known assistant command domains.", "PURPOSE": "Validate user permissions for parsed intent before confirmation/dispatch." @@ -10834,7 +10689,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Resolution functions never raise; they return None on failure.", "LAYER": "API", "PURPOSE": "Environment, dashboard, provider, and task resolution utilities for the assistant API." @@ -10863,20 +10718,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -10899,7 +10740,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns first matched token or None.", "PRE": "patterns contain at least one capture group.", "PURPOSE": "Extract first regex match group from text by ordered pattern list." @@ -10937,7 +10778,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns matched environment id or None.", "PRE": "config_manager provides environment list.", "PURPOSE": "Resolve environment identifier/name token to canonical environment id." @@ -10975,7 +10816,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns True for production/prod synonyms, else False.", "PRE": "config_manager provides environments or token text is provided.", "PURPOSE": "Determine whether environment token resolves to production-like target." @@ -11013,7 +10854,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns provider id or None when no providers configured.", "PRE": "db session can load provider list through LLMProviderService.", "PURPOSE": "Resolve provider token to provider id with active/default fallback." @@ -11051,7 +10892,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns default environment id or None when environment list is empty.", "PRE": "config_manager returns environments list.", "PURPOSE": "Resolve default environment id from settings or first configured environment." @@ -11089,7 +10930,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns dashboard id when uniquely matched, otherwise None.", "PRE": "dashboard_ref is a non-empty string-like token.", "PURPOSE": "Resolve dashboard id by title or slug reference in selected environment." @@ -11127,7 +10968,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns resolved dashboard id or None when ambiguous/unresolvable.", "PRE": "entities may contain dashboard_id as int/str and optional dashboard_ref.", "PURPOSE": "Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback." @@ -11165,7 +11006,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns matching environment name or fallback id.", "PRE": "environment id may be None.", "PURPOSE": "Resolve human-readable environment name by id." @@ -11203,7 +11044,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns zero or more assistant actions for dashboard open/diff.", "PRE": "task object is available.", "PURPOSE": "Build deep-link actions to verify task result from assistant chat." @@ -11241,7 +11082,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns non-empty summary line for known task types or empty string fallback.", "PRE": "task may contain plugin-specific result payload.", "PURPOSE": "Build compact textual summary for completed tasks to reduce \"black box\" effect." @@ -11279,7 +11120,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "INVARIANT": "Risky operations are never executed without valid confirmation token.", "LAYER": "API", "PURPOSE": "FastAPI route handlers for the assistant API — message sending, confirmation, conversation management." @@ -11329,20 +11170,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "DATA_CONTRACT", @@ -11370,6 +11197,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", @@ -11392,7 +11237,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]", "INVARIANT": "non-safe operations are gated with confirmation before execution from this endpoint.", "POST": "Response state is one of clarification/confirmation/started/success/denied/failed.", @@ -11445,6 +11290,24 @@ "tag": "RETURN", "message": "@RETURN is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -11459,7 +11322,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Confirmation state becomes consumed and operation result is persisted in history.", "PRE": "confirmation_id exists, belongs to current user, is pending, and not expired.", "PURPOSE": "Execute previously requested risky operation after explicit user confirmation.", @@ -11504,7 +11367,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Confirmation becomes cancelled and cannot be executed anymore.", "PRE": "confirmation_id exists, belongs to current user, and is still pending.", "PURPOSE": "Cancel pending risky operation and mark confirmation token as cancelled.", @@ -11549,7 +11412,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "In-memory stores are module-level singletons shared across the assistant package.", "LAYER": "API", "PURPOSE": "Pydantic models, in-memory stores, and permission mappings for the assistant API." @@ -11578,20 +11441,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -11613,6 +11462,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -11630,6 +11480,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -11648,7 +11499,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "DATA_CONTRACT": "Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]", "INVARIANT": "message is always non-empty and no longer than 4000 characters.", "POST": "Request object provides message text and optional conversation binding.", @@ -11701,6 +11552,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -11731,6 +11591,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -11749,7 +11610,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "DATA_CONTRACT": "Input[type:str, label:str, target?:str] -> Output[AssistantAction]", "INVARIANT": "type and label are required for every UI action.", "POST": "Action can be rendered as button on frontend.", @@ -11802,6 +11663,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -11832,6 +11702,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -11850,7 +11721,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "DATA_CONTRACT": "Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]", "INVARIANT": "created_at and state are always present in endpoint responses.", "POST": "Payload may include task_id/confirmation_id/actions for UI follow-up.", @@ -11915,6 +11786,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -11945,6 +11825,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "RETURNED_BY" @@ -11962,6 +11843,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "RETURNED_BY" @@ -11979,6 +11861,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "RETURNED_BY" @@ -11997,7 +11880,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "DATA_CONTRACT": "Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]", "INVARIANT": "state defaults to \"pending\" and expires_at bounds confirmation validity.", "POST": "Record tracks lifecycle state and expiry timestamp.", @@ -12062,6 +11945,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -12092,6 +11984,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -12109,6 +12002,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -12126,6 +12020,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -12144,7 +12039,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "API never reports prepared status if preparation errors are present.", "LAYER": "API", "POST": "Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.", @@ -12181,20 +12076,6 @@ "actual_complexity": 4, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -12212,7 +12093,17 @@ "PURPOSE": "Request schema for candidate preparation endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PrepareCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate preparation endpoint.\nclass PrepareCandidateRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n sources: List[str] = Field(default_factory=list)\n operator_id: str = Field(min_length=1)\n\n\n# [/DEF:PrepareCandidateRequest:Class]\n" }, @@ -12228,7 +12119,17 @@ "PURPOSE": "Request schema for clean compliance check run startup." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:StartCheckRequest:Class]\n# @PURPOSE: Request schema for clean compliance check run startup.\nclass StartCheckRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n profile: str = Field(default=\"enterprise-clean\")\n execution_mode: str = Field(default=\"tui\")\n triggered_by: str = Field(default=\"system\")\n\n\n# [/DEF:StartCheckRequest:Class]\n" }, @@ -12244,7 +12145,17 @@ "PURPOSE": "Request schema for candidate registration endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RegisterCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate registration endpoint.\nclass RegisterCandidateRequest(BaseModel):\n id: str = Field(min_length=1)\n version: str = Field(min_length=1)\n source_snapshot_ref: str = Field(min_length=1)\n created_by: str = Field(min_length=1)\n\n\n# [/DEF:RegisterCandidateRequest:Class]\n" }, @@ -12260,7 +12171,17 @@ "PURPOSE": "Request schema for candidate artifact import endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ImportArtifactsRequest:Class]\n# @PURPOSE: Request schema for candidate artifact import endpoint.\nclass ImportArtifactsRequest(BaseModel):\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n\n\n# [/DEF:ImportArtifactsRequest:Class]\n" }, @@ -12276,7 +12197,17 @@ "PURPOSE": "Request schema for manifest build endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BuildManifestRequest:Class]\n# @PURPOSE: Request schema for manifest build endpoint.\nclass BuildManifestRequest(BaseModel):\n created_by: str = Field(default=\"system\")\n\n\n# [/DEF:BuildManifestRequest:Class]\n" }, @@ -12292,7 +12223,17 @@ "PURPOSE": "Request schema for compliance run creation with optional manifest pinning." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CreateComplianceRunRequest:Class]\n# @PURPOSE: Request schema for compliance run creation with optional manifest pinning.\nclass CreateComplianceRunRequest(BaseModel):\n requested_by: str = Field(min_length=1)\n manifest_id: str | None = None\n\n\n# [/DEF:CreateComplianceRunRequest:Class]\n" }, @@ -12328,6 +12269,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12365,6 +12315,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12402,6 +12361,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12439,6 +12407,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12476,6 +12453,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12513,6 +12499,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12550,6 +12545,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12587,6 +12591,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -12601,7 +12614,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "UI (API)", "POST": "Candidate registration, approval, publication, and revocation routes are registered without behavior changes.", "PRE": "Clean release repository dependency is available for candidate lifecycle endpoints.", @@ -12628,22 +12641,7 @@ "target_ref": "[publish_candidate]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:CleanReleaseV2Api:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Redesigned clean release API for headless candidate lifecycle.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION: CALLS -> [approve_candidate]\n# @RELATION: CALLS -> [publish_candidate]\n# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.\n# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.\n# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import List, Dict, Any\nfrom datetime import datetime, timezone\nfrom ...services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ...services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\nfrom ...dependencies import get_clean_release_repository\nfrom ...services.clean_release.enums import CandidateStatus\nfrom ...models.clean_release import (\n ReleaseCandidate,\n CandidateArtifact,\n DistributionManifest,\n)\nfrom ...services.clean_release.dto import CandidateDTO, ManifestDTO\n\nrouter = APIRouter(prefix=\"/api/v2/clean-release\", tags=[\"Clean Release V2\"])\n\n\n# [DEF:ApprovalRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# [/DEF:ApprovalRequest:Class]\n\n\n# [DEF:PublishRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# [/DEF:PublishRequest:Class]\n\n\n# [DEF:RevokeRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# [/DEF:RevokeRequest:Class]\n\n\n# [DEF:register_candidate:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Register a new release candidate.\n# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).\n# @POST: Candidate is saved in repository.\n# @RETURN: CandidateDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION: DEPENDS_ON -> [clean_release_dto]\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate(\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = ReleaseCandidate(\n id=payload[\"id\"],\n version=payload[\"version\"],\n source_snapshot_ref=payload[\"source_snapshot_ref\"],\n created_by=payload[\"created_by\"],\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# [/DEF:register_candidate:Function]\n\n\n# [DEF:import_artifacts:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Associate artifacts with a release candidate.\n# @PRE: Candidate exists.\n# @POST: Artifacts are processed (placeholder).\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_artifacts(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n for art_data in payload.get(\"artifacts\", []):\n artifact = CandidateArtifact(\n id=art_data[\"id\"],\n candidate_id=candidate_id,\n path=art_data[\"path\"],\n sha256=art_data[\"sha256\"],\n size=art_data[\"size\"],\n )\n # In a real repo we'd have save_artifact\n # repository.save_artifact(artifact)\n pass\n\n return {\"status\": \"success\"}\n\n\n# [/DEF:import_artifacts:Function]\n\n\n# [DEF:build_manifest:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Generate distribution manifest for a candidate.\n# @PRE: Candidate exists.\n# @POST: Manifest is created and saved.\n# @RETURN: ManifestDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_manifest(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n manifest = DistributionManifest(\n id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash-123\",\n artifacts_digest=\"art-hash-123\",\n created_by=\"system\",\n created_at=datetime.now(timezone.utc),\n source_snapshot_ref=candidate.source_snapshot_ref,\n content_json={\"items\": [], \"summary\": {}},\n )\n repository.save_manifest(manifest)\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# [/DEF:build_manifest:Function]\n\n\n# [DEF:approve_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate approval.\n# @RELATION: CALLS -> [approve_candidate]\n@router.post(\"/candidates/{candidate_id}/approve\")\nasync def approve_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:approve_candidate_endpoint:Function]\n\n\n# [DEF:reject_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate rejection.\n# @RELATION: CALLS -> [reject_candidate]\n@router.post(\"/candidates/{candidate_id}/reject\")\nasync def reject_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:reject_candidate_endpoint:Function]\n\n\n# [DEF:publish_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to publish an approved candidate.\n# @RELATION: CALLS -> [publish_candidate]\n@router.post(\"/candidates/{candidate_id}/publish\")\nasync def publish_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n published_by=str(payload[\"published_by\"]),\n target_channel=str(payload[\"target_channel\"]),\n publication_ref=payload.get(\"publication_ref\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:publish_candidate_endpoint:Function]\n\n\n# [DEF:revoke_publication_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to revoke a previous publication.\n# @RELATION: CALLS -> [revoke_publication]\n@router.post(\"/publications/{publication_id}/revoke\")\nasync def revoke_publication_endpoint(\n publication_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=publication_id,\n revoked_by=str(payload[\"revoked_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:revoke_publication_endpoint:Function]\n\n# [/DEF:CleanReleaseV2Api:Module]\n" }, @@ -12656,11 +12654,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Schema for approval request payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ApprovalRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# [/DEF:ApprovalRequest:Class]\n" }, @@ -12673,11 +12681,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Schema for publication request payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PublishRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# [/DEF:PublishRequest:Class]\n" }, @@ -12690,11 +12708,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Schema for revocation request payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RevokeRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# [/DEF:RevokeRequest:Class]\n" }, @@ -12707,7 +12735,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Candidate is saved in repository.", "PRE": "Payload contains required fields (id, version, source_snapshot_ref, created_by).", "PURPOSE": "Register a new release candidate.", @@ -12765,7 +12793,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Artifacts are processed (placeholder).", "PRE": "Candidate exists.", "PURPOSE": "Associate artifacts with a release candidate." @@ -12810,7 +12838,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Manifest is created and saved.", "PRE": "Candidate exists.", "PURPOSE": "Generate distribution manifest for a candidate.", @@ -12862,7 +12890,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Endpoint to record candidate approval." }, "relations": [ @@ -12886,7 +12914,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Endpoint to record candidate rejection." }, "relations": [ @@ -12910,7 +12938,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Endpoint to publish an approved candidate." }, "relations": [ @@ -12934,7 +12962,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Endpoint to revoke a previous publication." }, "relations": [ @@ -12958,7 +12986,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (API)", "PURPOSE": "Defines the FastAPI router for managing external database connections.", "SEMANTICS": [ @@ -12982,22 +13010,7 @@ "target_ref": "[ConnectionConfig]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ConnectionsRouter:Module]\n# @SEMANTICS: api, router, connections, database\n# @PURPOSE: Defines the FastAPI router for managing external database connections.\n# @COMPLEXITY: 3\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [get_db]\n# @RELATION: DEPENDS_ON -> [ConnectionConfig]\n\n# [SECTION: IMPORTS]\nfrom typing import List, Optional\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\nfrom ...core.database import get_db, ensure_connection_configs_table\nfrom ...models.connection import ConnectionConfig\nfrom pydantic import BaseModel\nfrom datetime import datetime\nfrom ...core.logger import logger, belief_scope\n# [/SECTION]\n\nrouter = APIRouter()\n\n\n# [DEF:_ensure_connections_schema:Function]\n# @PURPOSE: Ensures the connection_configs table exists before CRUD access.\n# @COMPLEXITY: 3\n# @PRE: db is an active SQLAlchemy session.\n# @POST: The current bind can safely query ConnectionConfig.\n# @RELATION: CALLS -> ensure_connection_configs_table\ndef _ensure_connections_schema(db: Session):\n with belief_scope(\"ConnectionsRouter.ensure_schema\"):\n ensure_connection_configs_table(db.get_bind())\n\n\n# [/DEF:_ensure_connections_schema:Function]\n\n\n# [DEF:ConnectionSchema:Class]\n# @PURPOSE: Pydantic model for connection response.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionSchema(BaseModel):\n id: str\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n created_at: datetime\n\n class Config:\n orm_mode = True\n\n\n# [/DEF:ConnectionSchema:Class]\n\n\n# [DEF:ConnectionCreate:Class]\n# @PURPOSE: Pydantic model for creating a connection.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionCreate(BaseModel):\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n password: Optional[str] = None\n\n\n# [/DEF:ConnectionCreate:Class]\n\n\n# [DEF:list_connections:Function]\n# @PURPOSE: Lists all saved connections.\n# @COMPLEXITY: 3\n# @PRE: Database session is active.\n# @POST: Returns list of connection configs.\n# @PARAM: db (Session) - Database session.\n# @RETURN: List[ConnectionSchema] - List of connections.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.get(\"\", response_model=List[ConnectionSchema])\nasync def list_connections(db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.list_connections\"):\n _ensure_connections_schema(db)\n connections = db.query(ConnectionConfig).all()\n return connections\n\n\n# [/DEF:list_connections:Function]\n\n\n# [DEF:create_connection:Function]\n# @PURPOSE: Creates a new connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection name is unique.\n# @POST: Connection is saved to DB.\n# @PARAM: connection (ConnectionCreate) - Config data.\n# @PARAM: db (Session) - Database session.\n# @RETURN: ConnectionSchema - Created connection.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.post(\"\", response_model=ConnectionSchema, status_code=status.HTTP_201_CREATED)\nasync def create_connection(\n connection: ConnectionCreate, db: Session = Depends(get_db)\n):\n with belief_scope(\"ConnectionsRouter.create_connection\", f\"name={connection.name}\"):\n _ensure_connections_schema(db)\n db_connection = ConnectionConfig(**connection.dict())\n db.add(db_connection)\n db.commit()\n db.refresh(db_connection)\n logger.info(\n f\"[ConnectionsRouter.create_connection][Success] Created connection {db_connection.id}\"\n )\n return db_connection\n\n\n# [/DEF:create_connection:Function]\n\n\n# [DEF:delete_connection:Function]\n# @PURPOSE: Deletes a connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection ID exists.\n# @POST: Connection is removed from DB.\n# @PARAM: connection_id (str) - ID to delete.\n# @PARAM: db (Session) - Database session.\n# @RETURN: None.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.delete(\"/{connection_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_connection(connection_id: str, db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.delete_connection\", f\"id={connection_id}\"):\n _ensure_connections_schema(db)\n db_connection = (\n db.query(ConnectionConfig)\n .filter(ConnectionConfig.id == connection_id)\n .first()\n )\n if not db_connection:\n logger.error(\n f\"[ConnectionsRouter.delete_connection][State] Connection {connection_id} not found\"\n )\n raise HTTPException(status_code=404, detail=\"Connection not found\")\n db.delete(db_connection)\n db.commit()\n logger.info(\n f\"[ConnectionsRouter.delete_connection][Success] Deleted connection {connection_id}\"\n )\n return\n\n\n# [/DEF:delete_connection:Function]\n\n# [/DEF:ConnectionsRouter:Module]\n" }, @@ -13010,7 +13023,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "The current bind can safely query ConnectionConfig.", "PRE": "db is an active SQLAlchemy session.", "PURPOSE": "Ensures the connection_configs table exists before CRUD access." @@ -13055,7 +13068,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Pydantic model for connection response." }, "relations": [ @@ -13079,7 +13092,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Pydantic model for creating a connection." }, "relations": [ @@ -13103,7 +13116,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Database session.", "POST": "Returns list of connection configs.", "PRE": "Database session is active.", @@ -13168,7 +13181,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Database session.", "POST": "Connection is saved to DB.", "PRE": "Connection name is unique.", @@ -13233,7 +13246,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Database session.", "POST": "Connection is removed from DB.", "PRE": "Connection ID exists.", @@ -13298,7 +13311,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input(env_id, filters) -> Output(DashboardsResponse)", "INVARIANT": "All dashboard responses include git_status and last_task metadata", "LAYER": "API", @@ -13335,20 +13348,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -13369,6 +13368,24 @@ "actual_complexity": 5, "contract_type": "Module" } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -13383,7 +13400,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "Dashboard action route handlers — migrate, backup.", "SEMANTICS": [ @@ -13408,20 +13425,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -13444,7 +13447,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "request (MigrateRequest) - Migration request with source, target, and dashboard IDs", "POST": "Task is created and queued for execution", "PRE": "dashboard_ids is a non-empty list", @@ -13518,7 +13521,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "request (BackupRequest) - Backup request with environment and dashboard IDs", "POST": "If schedule is provided, a scheduled task is created", "PRE": "dashboard_ids is a non-empty list", @@ -13592,7 +13595,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Dashboard detail, db-mappings, task history, thumbnail route handlers.", "SEMANTICS": [ @@ -13628,22 +13631,7 @@ "target_ref": "[DashboardProjection]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:DashboardDetailRoutes:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, dashboards, detail, routes\n# @PURPOSE: Dashboard detail, db-mappings, task history, thumbnail route handlers.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardRouter]\n# @RELATION: DEPENDS_ON -> [DashboardSchemas]\n# @RELATION: DEPENDS_ON -> [DashboardHelpers]\n# @RELATION: DEPENDS_ON -> [DashboardProjection]\n\n# [SECTION: IMPORTS]\nfrom typing import Optional, List, Dict, Any\nimport re\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends, HTTPException, Query, Response\nfrom fastapi.responses import JSONResponse\n\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n get_mapping_service,\n has_permission,\n)\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.superset_client import SupersetClient\nfrom src.core.logger import logger, belief_scope\nfrom src.core.utils.network import DashboardNotFoundError\nfrom ._helpers import (\n _resolve_dashboard_id_from_ref,\n _resolve_dashboard_id_from_ref_async,\n)\nfrom ._projection import (\n _task_matches_dashboard,\n)\nfrom ._schemas import (\n DashboardDetailResponse,\n DashboardTaskHistoryItem,\n DashboardTaskHistoryResponse,\n DatabaseMapping,\n DatabaseMappingsResponse,\n)\nfrom ._router import router\n# [/SECTION]\n\n\n# [DEF:get_database_mappings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get database mapping suggestions between source and target environments\n# @PRE: User has permission plugin:migration:read\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @POST: Returns list of suggested database mappings with confidence scores\n# @PARAM: source_env_id (str) - Source environment ID\n# @PARAM: target_env_id (str) - Target environment ID\n# @RETURN: DatabaseMappingsResponse - List of suggested mappings\n# @RELATION: CALLS ->[MappingService:get_suggestions]\n@router.get(\"/db-mappings\", response_model=DatabaseMappingsResponse)\nasync def get_database_mappings(\n source_env_id: str,\n target_env_id: str,\n config_manager=Depends(get_config_manager),\n mapping_service=Depends(get_mapping_service),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_database_mappings\", f\"source={source_env_id}, target={target_env_id}\"\n ):\n environments = config_manager.get_environments()\n source_env = next((e for e in environments if e.id == source_env_id), None)\n target_env = next((e for e in environments if e.id == target_env_id), None)\n\n if not source_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n suggestions = await mapping_service.get_suggestions(\n source_env_id, target_env_id\n )\n\n mappings = [\n DatabaseMapping(\n source_db=s.get(\"source_db\", \"\"),\n target_db=s.get(\"target_db\", \"\"),\n source_db_uuid=s.get(\"source_db_uuid\"),\n target_db_uuid=s.get(\"target_db_uuid\"),\n confidence=s.get(\"confidence\", 0.0),\n )\n for s in suggestions\n ]\n\n logger.info(\n f\"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions\"\n )\n\n return DatabaseMappingsResponse(mappings=mappings)\n\n except Exception as e:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to get database mappings: {str(e)}\"\n )\n\n\n# [/DEF:get_database_mappings:Function]\n\n\n# [DEF:get_dashboard_detail:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Fetch detailed dashboard info with related charts and datasets\n# @PRE: env_id must be valid and dashboard ref (slug or id) must exist\n# @POST: Returns dashboard detail payload for overview page\n# @RELATION: CALLS ->[AsyncSupersetClient]\n@router.get(\"/{dashboard_ref}\", response_model=DashboardDetailResponse)\nasync def get_dashboard_detail(\n dashboard_ref: str,\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_detail\", f\"dashboard_ref={dashboard_ref}, env_id={env_id}\"\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n sync_client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, sync_client)\n detail = sync_client.get_dashboard_detail(dashboard_id)\n logger.info(\n f\"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets\"\n )\n return DashboardDetailResponse(**detail)\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboard detail: {str(e)}\"\n )\n\n\n# [/DEF:get_dashboard_detail:Function]\n\n\n# [DEF:get_dashboard_tasks_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard.\n# @PRE: dashboard ref (slug or id) is valid.\n# @POST: Response contains sorted task history (newest first).\n@router.get(\"/{dashboard_ref}/tasks\", response_model=DashboardTaskHistoryResponse)\nasync def get_dashboard_tasks_history(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n limit: int = Query(20, ge=1, le=100),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_tasks_history\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}\",\n ):\n dashboard_id: Optional[int] = None\n client: Optional[AsyncSupersetClient] = None\n try:\n if dashboard_ref.isdigit():\n dashboard_id = int(dashboard_ref)\n elif env_id:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n client = AsyncSupersetClient(env)\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, client\n )\n else:\n logger.error(\n \"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id\"\n )\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required when dashboard reference is a slug\",\n )\n\n matching_tasks = []\n for task in task_manager.get_all_tasks():\n if _task_matches_dashboard(task, dashboard_id, env_id):\n matching_tasks.append(task)\n\n def _sort_key(task_obj: Any) -> str:\n return str(getattr(task_obj, \"started_at\", \"\") or \"\") or str(\n getattr(task_obj, \"finished_at\", \"\") or \"\"\n )\n\n matching_tasks.sort(key=_sort_key, reverse=True)\n selected = matching_tasks[:limit]\n\n items = []\n for task in selected:\n result = getattr(task, \"result\", None)\n summary = None\n validation_status = None\n if isinstance(result, dict):\n raw_validation_status = result.get(\"status\")\n if raw_validation_status is not None:\n validation_status = str(raw_validation_status)\n summary = (\n result.get(\"summary\")\n or result.get(\"status\")\n or result.get(\"message\")\n )\n params = getattr(task, \"params\", {}) or {}\n items.append(\n DashboardTaskHistoryItem(\n id=str(getattr(task, \"id\", \"\")),\n plugin_id=str(getattr(task, \"plugin_id\", \"\")),\n status=str(getattr(task, \"status\", \"\")),\n validation_status=validation_status,\n started_at=getattr(task, \"started_at\", None).isoformat()\n if getattr(task, \"started_at\", None)\n else None,\n finished_at=getattr(task, \"finished_at\", None).isoformat()\n if getattr(task, \"finished_at\", None)\n else None,\n env_id=str(params.get(\"environment_id\") or params.get(\"env\"))\n if (params.get(\"environment_id\") or params.get(\"env\"))\n else None,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}\"\n )\n return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)\n finally:\n if client is not None:\n await client.aclose()\n\n\n# [/DEF:get_dashboard_tasks_history:Function]\n\n\n# [DEF:get_dashboard_thumbnail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Proxies Superset dashboard thumbnail with cache support.\n# @RELATION: CALLS ->[AsyncSupersetClient]\n# @PRE: env_id must exist.\n# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.\n@router.get(\"/{dashboard_ref}/thumbnail\")\nasync def get_dashboard_thumbnail(\n dashboard_ref: str,\n env_id: str,\n force: bool = Query(False),\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_thumbnail\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}\",\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)\n digest = None\n thumb_endpoint = None\n\n try:\n screenshot_payload = client.network.request(\n method=\"POST\",\n endpoint=f\"/dashboard/{dashboard_id}/cache_dashboard_screenshot/\",\n json={\"force\": force},\n )\n payload = (\n screenshot_payload.get(\"result\", screenshot_payload)\n if isinstance(screenshot_payload, dict)\n else {}\n )\n image_url = (\n payload.get(\"image_url\", \"\") if isinstance(payload, dict) else \"\"\n )\n if isinstance(image_url, str) and image_url:\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n image_url,\n )\n if matched:\n digest = matched.group(1)\n except DashboardNotFoundError:\n logger.warning(\n \"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url\"\n )\n\n if not digest:\n dashboard_payload = client.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}\",\n )\n dashboard_data = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n thumbnail_url = (\n dashboard_data.get(\"thumbnail_url\", \"\")\n if isinstance(dashboard_data, dict)\n else \"\"\n )\n if isinstance(thumbnail_url, str) and thumbnail_url:\n parsed = urlparse(thumbnail_url)\n parsed_path = parsed.path or thumbnail_url\n if parsed_path.startswith(\"/api/v1/\"):\n parsed_path = parsed_path[len(\"/api/v1\") :]\n thumb_endpoint = parsed_path\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n parsed_path,\n )\n if matched:\n digest = matched.group(1)\n\n if not thumb_endpoint:\n thumb_endpoint = (\n f\"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/\"\n )\n\n thumb_response = client.network.request(\n method=\"GET\",\n endpoint=thumb_endpoint,\n raw_response=True,\n allow_redirects=True,\n )\n\n if thumb_response.status_code == 202:\n payload_202: Dict[str, Any] = {}\n try:\n payload_202 = thumb_response.json()\n except Exception:\n payload_202 = {\"message\": \"Thumbnail is being generated\"}\n return JSONResponse(status_code=202, content=payload_202)\n\n content_type = thumb_response.headers.get(\"Content-Type\", \"image/png\")\n return Response(content=thumb_response.content, media_type=content_type)\n except DashboardNotFoundError as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}\"\n )\n raise HTTPException(status_code=404, detail=\"Dashboard thumbnail not found\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}\"\n )\n raise HTTPException(\n status_code=503,\n detail=f\"Failed to fetch dashboard thumbnail: {str(e)}\",\n )\n\n\n# [/DEF:get_dashboard_thumbnail:Function]\n# [/DEF:DashboardDetailRoutes:Module]\n" }, @@ -13656,7 +13644,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "target_env_id (str) - Target environment ID", "POST": "Returns list of suggested database mappings with confidence scores", "PRE": "source_env_id and target_env_id are valid environment IDs", @@ -13724,7 +13712,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns dashboard detail payload for overview page", "PRE": "env_id must be valid and dashboard ref (slug or id) must exist", "PURPOSE": "Fetch detailed dashboard info with related charts and datasets" @@ -13778,7 +13766,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Response contains sorted task history (newest first).", "PRE": "dashboard ref (slug or id) is valid.", "PURPOSE": "Returns history of backup and LLM validation tasks for a dashboard." @@ -13816,7 +13804,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns image bytes or 202 when thumbnail is being prepared by Superset.", "PRE": "env_id must exist.", "PURPOSE": "Proxies Superset dashboard thumbnail with cache support." @@ -13861,7 +13849,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Infra", "PURPOSE": "Basic helper functions for dashboard route handlers — slug resolution, filter normalization.", "SEMANTICS": [ @@ -13908,7 +13896,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns dashboard ID when found, otherwise None.", "PRE": "`dashboard_slug` is non-empty.", "PURPOSE": "Resolve dashboard numeric ID by slug using Superset list endpoint." @@ -13946,7 +13934,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns a valid dashboard ID or raises HTTPException(404).", "PRE": "`dashboard_ref` is provided in route path.", "PURPOSE": "Resolve dashboard ID from slug-first reference with numeric fallback." @@ -13984,7 +13972,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns dashboard ID when found, otherwise None.", "PRE": "dashboard_slug is non-empty.", "PURPOSE": "Resolve dashboard numeric ID by slug using async Superset list endpoint." @@ -14022,7 +14010,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns valid dashboard ID or raises HTTPException(404).", "PRE": "dashboard_ref is provided in route path.", "PURPOSE": "Resolve dashboard ID from slug-first reference using async Superset client." @@ -14060,7 +14048,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns trimmed normalized list preserving input order.", "PRE": "values may be None or list of strings.", "PURPOSE": "Normalize query filter values to lower-cased non-empty tokens." @@ -14098,7 +14086,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns one of ok|diff|no_repo|error|pending.", "PRE": "dashboard payload may contain git_status or None.", "PURPOSE": "Build comparable git status token for dashboards filtering." @@ -14136,7 +14124,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "API", "PURPOSE": "Dashboard listing route handler for Dashboard Hub.", "SEMANTICS": [ @@ -14173,20 +14161,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "POST", @@ -14227,7 +14201,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "page_size (Optional[int]) - Items per page (default: 10, max: 100)", "POST": "Response includes effective profile filter metadata for main dashboards page context", "PRE": "page_size must be between 1 and 100 if provided", @@ -14286,7 +14260,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Infra", "PURPOSE": "Dashboard response projection and profile-filter helpers for Dashboard Hub routes.", "SEMANTICS": [ @@ -14334,7 +14308,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize actor alias token to comparable trim+lower text." }, "relations": [], @@ -14351,7 +14325,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Project owner payload value into stable display string for API response contracts." }, "relations": [], @@ -14368,7 +14342,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize dashboard owners payload to optional list of display strings." }, "relations": [], @@ -14385,7 +14359,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Project dashboard payloads to response-contract-safe shape." }, "relations": [], @@ -14402,7 +14376,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve dashboard profile-filter binding through current or legacy profile service contracts." }, "relations": [], @@ -14419,7 +14393,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.", "SIDE_EFFECT": "Performs at most one Superset users-lookup request." }, @@ -14447,7 +14421,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Apply profile actor matching against multiple aliases (username + optional display name)." }, "relations": [], @@ -14464,7 +14438,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Checks whether task params are tied to a specific dashboard and environment." }, "relations": [], @@ -14481,7 +14455,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Single APIRouter instance for all dashboard endpoints." }, "relations": [ @@ -14493,6 +14467,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -14515,7 +14498,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Infra", "PURPOSE": "DTO classes for the Dashboard Hub API.", "SEMANTICS": [ @@ -14534,6 +14517,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -14556,7 +14548,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for dashboard Git synchronization status." }, "relations": [], @@ -14572,7 +14564,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14597,7 +14591,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14623,7 +14619,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO representing a single dashboard with projected metadata." }, "relations": [], @@ -14639,7 +14635,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14664,7 +14662,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14690,7 +14690,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Metadata about applied profile filters for UI context." }, "relations": [], @@ -14706,7 +14706,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14731,7 +14733,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14757,7 +14761,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Envelope DTO for paginated dashboards list." }, "relations": [], @@ -14773,7 +14777,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14798,7 +14804,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14824,7 +14832,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for a chart linked to a dashboard." }, "relations": [], @@ -14840,7 +14848,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14865,7 +14875,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14891,7 +14903,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for a dataset associated with a dashboard." }, "relations": [], @@ -14907,7 +14919,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14932,7 +14946,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -14958,7 +14974,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Detailed dashboard metadata including children." }, "relations": [], @@ -14974,7 +14990,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -14999,7 +15017,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15025,7 +15045,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Individual history record entry." }, "relations": [], @@ -15041,7 +15061,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15066,7 +15088,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15092,7 +15116,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Collection DTO for task history." }, "relations": [], @@ -15108,7 +15132,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15133,7 +15159,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15159,7 +15187,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "DTO for cross-environment database ID mapping." }, "relations": [], @@ -15175,7 +15203,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15200,7 +15230,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15226,7 +15258,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Wrapper for database mappings." }, "relations": [], @@ -15242,7 +15274,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15267,7 +15301,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15293,7 +15329,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for dashboard migration requests." }, "relations": [], @@ -15309,7 +15345,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15334,7 +15372,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15360,7 +15400,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for dashboard backup requests." }, "relations": [], @@ -15376,7 +15416,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -15401,7 +15443,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -15427,27 +15471,12 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints." }, "relations": [], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:DatasetReviewDependencies:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.\n# @LAYER: API\n\nfrom __future__ import annotations\n\nimport json\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Union, cast\n\nfrom fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n has_permission,\n)\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ApprovalState,\n ArtifactFormat,\n CandidateStatus,\n ClarificationSession,\n DatasetReviewSession,\n ExecutionMapping,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SemanticCandidate,\n SemanticFieldEntry,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationAnswerDto,\n ClarificationQuestionDto,\n ClarificationSessionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationEngine,\n ClarificationQuestionPayload,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\nlog = MarkerLogger(\"DatasetReviewDeps\")\n\n\n# [DEF:StartSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# [/DEF:StartSessionRequest:Class]\n\n\n# [DEF:UpdateSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: Optional[str] = None\n\n\n# [/DEF:UpdateSessionRequest:Class]\n\n\n# [DEF:SessionCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: List[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# [/DEF:SessionCollectionResponse:Class]\n\n\n# [DEF:ExportArtifactResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: Optional[str] = None\n content: Dict[str, Any]\n\n\n# [/DEF:ExportArtifactResponse:Class]\n\n\n# [DEF:FieldSemanticUpdateRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: Optional[str] = None\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n lock_field: bool = False\n resolution_note: Optional[str] = None\n\n\n# [/DEF:FieldSemanticUpdateRequest:Class]\n\n\n# [DEF:FeedbackRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# [/DEF:FeedbackRequest:Class]\n\n\n# [DEF:ClarificationAnswerRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n\n\n# [/DEF:ClarificationAnswerRequest:Class]\n\n\n# [DEF:ClarificationSessionSummaryResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n\n\n# [/DEF:ClarificationSessionSummaryResponse:Class]\n\n\n# [DEF:ClarificationStateResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: Optional[ClarificationSessionSummaryResponse] = None\n current_question: Optional[ClarificationQuestionDto] = None\n\n\n# [/DEF:ClarificationStateResponse:Class]\n\n\n# [DEF:ClarificationAnswerResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: List[ValidationFindingDto]\n\n\n# [/DEF:ClarificationAnswerResultResponse:Class]\n\n\n# [DEF:FeedbackResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# [/DEF:FeedbackResponse:Class]\n\n\n# [DEF:ApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: Optional[str] = None\n\n\n# [/DEF:ApproveMappingRequest:Class]\n\n\n# [DEF:BatchApproveSemanticItemRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# [/DEF:BatchApproveSemanticItemRequest:Class]\n\n\n# [DEF:BatchApproveSemanticRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# [/DEF:BatchApproveSemanticRequest:Class]\n\n\n# [DEF:BatchApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: List[str] = Field(..., min_length=1)\n approval_note: Optional[str] = None\n\n\n# [/DEF:BatchApproveMappingRequest:Class]\n\n\n# [DEF:PreviewEnqueueResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: Optional[int] = None\n preview_status: str\n task_id: Optional[str] = None\n\n\n# [/DEF:PreviewEnqueueResultResponse:Class]\n\n\n# [DEF:MappingCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: List[ExecutionMappingDto]\n\n\n# [/DEF:MappingCollectionResponse:Class]\n\n\n# [DEF:UpdateExecutionMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Optional[Any] = None\n mapping_method: Optional[str] = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: Optional[str] = None\n\n\n# [/DEF:UpdateExecutionMappingRequest:Class]\n\n\n# [DEF:LaunchDatasetResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# [/DEF:LaunchDatasetResponse:Class]\n\n\n# --- Dependency Injection ---\n\n# [DEF:_require_auto_review_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US1 dataset review endpoints behind the configured feature flag.\ndef _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_auto_review_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_auto_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset auto review feature is disabled\")\n return True\n\n\n# [/DEF:_require_auto_review_flag:Function]\n\n\n# [DEF:_require_clarification_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard clarification-specific US2 endpoints behind the configured feature flag.\ndef _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_clarification_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_clarification:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset clarification feature is disabled\")\n return True\n\n\n# [/DEF:_require_clarification_flag:Function]\n\n\n# [DEF:_require_execution_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US3 execution endpoints behind the configured feature flag.\ndef _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_execution_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_execution:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset execution feature is disabled\")\n return True\n\n\n# [/DEF:_require_execution_flag:Function]\n\n\n# [DEF:_get_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# [/DEF:_get_repository:Function]\n\n\n# [DEF:_get_orchestrator:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# [/DEF:_get_orchestrator:Function]\n\n\n# [DEF:_get_clarification_engine:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# [/DEF:_get_clarification_engine:Function]\n\n\n# --- Serialization Helpers ---\n\n# [DEF:_serialize_session_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# [/DEF:_serialize_session_summary:Function]\n\n\n# [DEF:_serialize_session_detail:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# [/DEF:_serialize_session_detail:Function]\n\n\n# [DEF:_require_session_version_header:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# [/DEF:_require_session_version_header:Function]\n\n\n# [DEF:_build_session_version_conflict_http_exception:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# [/DEF:_build_session_version_conflict_http_exception:Function]\n\n\n# [DEF:_enforce_session_version:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.\ndef _enforce_session_version(repository, session, expected_version):\n with belief_scope(\"_enforce_session_version\"):\n try:\n repository.require_session_version(session, expected_version)\n except DatasetReviewSessionVersionConflictError as exc:\n log.explore(\"Dataset review optimistic-lock conflict detected\", payload={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version}, error=\"Optimistic lock conflict\")\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_enforce_session_version:Function]\n\n\n# [DEF:_get_owned_session_or_404:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve one session for current user or collaborator scope, returning 404 when inaccessible.\ndef _get_owned_session_or_404(repository, session_id, current_user):\n with belief_scope(\"_get_owned_session_or_404\"):\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Session not found\")\n return session\n\n\n# [/DEF:_get_owned_session_or_404:Function]\n\n\n# [DEF:_require_owner_mutation_scope:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Enforce owner-only mutation scope.\ndef _require_owner_mutation_scope(session, current_user):\n with belief_scope(\"_require_owner_mutation_scope\"):\n if session.user_id != current_user.id:\n raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Only the owner can mutate dataset review state\")\n return session\n\n\n# [/DEF:_require_owner_mutation_scope:Function]\n\n\n# [DEF:_prepare_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve owner-scoped mutation session and enforce optimistic-lock version.\ndef _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):\n with belief_scope(\"_prepare_owned_session_mutation\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n _require_owner_mutation_scope(session, current_user)\n return _enforce_session_version(repository, session, expected_version)\n\n\n# [/DEF:_prepare_owned_session_mutation:Function]\n\n\n# [DEF:_commit_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Centralize session version bumping and commit semantics.\ndef _commit_owned_session_mutation(repository, session, *, refresh_targets=None):\n with belief_scope(\"_commit_owned_session_mutation\"):\n try:\n repository.commit_session_mutation(session, refresh_targets=refresh_targets)\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_commit_owned_session_mutation:Function]\n\n\n# [DEF:_record_session_event:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# [/DEF:_record_session_event:Function]\n\n\n# [DEF:_serialize_semantic_field:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_semantic_field:Function]\n\n\n# [DEF:_serialize_execution_mapping:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_execution_mapping:Function]\n\n\n# [DEF:_serialize_preview:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_preview:Function]\n\n\n# [DEF:_serialize_run_context:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_run_context:Function]\n\n\n# [DEF:_serialize_clarification_question_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# [/DEF:_serialize_clarification_question_payload:Function]\n\n\n# [DEF:_serialize_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# [/DEF:_serialize_clarification_state:Function]\n\n\n# [DEF:_serialize_empty_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# [/DEF:_serialize_empty_clarification_state:Function]\n\n\n# [DEF:_get_latest_clarification_session_or_404:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# [/DEF:_get_latest_clarification_session_or_404:Function]\n\n\n# [DEF:_get_owned_mapping_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve one execution mapping inside one owned session.\ndef _get_owned_mapping_or_404(session, mapping_id):\n for mapping in session.execution_mappings:\n if mapping.mapping_id == mapping_id:\n return mapping\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Execution mapping not found\")\n\n\n# [/DEF:_get_owned_mapping_or_404:Function]\n\n\n# [DEF:_get_owned_field_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve a semantic field inside one owned session.\ndef _get_owned_field_or_404(session, field_id):\n for field in session.semantic_fields:\n if field.field_id == field_id:\n return field\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic field not found\")\n\n\n# [/DEF:_get_owned_field_or_404:Function]\n\n\n# [DEF:_map_candidate_provenance:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# [/DEF:_map_candidate_provenance:Function]\n\n\n# [DEF:_resolve_candidate_source_version:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# [/DEF:_resolve_candidate_source_version:Function]\n\n\n# [DEF:_update_semantic_field_state:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply field-level semantic manual override or candidate acceptance.\n# @POST: Manual overrides always set manual provenance plus lock.\ndef _update_semantic_field_state(field, request, changed_by):\n has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])\n selected_candidate = None\n if request.candidate_id:\n selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)\n if selected_candidate is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic candidate not found\")\n\n if has_manual_override:\n field.verbose_name = request.verbose_name\n field.description = request.description\n field.display_format = request.display_format\n field.provenance = FieldProvenance.MANUAL_OVERRIDE\n field.source_id = None\n field.source_version = None\n field.confidence_rank = None\n field.is_locked = True\n field.has_conflict = False\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.SUPERSEDED\n return field\n\n if selected_candidate is not None:\n field.verbose_name = selected_candidate.proposed_verbose_name\n field.description = selected_candidate.proposed_description\n field.display_format = selected_candidate.proposed_display_format\n field.provenance = _map_candidate_provenance(selected_candidate)\n field.source_id = selected_candidate.source_id\n field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)\n field.confidence_rank = selected_candidate.candidate_rank\n field.is_locked = bool(request.lock_field or field.is_locked)\n field.has_conflict = len(field.candidates) > 1\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED\n return field\n\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Provide candidate_id or at least one manual override field\")\n\n\n# [/DEF:_update_semantic_field_state:Function]\n\n\n# [DEF:_build_sql_lab_redirect_url:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# [/DEF:_build_sql_lab_redirect_url:Function]\n\n\n# [DEF:_build_documentation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce session documentation export content.\ndef _build_documentation_export(session, export_format):\n profile = session.profile\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Dataset Review: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Environment: {session.environment_id}\", f\"- Readiness: {session.readiness_state.value}\", f\"- Recommended action: {session.recommended_action.value}\", \"\", \"## Business Summary\", profile.business_summary if profile else \"No profile summary available.\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- [{f.severity.value}] {f.title}: {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session\": _serialize_session_summary(session).model_dump(mode=\"json\"), \"profile\": profile and {\"dataset_name\": profile.dataset_name, \"business_summary\": profile.business_summary, \"confidence_state\": profile.confidence_state.value, \"dataset_type\": profile.dataset_type}, \"findings\": [{\"code\": f.code, \"severity\": f.severity.value, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.json\", \"content\": content}\n\n\n# [/DEF:_build_documentation_export:Function]\n\n\n# [DEF:_build_validation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce validation-focused export content.\ndef _build_validation_export(session, export_format):\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Validation Report: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Readiness: {session.readiness_state.value}\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- `{f.code}` [{f.severity.value}] {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session_id\": session.session_id, \"dataset_ref\": session.dataset_ref, \"readiness_state\": session.readiness_state.value, \"findings\": [{\"finding_id\": f.finding_id, \"area\": f.area.value, \"severity\": f.severity.value, \"code\": f.code, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.json\", \"content\": content}\n\n\n# [/DEF:_build_validation_export:Function]\n\n\n# [/DEF:DatasetReviewDependencies:Module]\n" }, @@ -15460,11 +15489,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for starting one dataset review session." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:StartSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# [/DEF:StartSessionRequest:Class]\n" }, @@ -15477,11 +15516,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for lifecycle state updates on an existing session." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:UpdateSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: Optional[str] = None\n\n\n# [/DEF:UpdateSessionRequest:Class]\n" }, @@ -15494,11 +15543,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Paginated session collection response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SessionCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: List[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# [/DEF:SessionCollectionResponse:Class]\n" }, @@ -15511,11 +15570,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Inline export response for documentation or validation outputs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ExportArtifactResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: Optional[str] = None\n content: Dict[str, Any]\n\n\n# [/DEF:ExportArtifactResponse:Class]\n" }, @@ -15528,11 +15597,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for field-level semantic candidate acceptance or manual override." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FieldSemanticUpdateRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: Optional[str] = None\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n lock_field: bool = False\n resolution_note: Optional[str] = None\n\n\n# [/DEF:FieldSemanticUpdateRequest:Class]\n" }, @@ -15545,11 +15624,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for thumbs up/down feedback." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FeedbackRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# [/DEF:FeedbackRequest:Class]\n" }, @@ -15562,11 +15651,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for submitting one clarification answer." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationAnswerRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n\n\n# [/DEF:ClarificationAnswerRequest:Class]\n" }, @@ -15579,11 +15678,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Summary DTO for current clarification session state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationSessionSummaryResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n\n\n# [/DEF:ClarificationSessionSummaryResponse:Class]\n" }, @@ -15596,11 +15705,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Response DTO for current clarification state and active question payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationStateResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: Optional[ClarificationSessionSummaryResponse] = None\n current_question: Optional[ClarificationQuestionDto] = None\n\n\n# [/DEF:ClarificationStateResponse:Class]\n" }, @@ -15613,11 +15732,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Response DTO for one clarification answer mutation result." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationAnswerResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: List[ValidationFindingDto]\n\n\n# [/DEF:ClarificationAnswerResultResponse:Class]\n" }, @@ -15630,11 +15759,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Minimal response DTO for persisted AI feedback actions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FeedbackResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# [/DEF:FeedbackResponse:Class]\n" }, @@ -15647,11 +15786,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Optional request DTO for explicit mapping approval audit notes." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: Optional[str] = None\n\n\n# [/DEF:ApproveMappingRequest:Class]\n" }, @@ -15664,11 +15813,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for one batch semantic-approval item." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BatchApproveSemanticItemRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# [/DEF:BatchApproveSemanticItemRequest:Class]\n" }, @@ -15681,11 +15840,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for explicit batch semantic approvals." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BatchApproveSemanticRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# [/DEF:BatchApproveSemanticRequest:Class]\n" }, @@ -15698,11 +15867,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for explicit batch mapping approvals." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BatchApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: List[str] = Field(..., min_length=1)\n approval_note: Optional[str] = None\n\n\n# [/DEF:BatchApproveMappingRequest:Class]\n" }, @@ -15715,11 +15894,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Async preview trigger response exposing only enqueue state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PreviewEnqueueResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: Optional[int] = None\n preview_status: str\n task_id: Optional[str] = None\n\n\n# [/DEF:PreviewEnqueueResultResponse:Class]\n" }, @@ -15732,11 +15921,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Wrapper for execution mapping list responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MappingCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: List[ExecutionMappingDto]\n\n\n# [/DEF:MappingCollectionResponse:Class]\n" }, @@ -15749,11 +15948,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for one manual execution-mapping override update." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:UpdateExecutionMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Optional[Any] = None\n mapping_method: Optional[str] = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: Optional[str] = None\n\n\n# [/DEF:UpdateExecutionMappingRequest:Class]\n" }, @@ -15766,11 +15975,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Launch result exposing audited run context and SQL Lab redirect target." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LaunchDatasetResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# [/DEF:LaunchDatasetResponse:Class]\n" }, @@ -15783,7 +16002,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Guard US1 dataset review endpoints behind the configured feature flag." }, "relations": [], @@ -15800,7 +16019,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Guard clarification-specific US2 endpoints behind the configured feature flag." }, "relations": [], @@ -15817,7 +16036,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Guard US3 execution endpoints behind the configured feature flag." }, "relations": [], @@ -15834,11 +16053,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Build repository dependency." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_get_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# [/DEF:_get_repository:Function]\n" }, @@ -15851,11 +16080,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Build orchestrator dependency." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_get_orchestrator:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# [/DEF:_get_orchestrator:Function]\n" }, @@ -15868,11 +16107,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Build clarification engine dependency." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_get_clarification_engine:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# [/DEF:_get_clarification_engine:Function]\n" }, @@ -15885,11 +16134,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map session aggregate into stable API summary DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_session_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# [/DEF:_serialize_session_summary:Function]\n" }, @@ -15902,11 +16161,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map session aggregate into stable API detail DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_session_detail:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# [/DEF:_serialize_session_detail:Function]\n" }, @@ -15919,11 +16188,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Read the optimistic-lock session version header." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_require_session_version_header:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# [/DEF:_require_session_version_header:Function]\n" }, @@ -15936,11 +16215,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Normalize optimistic-lock conflict errors into HTTP 409 responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_build_session_version_conflict_http_exception:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# [/DEF:_build_session_version_conflict_http_exception:Function]\n" }, @@ -15953,7 +16242,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses." }, "relations": [], @@ -15980,7 +16269,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Resolve one session for current user or collaborator scope, returning 404 when inaccessible." }, "relations": [], @@ -16007,7 +16296,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Enforce owner-only mutation scope." }, "relations": [], @@ -16024,7 +16313,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Resolve owner-scoped mutation session and enforce optimistic-lock version." }, "relations": [], @@ -16051,7 +16340,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Centralize session version bumping and commit semantics." }, "relations": [], @@ -16078,11 +16367,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Persist one explicit audit event for an owned mutation endpoint." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_record_session_event:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# [/DEF:_record_session_event:Function]\n" }, @@ -16095,11 +16394,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map one semantic field into stable DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_semantic_field:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_semantic_field:Function]\n" }, @@ -16112,11 +16421,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map one execution mapping into stable DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_execution_mapping:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_execution_mapping:Function]\n" }, @@ -16129,11 +16448,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map one preview into stable DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_preview:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_preview:Function]\n" }, @@ -16146,11 +16475,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Map one run context into stable DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_run_context:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_run_context:Function]\n" }, @@ -16163,11 +16502,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Convert clarification engine payload into API DTO." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_clarification_question_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# [/DEF:_serialize_clarification_question_payload:Function]\n" }, @@ -16180,11 +16529,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Convert clarification engine state into API response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# [/DEF:_serialize_clarification_state:Function]\n" }, @@ -16197,11 +16556,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Return empty clarification payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_serialize_empty_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# [/DEF:_serialize_empty_clarification_state:Function]\n" }, @@ -16214,11 +16583,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Resolve the latest clarification aggregate or raise." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_get_latest_clarification_session_or_404:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# [/DEF:_get_latest_clarification_session_or_404:Function]\n" }, @@ -16231,7 +16610,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve one execution mapping inside one owned session." }, "relations": [], @@ -16248,7 +16627,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve a semantic field inside one owned session." }, "relations": [], @@ -16265,11 +16644,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Translate accepted semantic candidate type into stable field provenance." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_map_candidate_provenance:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# [/DEF:_map_candidate_provenance:Function]\n" }, @@ -16282,11 +16671,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Resolve the semantic source version for one accepted candidate." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_resolve_candidate_source_version:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# [/DEF:_resolve_candidate_source_version:Function]\n" }, @@ -16299,7 +16698,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Manual overrides always set manual provenance plus lock.", "PURPOSE": "Apply field-level semantic manual override or candidate acceptance." }, @@ -16336,11 +16735,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Build SQL Lab redirect URL." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_build_sql_lab_redirect_url:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# [/DEF:_build_sql_lab_redirect_url:Function]\n" }, @@ -16353,7 +16762,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Produce session documentation export content." }, "relations": [], @@ -16370,7 +16779,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Produce validation-focused export content." }, "relations": [], @@ -16387,20 +16796,11 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist thumbs up/down feedback for clarification question/answer content." }, "relations": [], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -16423,7 +16823,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "List resumable dataset review sessions for the current user." }, "relations": [], @@ -16450,7 +16850,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Start a new dataset review session from a Superset link or dataset selection." }, "relations": [], @@ -16504,7 +16904,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Return the full accessible dataset review session aggregate." }, "relations": [], @@ -16531,7 +16931,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Update resumable lifecycle status for an owned session." }, "relations": [], @@ -16558,7 +16958,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Archive or hard-delete a session owned by the current user." }, "relations": [], @@ -16585,7 +16985,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Export documentation output for the current session." }, "relations": [], @@ -16612,7 +17012,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Export validation findings for the current session." }, "relations": [], @@ -16639,7 +17039,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Return the current clarification session summary and active question payload." }, "relations": [], @@ -16666,7 +17066,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Resume clarification mode on the highest-priority unresolved question." }, "relations": [], @@ -16693,7 +17093,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist one clarification answer before advancing the active pointer." }, "relations": [], @@ -16720,7 +17120,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Apply one field-level semantic candidate decision or manual override." }, "relations": [], @@ -16747,7 +17147,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Lock one semantic field against later automatic overwrite." }, "relations": [], @@ -16774,7 +17174,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Unlock one semantic field so later automated candidate application may replace it." }, "relations": [], @@ -16801,7 +17201,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Approve multiple semantic candidate decisions in one batch." }, "relations": [], @@ -16828,7 +17228,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Return the current mapping-review set for one accessible session." }, "relations": [], @@ -16855,7 +17255,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist one owner-authorized execution-mapping effective value override." }, "relations": [], @@ -16882,7 +17282,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Explicitly approve a warning-sensitive mapping transformation." }, "relations": [], @@ -16909,7 +17309,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Approve multiple warning-sensitive execution mappings in one batch." }, "relations": [], @@ -16936,7 +17336,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Trigger Superset-side preview compilation for the current owned execution context." }, "relations": [], @@ -16963,7 +17363,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Execute the current owned session launch handoff and return audited SQL Lab run context." }, "relations": [], @@ -16990,7 +17390,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist thumbs up/down feedback for AI-assisted semantic field content." }, "relations": [], @@ -17017,7 +17417,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Persist thumbs up/down feedback for clarification question/answer content." }, "relations": [], @@ -17044,7 +17444,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All dataset responses include last_task metadata", "LAYER": "API", "PURPOSE": "API endpoints for the Dataset Hub - listing datasets with mapping progress", @@ -17084,20 +17484,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -17112,7 +17498,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for dataset mapping progress statistics" }, "relations": [], @@ -17128,7 +17514,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17153,7 +17541,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17179,7 +17569,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for the most recent task associated with a dataset" }, "relations": [], @@ -17195,7 +17585,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17220,7 +17612,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17246,7 +17640,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Summary DTO for a dataset in the hub listing" }, "relations": [], @@ -17262,7 +17656,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17287,7 +17683,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17313,7 +17711,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for a dashboard linked to a dataset" }, "relations": [], @@ -17329,7 +17727,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17354,7 +17754,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17380,7 +17782,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "DTO for a single dataset column's metadata" }, "relations": [], @@ -17396,7 +17798,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17421,7 +17825,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17447,7 +17853,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Detailed DTO for a dataset including columns and links" }, "relations": [], @@ -17463,7 +17869,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17488,7 +17896,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17514,7 +17924,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Paginated response DTO for dataset listings" }, "relations": [], @@ -17530,7 +17940,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17555,7 +17967,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17581,7 +17995,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Response DTO containing a task ID for tracking" }, "relations": [], @@ -17597,7 +18011,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17622,7 +18038,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17648,7 +18066,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "search (Optional[str]) - Filter by table name", "POST": "Returns a list of all dataset IDs", "PRE": "env_id must be a valid environment ID", @@ -17707,7 +18125,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "page_size (Optional[int]) - Items per page (default: 10, max: 100)", "POST": "Response includes pagination metadata (page, page_size, total, total_pages)", "PRE": "page_size must be between 1 and 100 if provided", @@ -17766,7 +18184,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for initiating column mapping" }, "relations": [], @@ -17782,7 +18200,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17807,7 +18227,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17833,7 +18255,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "request (MapColumnsRequest) - Mapping request with environment and dataset IDs", "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", @@ -17898,7 +18320,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Request DTO for initiating documentation generation" }, "relations": [], @@ -17914,7 +18336,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -17939,7 +18363,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -17965,7 +18391,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "request (GenerateDocsRequest) - Documentation generation request", "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", @@ -18030,7 +18456,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "dataset_id (int) - The dataset ID", "POST": "Returns detailed dataset info with columns and linked dashboards", "PRE": "dataset_id is a valid dataset ID", @@ -18089,7 +18515,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Environment IDs must exist in the configuration.", "LAYER": "API", "PURPOSE": "API endpoints for listing environments and their databases.", @@ -18123,20 +18549,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -18174,6 +18586,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -18251,7 +18672,9 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -18264,20 +18687,6 @@ "contract_type": "Function" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "unknown_tag", "tag": "PARAM", @@ -18302,6 +18711,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -18309,7 +18727,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -18357,7 +18778,9 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -18370,20 +18793,6 @@ "contract_type": "Function" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "unknown_tag", "tag": "PARAM", @@ -18408,6 +18817,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -18421,7 +18839,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -18447,7 +18868,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Package root for decomposed git routes. Re-exports all public symbols from submodules.", "SEMANTICS": [ @@ -18465,20 +18886,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -18491,6 +18898,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -18509,7 +18917,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "FastAPI endpoints for Git server configuration CRUD and connection testing.", "SEMANTICS": [ @@ -18520,22 +18928,7 @@ ] }, "relations": [], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:GitConfigRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoints for Git server configuration CRUD and connection testing.\n# @LAYER: API\n# @SEMANTICS: git, config, routes, crud\n\nfrom typing import List\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import has_permission\nfrom src.models.git import GitServerConfig\n\nfrom src.api.routes.git_schemas import (\n GitServerConfigCreate,\n GitServerConfigSchema,\n GitServerConfigUpdate,\n)\n\nfrom ._router import router\nfrom ._helpers import _get_git_config_or_404\nfrom ._deps import get_git_service\n\n\n# [DEF:get_git_configs:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all configured Git servers.\n@router.get(\"/config\", response_model=List[GitServerConfigSchema])\nasync def get_git_configs(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"get_git_configs\"):\n configs = db.query(GitServerConfig).all()\n result = []\n for config in configs:\n schema = GitServerConfigSchema.from_orm(config)\n schema.pat = \"********\"\n result.append(schema)\n return result\n# [/DEF:get_git_configs:Function]\n\n\n# [DEF:create_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Register a new Git server configuration.\n@router.post(\"/config\", response_model=GitServerConfigSchema)\nasync def create_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_git_config\"):\n config_dict = config.dict(exclude={\"config_id\"})\n db_config = GitServerConfig(**config_dict)\n db.add(db_config)\n db.commit()\n db.refresh(db_config)\n return db_config\n# [/DEF:create_git_config:Function]\n\n\n# [DEF:update_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Update an existing Git server configuration.\n@router.put(\"/config/{config_id}\", response_model=GitServerConfigSchema)\nasync def update_git_config(\n config_id: str,\n config_update: GitServerConfigUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n update_data = config_update.dict(exclude_unset=True)\n if update_data.get(\"pat\") == \"********\":\n update_data.pop(\"pat\")\n\n for key, value in update_data.items():\n setattr(db_config, key, value)\n\n db.commit()\n db.refresh(db_config)\n\n result_schema = GitServerConfigSchema.from_orm(db_config)\n result_schema.pat = \"********\"\n return result_schema\n# [/DEF:update_git_config:Function]\n\n\n# [DEF:delete_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Remove a Git server configuration.\n@router.delete(\"/config/{config_id}\")\nasync def delete_git_config(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n db.delete(db_config)\n db.commit()\n return {\"status\": \"success\", \"message\": \"Configuration deleted\"}\n# [/DEF:delete_git_config:Function]\n\n\n# [DEF:test_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Validate connection to a Git server using provided credentials.\n@router.post(\"/config/test\")\nasync def test_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"test_git_config\"):\n _git_service = get_git_service()\n pat_to_use = config.pat\n if pat_to_use == \"********\":\n if config.config_id:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == config.config_id)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n else:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.url == config.url)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n\n success = await _git_service.test_connection(\n config.provider, config.url, pat_to_use\n )\n if success:\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n else:\n raise HTTPException(status_code=400, detail=\"Connection failed\")\n# [/DEF:test_git_config:Function]\n# [/DEF:GitConfigRoutes:Module]\n" }, @@ -18548,7 +18941,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "List all configured Git servers." }, "relations": [], @@ -18565,7 +18958,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Register a new Git server configuration." }, "relations": [], @@ -18582,7 +18975,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Update an existing Git server configuration." }, "relations": [], @@ -18599,7 +18992,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Remove a Git server configuration." }, "relations": [], @@ -18616,7 +19009,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Validate connection to a Git server using provided credentials." }, "relations": [], @@ -18633,7 +19026,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "get_git_service() resolves from sys.modules at call time so test monkeypatching", "LAYER": "API", "PURPOSE": "Shared dependency wiring for monkeypatch-safe git_service access and constants.", @@ -18655,17 +19048,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -18681,7 +19069,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "FastAPI endpoint for listing deployment environments.", "SEMANTICS": [ @@ -18691,22 +19079,7 @@ ] }, "relations": [], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:GitEnvironmentRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoint for listing deployment environments.\n# @LAYER: API\n# @SEMANTICS: git, environments, routes\n\nfrom typing import List\n\nfrom fastapi import Depends\n\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom src.api.routes.git_schemas import DeploymentEnvironmentSchema\n\nfrom ._router import router\n\n\n# [DEF:get_environments:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all deployment environments.\n@router.get(\"/environments\", response_model=List[DeploymentEnvironmentSchema])\nasync def get_environments(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"environments\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n envs = config_manager.get_environments()\n return [\n DeploymentEnvironmentSchema(\n id=e.id, name=e.name, superset_url=e.url, is_active=True\n )\n for e in envs\n ]\n# [/DEF:get_environments:Function]\n# [/DEF:GitEnvironmentRoutes:Module]\n" }, @@ -18719,7 +19092,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "API", "PURPOSE": "FastAPI endpoints for Gitea-specific repository operations.", "SEMANTICS": [ @@ -18730,22 +19103,7 @@ ] }, "relations": [], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:GitGiteaRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoints for Gitea-specific repository operations.\n# @LAYER: API\n# @SEMANTICS: git, gitea, routes, repositories\n\nfrom typing import List\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import has_permission\nfrom src.models.git import GitProvider\n\nfrom src.api.routes.git_schemas import (\n GiteaRepoCreateRequest,\n GiteaRepoSchema,\n RemoteRepoCreateRequest,\n RemoteRepoSchema,\n)\n\nfrom ._router import router\nfrom ._helpers import _get_git_config_or_404\nfrom ._deps import get_git_service\n\n\n# [DEF:list_gitea_repositories:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List repositories in Gitea for a saved Gitea config.\n@router.get(\"/config/{config_id}/gitea/repos\", response_model=List[GiteaRepoSchema])\nasync def list_gitea_repositories(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n _gs = get_git_service()\n with belief_scope(\"list_gitea_repositories\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repos = await _gs.list_gitea_repositories(config.url, config.pat)\n return [\n GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n for repo in repos\n ]\n# [/DEF:list_gitea_repositories:Function]\n\n\n# [DEF:create_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create a repository in Gitea for a saved Gitea config.\n@router.post(\"/config/{config_id}/gitea/repos\", response_model=GiteaRepoSchema)\nasync def create_gitea_repository(\n config_id: str,\n request: GiteaRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n return GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_gitea_repository:Function]\n\n\n# [DEF:delete_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Delete repository in Gitea for a saved Gitea config.\n@router.delete(\"/config/{config_id}/gitea/repos/{owner}/{repo_name}\")\nasync def delete_gitea_repository(\n config_id: str,\n owner: str,\n repo_name: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"delete_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n await _gs.delete_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n owner=owner,\n repo_name=repo_name,\n )\n return {\"status\": \"success\", \"message\": \"Repository deleted\"}\n# [/DEF:delete_gitea_repository:Function]\n\n\n# [DEF:create_remote_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create repository on remote Git server using selected provider config.\n@router.post(\"/config/{config_id}/repositories\", response_model=RemoteRepoSchema)\nasync def create_remote_repository(\n config_id: str,\n request: RemoteRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_remote_repository\"):\n config = _get_git_config_or_404(db, config_id)\n\n if config.provider == GitProvider.GITEA:\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITHUB:\n repo = await _gs.create_github_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITLAB:\n repo = await _gs.create_gitlab_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} is not supported\",\n )\n\n return RemoteRepoSchema(\n provider=config.provider,\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", repo.get(\"name\", \"\")),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_remote_repository:Function]\n# [/DEF:GitGiteaRoutes:Module]\n" }, @@ -18758,7 +19116,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "List repositories in Gitea for a saved Gitea config." }, "relations": [], @@ -18775,7 +19133,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Create a repository in Gitea for a saved Gitea config." }, "relations": [], @@ -18792,7 +19150,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Delete repository in Gitea for a saved Gitea config." }, "relations": [], @@ -18809,7 +19167,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Create repository on remote Git server using selected provider config." }, "relations": [], @@ -18826,7 +19184,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "Shared helper functions for Git route modules.", "SEMANTICS": [ @@ -18857,20 +19215,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -18883,6 +19227,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -18900,6 +19245,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -18917,6 +19263,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -18935,7 +19282,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns a stable payload compatible with frontend repository status parsing.", "PURPOSE": "Build a consistent status payload for dashboards without initialized repositories." }, @@ -18949,6 +19296,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -18963,7 +19319,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Raises HTTPException(500) with route-specific context.", "PRE": "`error` is a non-HTTPException instance.", "PURPOSE": "Convert unexpected route-level exceptions to stable 500 API responses." @@ -18987,6 +19343,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -19001,7 +19366,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns standard status payload or `NO_REPO` payload when repository path is absent.", "PRE": "`dashboard_id` is a valid integer.", "PURPOSE": "Resolve repository status for one dashboard with graceful NO_REPO semantics." @@ -19039,7 +19404,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve GitServerConfig by id or raise 404." }, "relations": [], @@ -19056,7 +19421,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve repository folder key with slug-first strategy and deterministic fallback." }, "relations": [], @@ -19073,11 +19438,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Normalize optional identity value into trimmed string or None." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:_sanitize_optional_identity_value:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optional identity value into trimmed string or None.\ndef _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# [/DEF:_sanitize_optional_identity_value:Function]\n" }, @@ -19090,7 +19465,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Resolve configured Git username/email from current user's profile preferences." }, "relations": [], @@ -19107,7 +19482,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Apply user-scoped Git identity to repository-local config before write/pull operations." }, "relations": [], @@ -19124,7 +19499,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).", "SEMANTICS": [ @@ -19136,20 +19511,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -19172,7 +19533,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Return unfinished-merge status for repository (web-only recovery support)." }, "relations": [], @@ -19189,7 +19550,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Return conflicted files with mine/theirs previews for web conflict resolver." }, "relations": [], @@ -19206,7 +19567,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Apply mine/theirs/manual conflict resolutions from WebUI and stage files." }, "relations": [ @@ -19230,7 +19591,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Abort unfinished merge from WebUI flow." }, "relations": [], @@ -19247,7 +19608,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Finalize unfinished merge from WebUI flow." }, "relations": [ @@ -19271,7 +19632,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).", "SEMANTICS": [ @@ -19284,20 +19645,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -19320,7 +19667,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Sync dashboard state from Superset to Git using the GitPlugin." }, "relations": [ @@ -19344,7 +19691,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Promote changes between branches via MR or direct merge." }, "relations": [ @@ -19368,7 +19715,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Deploy dashboard from Git to a target environment." }, "relations": [ @@ -19392,7 +19739,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).", "SEMANTICS": [ @@ -19407,20 +19754,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -19443,7 +19776,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Stage and commit changes in the dashboard's repository." }, "relations": [], @@ -19460,7 +19793,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Push local commits to the remote repository." }, "relations": [], @@ -19477,7 +19810,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Pull changes from the remote repository." }, "relations": [ @@ -19501,7 +19834,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Get current Git status for a dashboard repository." }, "relations": [], @@ -19518,7 +19851,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Get Git statuses for multiple dashboard repositories in one request." }, "relations": [], @@ -19535,7 +19868,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Get Git diff for a dashboard repository." }, "relations": [], @@ -19552,7 +19885,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Generate a suggested commit message using LLM." }, "relations": [ @@ -19576,7 +19909,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "API", "PURPOSE": "FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).", "SEMANTICS": [ @@ -19589,20 +19922,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -19625,7 +19944,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Link a dashboard to a Git repository and perform initial clone/init." }, "relations": [ @@ -19649,7 +19968,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Return repository binding with provider metadata for selected dashboard." }, "relations": [], @@ -19666,7 +19985,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Delete local repository workspace and DB binding for selected dashboard." }, "relations": [], @@ -19683,7 +20002,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "List all branches for a dashboard's repository." }, "relations": [], @@ -19700,7 +20019,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Create a new branch in the dashboard's repository." }, "relations": [], @@ -19717,7 +20036,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Switch the dashboard's repository to a specific branch." }, "relations": [], @@ -19734,7 +20053,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "API", "PURPOSE": "Shared APIRouter for all Git route modules.", "SEMANTICS": [ @@ -19747,17 +20066,12 @@ "relations": [], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } } ], @@ -19773,7 +20087,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "All schemas must be compatible with the FastAPI router.", "LAYER": "API", "PURPOSE": "Defines Pydantic models for the Git integration API layer.", @@ -19804,17 +20118,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -19839,11 +20148,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Base schema for Git server configuration attributes." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServerConfigBase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base schema for Git server configuration attributes.\nclass GitServerConfigBase(BaseModel):\n name: str = Field(..., description=\"Display name for the Git server\")\n provider: GitProvider = Field(..., description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str = Field(..., description=\"Server base URL\")\n pat: str = Field(..., description=\"Personal Access Token\")\n pat: str = Field(..., description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(\"main\", description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigBase:Class]\n" }, @@ -19859,7 +20178,17 @@ "PURPOSE": "Schema for updating an existing Git server configuration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServerConfigUpdate:Class]\n# @PURPOSE: Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: Optional[str] = Field(None, description=\"Display name for the Git server\")\n provider: Optional[GitProvider] = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: Optional[str] = Field(None, description=\"Server base URL\")\n pat: Optional[str] = Field(None, description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(None, description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigUpdate:Class]\n" }, @@ -19875,7 +20204,17 @@ "PURPOSE": "Schema for creating a new Git server configuration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServerConfigCreate:Class]\n# @PURPOSE: Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: Optional[str] = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# [/DEF:GitServerConfigCreate:Class]\n" }, @@ -19891,7 +20230,17 @@ "PURPOSE": "Schema for representing a Git server configuration with metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServerConfigSchema:Class]\n# @PURPOSE: Schema for representing a Git server configuration with metadata.\nclass GitServerConfigSchema(GitServerConfigBase):\n \"\"\"Schema for representing a Git server configuration with metadata.\"\"\"\n id: str\n status: GitStatus\n last_validated: datetime\n\n class Config:\n from_attributes = True\n# [/DEF:GitServerConfigSchema:Class]\n" }, @@ -19907,7 +20256,17 @@ "PURPOSE": "Schema for tracking a local Git repository linked to a dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitRepositorySchema:Class]\n# @PURPOSE: Schema for tracking a local Git repository linked to a dashboard.\nclass GitRepositorySchema(BaseModel):\n \"\"\"Schema for tracking a local Git repository linked to a dashboard.\"\"\"\n id: str\n dashboard_id: int\n config_id: str\n remote_url: str\n local_path: str\n current_branch: str\n sync_status: SyncStatus\n\n class Config:\n from_attributes = True\n# [/DEF:GitRepositorySchema:Class]\n" }, @@ -19923,7 +20282,17 @@ "PURPOSE": "Schema for representing a Git branch metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BranchSchema:Class]\n# @PURPOSE: Schema for representing a Git branch metadata.\nclass BranchSchema(BaseModel):\n \"\"\"Schema for representing a Git branch.\"\"\"\n name: str\n commit_hash: str\n is_remote: bool\n last_updated: datetime\n# [/DEF:BranchSchema:Class]\n" }, @@ -19939,7 +20308,17 @@ "PURPOSE": "Schema for representing Git commit details." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CommitSchema:Class]\n# @PURPOSE: Schema for representing Git commit details.\nclass CommitSchema(BaseModel):\n \"\"\"Schema for representing a Git commit.\"\"\"\n hash: str\n author: str\n email: str\n timestamp: datetime\n message: str\n files_changed: List[str]\n# [/DEF:CommitSchema:Class]\n" }, @@ -19955,7 +20334,17 @@ "PURPOSE": "Schema for branch creation requests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BranchCreate:Class]\n# @PURPOSE: Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# [/DEF:BranchCreate:Class]\n" }, @@ -19971,7 +20360,17 @@ "PURPOSE": "Schema for branch checkout requests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BranchCheckout:Class]\n# @PURPOSE: Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# [/DEF:BranchCheckout:Class]\n" }, @@ -19987,7 +20386,17 @@ "PURPOSE": "Schema for staging and committing changes." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CommitCreate:Class]\n# @PURPOSE: Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: List[str]\n# [/DEF:CommitCreate:Class]\n" }, @@ -20003,7 +20412,17 @@ "PURPOSE": "Schema for resolving merge conflicts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ConflictResolution:Class]\n# @PURPOSE: Schema for resolving merge conflicts.\nclass ConflictResolution(BaseModel):\n \"\"\"Schema for resolving merge conflicts.\"\"\"\n file_path: str\n resolution: str = Field(pattern=\"^(mine|theirs|manual)$\")\n content: Optional[str] = None\n# [/DEF:ConflictResolution:Class]\n" }, @@ -20019,7 +20438,17 @@ "PURPOSE": "Schema representing unfinished merge status for repository." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MergeStatusSchema:Class]\n# @PURPOSE: Schema representing unfinished merge status for repository.\nclass MergeStatusSchema(BaseModel):\n has_unfinished_merge: bool\n repository_path: str\n git_dir: str\n current_branch: str\n merge_head: Optional[str] = None\n merge_message_preview: Optional[str] = None\n conflicts_count: int = 0\n# [/DEF:MergeStatusSchema:Class]\n" }, @@ -20035,7 +20464,17 @@ "PURPOSE": "Schema describing one conflicted file with optional side snapshots." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MergeConflictFileSchema:Class]\n# @PURPOSE: Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: Optional[str] = None\n theirs: Optional[str] = None\n# [/DEF:MergeConflictFileSchema:Class]\n" }, @@ -20051,7 +20490,17 @@ "PURPOSE": "Request schema for resolving one or multiple merge conflicts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MergeResolveRequest:Class]\n# @PURPOSE: Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: List[ConflictResolution] = Field(default_factory=list)\n# [/DEF:MergeResolveRequest:Class]\n" }, @@ -20067,7 +20516,17 @@ "PURPOSE": "Request schema for finishing merge with optional explicit commit message." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MergeContinueRequest:Class]\n# @PURPOSE: Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: Optional[str] = None\n# [/DEF:MergeContinueRequest:Class]\n" }, @@ -20083,7 +20542,17 @@ "PURPOSE": "Schema for representing a target deployment environment." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DeploymentEnvironmentSchema:Class]\n# @PURPOSE: Schema for representing a target deployment environment.\nclass DeploymentEnvironmentSchema(BaseModel):\n \"\"\"Schema for representing a target deployment environment.\"\"\"\n id: str\n name: str\n superset_url: str\n is_active: bool\n\n class Config:\n from_attributes = True\n# [/DEF:DeploymentEnvironmentSchema:Class]\n" }, @@ -20099,7 +20568,17 @@ "PURPOSE": "Schema for dashboard deployment requests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DeployRequest:Class]\n# @PURPOSE: Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# [/DEF:DeployRequest:Class]\n" }, @@ -20115,7 +20594,17 @@ "PURPOSE": "Schema for repository initialization requests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RepoInitRequest:Class]\n# @PURPOSE: Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# [/DEF:RepoInitRequest:Class]\n" }, @@ -20131,7 +20620,17 @@ "PURPOSE": "Schema describing repository-to-config binding and provider metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RepositoryBindingSchema:Class]\n# @PURPOSE: Schema describing repository-to-config binding and provider metadata.\nclass RepositoryBindingSchema(BaseModel):\n dashboard_id: int\n config_id: str\n provider: GitProvider\n remote_url: str\n local_path: str\n# [/DEF:RepositoryBindingSchema:Class]\n" }, @@ -20147,7 +20646,17 @@ "PURPOSE": "Schema for requesting repository statuses for multiple dashboards in a single call." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RepoStatusBatchRequest:Class]\n# @PURPOSE: Schema for requesting repository statuses for multiple dashboards in a single call.\nclass RepoStatusBatchRequest(BaseModel):\n dashboard_ids: List[int] = Field(default_factory=list, description=\"Dashboard IDs to resolve repository statuses for\")\n# [/DEF:RepoStatusBatchRequest:Class]\n" }, @@ -20163,7 +20672,17 @@ "PURPOSE": "Schema for returning repository statuses keyed by dashboard ID." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RepoStatusBatchResponse:Class]\n# @PURPOSE: Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: Dict[str, Dict[str, Any]]\n# [/DEF:RepoStatusBatchResponse:Class]\n" }, @@ -20179,7 +20698,17 @@ "PURPOSE": "Schema describing a Gitea repository." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GiteaRepoSchema:Class]\n# @PURPOSE: Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:GiteaRepoSchema:Class]\n" }, @@ -20195,7 +20724,17 @@ "PURPOSE": "Request schema for creating a Gitea repository." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GiteaRepoCreateRequest:Class]\n# @PURPOSE: Request schema for creating a Gitea repository.\nclass GiteaRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:GiteaRepoCreateRequest:Class]\n" }, @@ -20211,7 +20750,17 @@ "PURPOSE": "Provider-agnostic remote repository payload." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RemoteRepoSchema:Class]\n# @PURPOSE: Provider-agnostic remote repository payload.\nclass RemoteRepoSchema(BaseModel):\n provider: GitProvider\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:RemoteRepoSchema:Class]\n" }, @@ -20227,7 +20776,17 @@ "PURPOSE": "Provider-agnostic repository creation request." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RemoteRepoCreateRequest:Class]\n# @PURPOSE: Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:RemoteRepoCreateRequest:Class]\n" }, @@ -20243,7 +20802,17 @@ "PURPOSE": "Request schema for branch promotion workflow." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PromoteRequest:Class]\n# @PURPOSE: Request schema for branch promotion workflow.\nclass PromoteRequest(BaseModel):\n from_branch: str = Field(..., min_length=1, max_length=255)\n to_branch: str = Field(..., min_length=1, max_length=255)\n mode: str = Field(default=\"mr\", pattern=\"^(mr|direct)$\")\n title: Optional[str] = None\n description: Optional[str] = None\n reason: Optional[str] = None\n draft: bool = False\n remove_source_branch: bool = False\n# [/DEF:PromoteRequest:Class]\n" }, @@ -20259,7 +20828,17 @@ "PURPOSE": "Response schema for promotion operation result." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PromoteResponse:Class]\n# @PURPOSE: Response schema for promotion operation result.\nclass PromoteResponse(BaseModel):\n mode: str\n from_branch: str\n to_branch: str\n status: str\n url: Optional[str] = None\n reference_id: Optional[str] = None\n policy_violation: bool = False\n# [/DEF:PromoteResponse:Class]\n" }, @@ -20272,7 +20851,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI/API", "PURPOSE": "API endpoints for dashboard health monitoring and status aggregation.", "SEMANTICS": [ @@ -20289,22 +20868,7 @@ "target_ref": "health_service" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI/API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI/API" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:health_router:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: health, monitoring, dashboards\n# @PURPOSE: API endpoints for dashboard health monitoring and status aggregation.\n# @LAYER: UI/API\n# @RELATION: DEPENDS_ON -> health_service\n\nfrom fastapi import APIRouter, Depends, Query, HTTPException, status\nfrom typing import List, Optional\nfrom sqlalchemy.orm import Session\nfrom ...core.database import get_db\nfrom ...services.health_service import HealthService\nfrom ...schemas.health import HealthSummaryResponse\nfrom ...dependencies import has_permission, get_config_manager, get_task_manager\n\nrouter = APIRouter(prefix=\"/api/health\", tags=[\"Health\"])\n\n# [DEF:get_health_summary:Function]\n# @PURPOSE: Get aggregated health status for all dashboards.\n# @PRE: Caller has read permission for dashboard health view.\n# @POST: Returns HealthSummaryResponse.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: Optional[str] = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# [/DEF:get_health_summary:Function]\n\n\n# [DEF:delete_health_report:Function]\n# @PURPOSE: Delete one persisted dashboard validation report from health summary.\n# @PRE: Caller has write permission for tasks/report maintenance.\n# @POST: Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# [/DEF:delete_health_report:Function]\n\n# [/DEF:health_router:Module]\n" }, @@ -20348,6 +20912,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20401,6 +20974,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20423,7 +21005,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (API)", "PURPOSE": "API routes for LLM provider configuration and management.", "SEMANTICS": [ @@ -20458,22 +21040,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:LlmRoutes:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, routes, llm\n# @PURPOSE: API routes for LLM provider configuration and management.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n# @RELATION: DEPENDS_ON -> [get_current_user]\n# @RELATION: DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import List, Optional\nimport httpx\nfrom ...core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"LlmRoutes\")\nfrom ...schemas.auth import User\nfrom ...dependencies import get_current_user as get_current_active_user\nfrom ...plugins.llm_analysis.models import (\n LLMProviderConfig,\n LLMProviderType,\n FetchModelsRequest,\n FetchModelsResponse,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...core.database import get_db\nfrom sqlalchemy.orm import Session\n\n# [DEF:router:Global]\n# @PURPOSE: APIRouter instance for LLM routes.\nrouter = APIRouter(tags=[\"LLM\"])\n# [/DEF:router:Global]\n\n\n# [DEF:_is_valid_runtime_api_key:Function]\n# @PURPOSE: Validate decrypted runtime API key presence/shape.\n# @PRE: value can be None.\n# @POST: Returns True only for non-placeholder key.\n# @RELATION: BINDS_TO -> [LlmRoutes]\ndef _is_valid_runtime_api_key(value: Optional[str]) -> bool:\n key = (value or \"\").strip()\n if not key:\n return False\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return False\n return len(key) >= 16\n\n\n# [/DEF:_is_valid_runtime_api_key:Function]\n\n\n# [DEF:_build_provider_auth_headers:Function]\n# @PURPOSE: Build auth headers for provider model listing based on provider type.\n# @PRE: provider_type is a valid LLMProviderType.\n# @POST: Returns dict of HTTP headers for the provider's model list API.\ndef _build_provider_auth_headers(api_key: Optional[str], provider_type: LLMProviderType) -> dict:\n headers = {}\n if not api_key:\n return headers\n if provider_type == LLMProviderType.KILO:\n headers[\"Authentication\"] = f\"Bearer {api_key}\"\n headers[\"X-API-Key\"] = api_key\n else:\n # OpenAI, OpenRouter — standard Bearer token\n headers[\"Authorization\"] = f\"Bearer {api_key}\"\n return headers\n\n\n# [/DEF:_build_provider_auth_headers:Function]\n\n\n# [DEF:get_providers:Function]\n# @PURPOSE: Retrieve all LLM provider configurations.\n# @PRE: User is authenticated.\n# @POST: Returns list of LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.get(\"/providers\", response_model=List[LLMProviderConfig])\nasync def get_providers(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n \"\"\"\n Get all LLM provider configurations.\n \"\"\"\n log.reason(\n f\"Fetching providers for user: {current_user.username}\"\n )\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n return [\n LLMProviderConfig(\n id=p.id,\n provider_type=LLMProviderType(p.provider_type),\n name=p.name,\n base_url=p.base_url,\n api_key=\"********\",\n default_model=p.default_model,\n is_active=p.is_active,\n )\n for p in providers\n ]\n\n\n# [/DEF:get_providers:Function]\n\n\n# [DEF:get_llm_status:Function]\n# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.\n# @PRE: User is authenticated.\n# @POST: configured=true only when an active provider with valid decrypted key exists.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: CALLS -> [_is_valid_runtime_api_key]\n@router.get(\"/status\")\nasync def get_llm_status(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n active_provider = next((p for p in providers if p.is_active), None)\n\n if not providers:\n return {\n \"configured\": False,\n \"reason\": \"no_providers_configured\",\n \"provider_count\": 0,\n \"active_provider_count\": 0,\n }\n\n if not active_provider:\n return {\n \"configured\": False,\n \"reason\": \"no_active_provider\",\n \"provider_count\": len(providers),\n \"active_provider_count\": 0,\n \"providers\": [\n {\n \"id\": provider.id,\n \"name\": provider.name,\n \"provider_type\": provider.provider_type,\n \"is_active\": bool(provider.is_active),\n }\n for provider in providers\n ],\n }\n\n api_key = service.get_decrypted_api_key(active_provider.id)\n if not _is_valid_runtime_api_key(api_key):\n return {\n \"configured\": False,\n \"reason\": \"invalid_api_key\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n return {\n \"configured\": True,\n \"reason\": \"ok\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n\n# [/DEF:get_llm_status:Function]\n\n\n# [DEF:create_provider:Function]\n# @PURPOSE: Create a new LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the created LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\n \"/providers\", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED\n)\nasync def create_provider(\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Create a new LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.create_provider(config)\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:create_provider:Function]\n\n\n# [DEF:update_provider:Function]\n# @PURPOSE: Update an existing LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the updated LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.put(\"/providers/{provider_id}\", response_model=LLMProviderConfig)\nasync def update_provider(\n provider_id: str,\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Update an existing LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.update_provider(provider_id, config)\n if not provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:update_provider:Function]\n\n\n# [DEF:delete_provider:Function]\n# @PURPOSE: Delete an LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns success status.\n# @RELATION: CALLS -> [LLMProviderService]\n@router.delete(\"/providers/{provider_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_provider(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Delete an LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n if not service.delete_provider(provider_id):\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n return\n\n\n# [/DEF:delete_provider:Function]\n\n\n# [DEF:fetch_models_for_provider:Function]\n# @PURPOSE: Fetch available models from a provider's API endpoint.\n# @PRE: valid base_url and optional api_key/provider_id.\n# @POST: Returns list of model IDs sorted alphabetically.\n# @SIDE_EFFECT: Makes HTTP GET to multiple possible model endpoints.\n@router.post(\"/providers/fetch-models\", response_model=FetchModelsResponse)\nasync def fetch_models_for_provider(\n request: FetchModelsRequest,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Fetch available models from a provider's models endpoint.\n\n Resolves API key in priority order:\n 1. Direct api_key from request body\n 2. Decrypted api_key from DB via provider_id (for existing providers)\n 3. No auth (for local providers without auth)\n\n Tries multiple paths in order:\n 1. {base_url}/models (OpenAI-compatible, stripped of /v1)\n 2. {base_url}/v1/models (OpenAI-compatible, with /v1)\n 3. {base_url}/api/tags (Ollama format)\n Returns empty list if none respond — user can enter model name manually.\n \"\"\"\n log.reason(\n f\"Fetching models for {request.base_url}\"\n )\n\n # Resolve API key: prefer direct, fall back to stored key via provider_id\n api_key = request.api_key\n if not api_key and request.provider_id:\n try:\n svc = LLMProviderService(db)\n api_key = svc.get_decrypted_api_key(request.provider_id)\n except Exception:\n log.explore(f\"Could not resolve API key for provider {request.provider_id}\", error=\"API key resolution failed\")\n\n base = request.base_url.rstrip(\"/\")\n\n # Collect possible model list endpoints\n candidates = []\n\n # If base ends with /v1, try both /models (after stripping /v1) and /v1/models\n if base.endswith(\"/v1\"):\n candidates.append(base[:-3] + \"/models\") # base without /v1 + /models\n candidates.append(base + \"/models\") # base with /v1 + /models (e.g. /v1/models)\n else:\n candidates.append(base + \"/models\") # /models\n candidates.append(base + \"/v1/models\") # /v1/models\n\n candidates.append(base + \"/api/tags\") # Ollama format\n\n headers = _build_provider_auth_headers(api_key, request.provider_type)\n last_error = None\n\n try:\n async with httpx.AsyncClient(timeout=10.0) as client:\n for url in candidates:\n try:\n response = await client.get(url, headers=headers)\n\n if response.status_code != 200:\n if response.status_code != 404:\n last_error = f\"HTTP {response.status_code} from {url}: {response.text[:200]}\"\n continue\n\n body = response.json()\n raw = body.get(\"data\") if isinstance(body, dict) else body\n\n # Try OpenAI format: [{\"id\": \"model-name\", ...}, ...] or list of strings\n if isinstance(raw, list):\n models = sorted(\n m.get(\"id\", str(m)) if isinstance(m, dict) else str(m)\n for m in raw\n )\n if models:\n log.reason(\n f\"Found {len(models)} models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n # Try Ollama format: {\"models\": [{\"name\": \"model-name\", ...}, ...]}\n if isinstance(body, dict):\n models_list = body.get(\"models\", [])\n if isinstance(models_list, list):\n models = sorted(\n m.get(\"name\", str(m)) if isinstance(m, dict) else str(m)\n for m in models_list\n )\n if models:\n log.reason(\n f\"Found {len(models)} Ollama models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n except httpx.TimeoutException:\n last_error = f\"Timeout at {url}\"\n except httpx.ConnectError:\n last_error = f\"Cannot connect at {url}\"\n except Exception as e:\n last_error = f\"Error at {url}: {str(e)[:100]}\"\n\n # All endpoints failed — return empty list with no error (user can type manually)\n log.explore(f\"No model endpoint responded for {request.base_url}. Last error: {last_error}\", error=f\"No model endpoint responded for {request.base_url}\")\n return FetchModelsResponse(models=[], total=0)\n\n except Exception as e:\n log.explore(f\"Unexpected error for {request.base_url}: {e}\", error=str(e))\n return FetchModelsResponse(models=[], total=0)\n\n\n# [/DEF:fetch_models_for_provider:Function]\n\n\n# [DEF:test_connection:Function]\n# @PURPOSE: Test connection to an LLM provider.\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMClient]\n@router.post(\"/providers/{provider_id}/test\")\nasync def test_connection(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n log.reason(\n f\"Testing connection for provider_id: {provider_id}\"\n )\n \"\"\"\n Test connection to an LLM provider.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n api_key = service.get_decrypted_api_key(provider_id)\n\n # Check if API key was successfully decrypted\n if not api_key:\n log.explore(f\"Failed to decrypt API key for provider {provider_id}\", error=\"API key decryption failed\")\n raise HTTPException(\n status_code=500,\n detail=\"Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key.\",\n )\n\n client = LLMClient(\n provider_type=LLMProviderType(db_provider.provider_type),\n api_key=api_key,\n base_url=db_provider.base_url,\n default_model=db_provider.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_connection:Function]\n\n\n# [DEF:test_provider_config:Function]\n# @PURPOSE: Test connection with a provided configuration (not yet saved).\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: DEPENDS_ON -> [LLMClient]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\"/providers/test\")\nasync def test_provider_config(\n config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)\n):\n \"\"\"\n Test connection with a provided configuration.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n log.reason(\n f\"Testing config for {config.name}\"\n )\n\n # Check if API key is provided\n if not config.api_key or config.api_key == \"********\":\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_provider_config:Function]\n\n# [/DEF:LlmRoutes:Module]\n" }, @@ -20502,7 +21069,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -20559,6 +21128,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20604,6 +21182,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -20655,6 +21242,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20714,6 +21310,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20773,6 +21378,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20832,6 +21446,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20885,6 +21508,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -20932,6 +21564,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -20991,6 +21632,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -21050,6 +21700,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -21072,7 +21731,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Mappings are persisted in the SQLite database.", "LAYER": "API", "PURPOSE": "API endpoints for managing database mappings and getting suggestions.", @@ -21112,20 +21771,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -21205,6 +21850,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -21242,6 +21896,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -21279,6 +21942,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -21293,7 +21965,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "[DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]", "INVARIANT": "Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.", "LAYER": "Infra", @@ -21401,16 +22073,43 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_not_for_contract_type", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -21425,7 +22124,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[DashboardSelection] -> Output[Dict[str, str]]", "INVARIANT": "Migration task dispatch never occurs before source and target environment ids pass guard validation.", "POST": "Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.", @@ -21447,7 +22146,26 @@ "target_ref": "[DashboardSelection]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:execute_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Validate migration selection and enqueue asynchronous migration task execution.\n# @PRE: DashboardSelection payload is valid and both source/target environments exist.\n# @POST: Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.\n# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]\n# @RELATION: CALLS ->[create_task]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.\n@router.post(\"/migration/execute\")\nasync def execute_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"execute_migration\"):\n logger.reason(\n f\"Initiating migration from {selection.source_env_id} to {selection.target_env_id}\"\n )\n\n # Validate environments exist\n environments = config_manager.get_environments()\n env_ids = {e.id for e in environments}\n\n if (\n selection.source_env_id not in env_ids\n or selection.target_env_id not in env_ids\n ):\n logger.explore(\n \"Invalid environment selection\",\n extra={\n \"source\": selection.source_env_id,\n \"target\": selection.target_env_id,\n },\n )\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n # Include replace_db_config and fix_cross_filters in the task parameters\n task_params = selection.dict()\n task_params[\"replace_db_config\"] = selection.replace_db_config\n task_params[\"fix_cross_filters\"] = selection.fix_cross_filters\n\n logger.reason(\n f\"Creating migration task with {len(selection.selected_ids)} dashboards\"\n )\n\n try:\n task = await task_manager.create_task(\"superset-migration\", task_params)\n logger.reflect(f\"Migration task created: {task.id}\")\n return {\"task_id\": task.id, \"message\": \"Migration initiated\"}\n except Exception as e:\n logger.explore(f\"Task creation failed: {e}\")\n raise HTTPException(\n status_code=500, detail=f\"Failed to create migration task: {str(e)}\"\n )\n\n\n# [/DEF:execute_migration:Function]\n" }, @@ -21460,7 +22178,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[DashboardSelection] -> Output[Dict[str, Any]]", "INVARIANT": "Dry-run flow remains read-only and rejects identical source/target environments before service execution.", "POST": "Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.", @@ -21482,7 +22200,26 @@ "target_ref": "[MigrationDryRunService]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:dry_run_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Build pre-flight migration diff and risk summary without mutating target systems.\n# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.\n# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.\n# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @RELATION: DEPENDS_ON ->[MigrationDryRunService]\n# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.\n@router.post(\"/migration/dry-run\", response_model=Dict[str, Any])\nasync def dry_run_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"dry_run_migration\"):\n logger.reason(\n f\"Starting dry run: {selection.source_env_id} -> {selection.target_env_id}\"\n )\n\n environments = config_manager.get_environments()\n env_map = {env.id: env for env in environments}\n source_env = env_map.get(selection.source_env_id)\n target_env = env_map.get(selection.target_env_id)\n\n if not source_env or not target_env:\n logger.explore(\"Invalid environment selection for dry run\")\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n if selection.source_env_id == selection.target_env_id:\n logger.explore(\"Source and target environments are identical\")\n raise HTTPException(\n status_code=400,\n detail=\"Source and target environments must be different\",\n )\n\n if not selection.selected_ids:\n logger.explore(\"No dashboards selected for dry run\")\n raise HTTPException(\n status_code=400, detail=\"No dashboards selected for dry run\"\n )\n\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n\n try:\n result = service.run(\n selection=selection,\n source_client=source_client,\n target_client=target_client,\n db=db,\n )\n logger.reflect(\"Dry run analysis complete\")\n return result\n except ValueError as exc:\n logger.explore(f\"Dry run orchestrator failed: {exc}\")\n raise HTTPException(status_code=500, detail=str(exc)) from exc\n\n\n# [/DEF:dry_run_migration:Function]\n" }, @@ -21495,7 +22232,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[None] -> Output[Dict[str, str]]", "POST": "Returns {\"cron\": str} reflecting current persisted settings value.", "PRE": "Configuration store is available and requester has READ permission.", @@ -21560,7 +22297,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[Dict[str, str]] -> Output[Dict[str, str]]", "POST": "Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.", "PRE": "Payload includes \"cron\" key and requester has WRITE permission.", @@ -21625,7 +22362,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[QueryParams] -> Output[Dict[str, Any]]", "POST": "Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.", "PRE": "skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.", @@ -21690,7 +22427,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[None] -> Output[Dict[str, Any]]", "POST": "Returns sync summary with synced/failed counts after attempting all environments.", "PRE": "At least one environment is configured and requester has EXECUTE permission.", @@ -21761,7 +22498,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (API)", "PURPOSE": "Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.", "SEMANTICS": [ @@ -21791,22 +22528,7 @@ "target_ref": "[API_Routes]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:PluginsRouter:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, router, plugins, list\n# @PURPOSE: Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [PluginConfig]\n# @RELATION: DEPENDS_ON -> [get_plugin_loader]\n# @RELATION: BINDS_TO -> [API_Routes]\nfrom typing import List\nfrom fastapi import APIRouter, Depends\n\nfrom ...core.plugin_base import PluginConfig\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...core.logger import belief_scope\n\nrouter = APIRouter()\n\n\n# [DEF:list_plugins:Function]\n# @PURPOSE: Retrieve a list of all available plugins.\n# @PRE: plugin_loader is injected via Depends.\n# @POST: Returns a list of PluginConfig objects.\n# @RETURN: List[PluginConfig] - List of registered plugins.\n# @RELATION: CALLS -> [get_plugin_loader]\n# @RELATION: DEPENDS_ON -> [PluginConfig]\n@router.get(\"\", response_model=List[PluginConfig])\nasync def list_plugins(\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"plugins\", \"READ\")),\n):\n with belief_scope(\"list_plugins\"):\n \"\"\"\n Retrieve a list of all available plugins.\n \"\"\"\n return plugin_loader.get_all_plugin_configs()\n\n\n# [/DEF:list_plugins:Function]\n# [/DEF:PluginsRouter:Module]\n" }, @@ -21857,6 +22579,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -21885,7 +22616,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Endpoints are self-scoped and never mutate another user preference.", "LAYER": "API", "PURPOSE": "Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.", @@ -21931,36 +22662,64 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -21974,6 +22733,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -21991,6 +22751,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22008,6 +22769,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22057,6 +22819,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -22110,6 +22881,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -22163,6 +22943,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -22216,6 +23005,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -22238,7 +23036,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "[ReportQuery] -> [ReportCollection | ReportDetailView]", "INVARIANT": "Endpoints are read-only and do not trigger long-running tasks.", "LAYER": "UI (API)", @@ -22283,17 +23081,21 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -22308,6 +23110,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22325,6 +23128,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22342,6 +23146,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22359,6 +23164,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22377,7 +23183,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PARAM": "field_name (str) - Query field name for diagnostics.", "POST": "Returns enum list or raises HTTP 400 with deterministic machine-readable payload.", "PRE": "raw may be None/empty or comma-separated values.", @@ -22417,6 +23223,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -22445,7 +23260,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "deterministic error payload for invalid filters.", "PRE": "authenticated/authorized request and validated query params.", "PURPOSE": "Return paginated unified reports list.", @@ -22511,6 +23326,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -22528,6 +23344,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22545,6 +23362,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22563,7 +23381,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "returns normalized detail envelope or 404 when report is not found.", "PRE": "authenticated/authorized request and existing report_id.", "PURPOSE": "Return one normalized report detail with diagnostics and next actions." @@ -22617,7 +23435,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All settings changes must be persisted via ConfigManager.", "LAYER": "API", "PUBLIC_API": "router", @@ -22659,20 +23477,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "unknown_tag", "tag": "PUBLIC_API", @@ -22691,6 +23495,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22708,6 +23513,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22725,6 +23531,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -22743,7 +23550,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Response model for logging configuration with current task log level.", "SEMANTICS": [ "logging", @@ -22753,6 +23560,15 @@ }, "relations": [], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -22760,7 +23576,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -22786,7 +23605,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Raises on auth/API failures; returns None on success.", "PRE": "env contains valid URL and credentials.", "PURPOSE": "Run lightweight Superset connectivity validation without full pagination scan." @@ -22824,7 +23643,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns masked AppConfig.", "PRE": "Config manager is available.", "PURPOSE": "Retrieves all application settings.", @@ -22869,7 +23688,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns dict with dataset_review and health_monitor booleans.", "PRE": "Config manager is available.", "PURPOSE": "Public endpoint returning feature flags for frontend sidebar filtering.", @@ -22894,6 +23713,24 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -22908,7 +23745,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Retrieves storage-specific settings.", "RETURN": "StorageConfig - The storage configuration." }, @@ -22933,7 +23770,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "storage (StorageConfig) - The new storage settings.", "POST": "Storage settings are updated and saved.", "PURPOSE": "Updates storage-specific settings.", @@ -22975,7 +23812,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "id (str) - The ID of the environment to test.", "POST": "Returns success or error status.", "PRE": "ID is provided.", @@ -23027,7 +23864,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns logging configuration.", "PRE": "Config manager is available.", "PURPOSE": "Retrieves current logging configuration.", @@ -23072,7 +23909,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "config (LoggingConfig) - The new logging configuration.", "POST": "Logging configuration is updated and saved.", "PRE": "New logging config is provided.", @@ -23124,11 +23961,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Response model for consolidated application settings." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ConsolidatedSettingsResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response model for consolidated application settings.\nclass ConsolidatedSettingsResponse(BaseModel):\n environments: List[dict]\n connections: List[dict]\n llm: dict\n llm_providers: List[dict]\n logging: dict\n storage: dict\n notifications: dict = {}\n features: dict = {}\n\n\n# [/DEF:ConsolidatedSettingsResponse:Class]\n" }, @@ -23141,7 +23988,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]", "POST": "Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.", "PRE": "Config manager is available and the caller holds admin settings read permission.", @@ -23208,6 +24055,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23225,6 +24073,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23242,6 +24091,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23259,6 +24109,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23276,6 +24127,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23293,6 +24145,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -23311,7 +24164,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Settings are updated and saved via ConfigManager.", "PRE": "User has admin permissions, config is valid.", "PURPOSE": "Bulk update application settings from the consolidated view." @@ -23349,7 +24202,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Lists all validation policies.", "RETURN": "List[ValidationPolicyResponse] - List of policies." }, @@ -23374,7 +24227,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "policy (ValidationPolicyCreate) - The policy data.", "PURPOSE": "Creates a new validation policy.", "RETURN": "ValidationPolicyResponse - The created policy." @@ -23406,7 +24259,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "policy (ValidationPolicyUpdate) - The updated policy data.", "PURPOSE": "Updates an existing validation policy.", "RETURN": "ValidationPolicyResponse - The updated policy." @@ -23438,7 +24291,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "id (str) - The ID of the policy to delete.", "PURPOSE": "Deletes a validation policy." }, @@ -23463,7 +24316,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All paths must be validated against path traversal.", "LAYER": "API", "PURPOSE": "API endpoints for file storage management (backups and repositories).", @@ -23499,20 +24352,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } } ], "anchor_syntax": "def", @@ -23527,7 +24366,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "path (Optional[str]) - Subpath within the category.", "POST": "Returns a list of StoredFile objects.", "PRE": "None.", @@ -23586,7 +24425,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "path (str) - Relative path of the item.", "POST": "Item is removed from storage.", "PRE": "category must be a valid FileCategory.", @@ -23655,7 +24494,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "path (str) - Relative path of the file.", "POST": "Returns a FileResponse.", "PRE": "category must be a valid FileCategory.", @@ -23714,7 +24553,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "path (str) - Absolute or storage-root-relative file path.", "POST": "Returns a FileResponse for existing files.", "PRE": "path must resolve under configured storage root.", @@ -23773,7 +24612,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "UI (API)", "PURPOSE": "Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.", "SEMANTICS": [ @@ -23806,22 +24645,7 @@ "target_ref": "[LLMProviderService]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TasksRouter:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, router, tasks, create, list, get, logs\n# @PURPOSE: Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n\n# [SECTION: IMPORTS]\nfrom typing import List, Dict, Any, Optional\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom pydantic import BaseModel\nfrom ...core.logger import belief_scope\n\nfrom ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry\nfrom ...core.task_manager.models import LogFilter, LogStats\nfrom ...dependencies import (\n get_task_manager,\n has_permission,\n get_current_user,\n get_config_manager,\n)\nfrom ...core.config_manager import ConfigManager\nfrom ...services.llm_prompt_templates import (\n is_multimodal_model,\n normalize_llm_settings,\n resolve_bound_provider_id,\n)\n# [/SECTION]\n\nrouter = APIRouter()\n\nTASK_TYPE_PLUGIN_MAP = {\n \"llm_validation\": [\"llm_dashboard_validation\"],\n \"backup\": [\"superset-backup\"],\n \"migration\": [\"superset-migration\"],\n}\n\n\nclass CreateTaskRequest(BaseModel):\n plugin_id: str\n params: Dict[str, Any]\n\n\nclass ResolveTaskRequest(BaseModel):\n resolution_params: Dict[str, Any]\n\n\nclass ResumeTaskRequest(BaseModel):\n passwords: Dict[str, str]\n\n\n# [DEF:create_task:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Create and start a new task for a given plugin.\n# @PARAM: request (CreateTaskRequest) - The request body containing plugin_id and params.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: plugin_id must exist and params must be valid for that plugin.\n# @POST: A new task is created and started.\n# @RETURN: Task - The created task instance.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n@router.post(\"\", response_model=Task, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n request: CreateTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n current_user=Depends(get_current_user),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n # Dynamic permission check based on plugin_id\n has_permission(f\"plugin:{request.plugin_id}\", \"EXECUTE\")(current_user)\n with belief_scope(\"create_task\"):\n try:\n # Special handling for LLM tasks to resolve provider config by task binding.\n if request.plugin_id in {\"llm_dashboard_validation\", \"llm_documentation\"}:\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n try:\n llm_service = LLMProviderService(db)\n provider_id = request.params.get(\"provider_id\")\n if not provider_id:\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n binding_key = (\n \"dashboard_validation\"\n if request.plugin_id == \"llm_dashboard_validation\"\n else \"documentation\"\n )\n provider_id = resolve_bound_provider_id(\n llm_settings, binding_key\n )\n if provider_id:\n request.params[\"provider_id\"] = provider_id\n if not provider_id:\n providers = llm_service.get_all_providers()\n active_provider = next(\n (p for p in providers if p.is_active), None\n )\n if active_provider:\n provider_id = active_provider.id\n request.params[\"provider_id\"] = provider_id\n\n if provider_id:\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n if (\n request.plugin_id == \"llm_dashboard_validation\"\n and not is_multimodal_model(\n db_provider.default_model,\n db_provider.provider_type,\n )\n ):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"Selected provider model is not multimodal for dashboard validation\",\n )\n finally:\n db.close()\n\n task = await task_manager.create_task(\n plugin_id=request.plugin_id, params=request.params\n )\n return task\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n\n\n# [/DEF:create_task:Function]\n\n\n# [DEF:list_tasks:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieve a list of tasks with pagination and optional status filter.\n# @PARAM: limit (int) - Maximum number of tasks to return.\n# @PARAM: offset (int) - Number of tasks to skip.\n# @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RETURN: List[Task] - List of tasks.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=List[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: Optional[TaskStatus] = Query(None, alias=\"status\"),\n task_type: Optional[str] = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: Optional[List[str]] = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# [/DEF:list_tasks:Function]\n\n\n# [DEF:get_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieve the details of a specific task.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns task details or raises 404.\n# @RETURN: Task - The task details.\n# @RELATION: CALLS -> [TaskManager]\n@router.get(\"/{task_id}\", response_model=Task)\nasync def get_task(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_task\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task\n\n\n# [/DEF:get_task:Function]\n\n\n# [DEF:get_task_logs:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Retrieve logs for a specific task with optional filtering.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: level (Optional[str]) - Filter by log level (DEBUG, INFO, WARNING, ERROR).\n# @PARAM: source (Optional[str]) - Filter by source component.\n# @PARAM: search (Optional[str]) - Text search in message.\n# @PARAM: offset (int) - Number of logs to skip.\n# @PARAM: limit (int) - Maximum number of logs to return.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns a list of log entries or raises 404.\n# @RETURN: List[LogEntry] - List of log entries.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [LogFilter]\n# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]\n# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.\n# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json\n# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.\n# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.\n# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.\n# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]\n@router.get(\"/{task_id}/logs\")\nasync def get_task_logs(\n task_id: str,\n level: Optional[str] = Query(\n None, description=\"Filter by log level (DEBUG, INFO, WARNING, ERROR)\"\n ),\n source: Optional[str] = Query(None, description=\"Filter by source component\"),\n search: Optional[str] = Query(None, description=\"Text search in message\"),\n offset: int = Query(0, ge=0, description=\"Number of logs to skip\"),\n limit: int = Query(\n 100, ge=1, le=1000, description=\"Maximum number of logs to return\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_logs\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n\n log_filter = LogFilter(\n level=level.upper() if level else None,\n source=source,\n search=search,\n offset=offset,\n limit=limit,\n )\n return task_manager.get_task_logs(task_id, log_filter)\n\n\n# [/DEF:get_task_logs:Function]\n\n\n# [DEF:get_task_log_stats:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get statistics about logs for a task (counts by level and source).\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns log statistics or raises 404.\n# @RETURN: LogStats - Statistics about task logs.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [LogStats]\n@router.get(\"/{task_id}/logs/stats\", response_model=LogStats)\nasync def get_task_log_stats(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_stats\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n stats_payload = task_manager.get_task_log_stats(task_id)\n if isinstance(stats_payload, LogStats):\n return stats_payload\n if isinstance(stats_payload, dict) and (\n \"total_count\" in stats_payload\n or \"by_level\" in stats_payload\n or \"by_source\" in stats_payload\n ):\n return LogStats(\n total_count=int(stats_payload.get(\"total_count\", 0) or 0),\n by_level=dict(stats_payload.get(\"by_level\") or {}),\n by_source=dict(stats_payload.get(\"by_source\") or {}),\n )\n flat_by_level = (\n dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}\n )\n return LogStats(\n total_count=sum(int(value or 0) for value in flat_by_level.values()),\n by_level={\n str(key): int(value or 0) for key, value in flat_by_level.items()\n },\n by_source={},\n )\n\n\n# [/DEF:get_task_log_stats:Function]\n\n\n# [DEF:get_task_log_sources:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get unique sources for a task's logs.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns list of unique source names or raises 404.\n# @RETURN: List[str] - Unique source names.\n# @RELATION: CALLS -> [TaskManager]\n@router.get(\"/{task_id}/logs/sources\", response_model=List[str])\nasync def get_task_log_sources(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_sources\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task_manager.get_task_log_sources(task_id)\n\n\n# [/DEF:get_task_log_sources:Function]\n\n\n# [DEF:resolve_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve a task that is awaiting mapping.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: request (ResolveTaskRequest) - The resolution parameters.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task must be in AWAITING_MAPPING status.\n# @POST: Task is resolved and resumes execution.\n# @RETURN: Task - The updated task object.\n# @RELATION: CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resolve\", response_model=Task)\nasync def resolve_task(\n task_id: str,\n request: ResolveTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resolve_task\"):\n try:\n await task_manager.resolve_task(task_id, request.resolution_params)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# [/DEF:resolve_task:Function]\n\n\n# [DEF:resume_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resume a task that is awaiting input (e.g., passwords).\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: request (ResumeTaskRequest) - The input (passwords).\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task must be in AWAITING_INPUT status.\n# @POST: Task resumes execution with provided input.\n# @RETURN: Task - The updated task object.\n# @RELATION: CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resume\", response_model=Task)\nasync def resume_task(\n task_id: str,\n request: ResumeTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resume_task\"):\n try:\n task_manager.resume_task_with_password(task_id, request.passwords)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# [/DEF:resume_task:Function]\n\n\n# [DEF:clear_tasks:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Clear tasks matching the status filter.\n# @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_manager is available.\n# @POST: Tasks are removed from memory/persistence.\n# @RELATION: CALLS -> [TaskManager]\n@router.delete(\"\", status_code=status.HTTP_204_NO_CONTENT)\nasync def clear_tasks(\n status: Optional[TaskStatus] = None,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"clear_tasks\", f\"status={status}\"):\n task_manager.clear_tasks(status)\n return\n\n\n# [/DEF:clear_tasks:Function]\n\n# [/DEF:TasksRouter:Module]\n" }, @@ -23834,7 +24658,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "task_manager (TaskManager) - The task manager instance.", "POST": "Returns a list of tasks.", "PRE": "task_manager must be available.", @@ -23908,7 +24732,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PARAM": "task_manager (TaskManager) - The task manager instance.", "POST": "Task resumes execution with provided input.", "PRE": "task must be in AWAITING_INPUT status.", @@ -23976,11 +24800,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Package imports all route modules and re-exports router. Sub-routes registered on shared router.", "PRE": "All sub-modules importable. Router instance available from ._router.", + "PURPOSE": "API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.", "SIDE_EFFECT": "Registers route handlers on shared APIRouter at import time." }, "relations": [ @@ -24027,23 +24851,7 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateRoutes [C:4] [TYPE Module] [SEMANTICS api,routes,translate]\n# @BRIEF API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslateJobResponse]\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @PRE All sub-modules importable. Router instance available from ._router.\n# @POST Package imports all route modules and re-exports router. Sub-routes registered on shared router.\n# @SIDE_EFFECT Registers route handlers on shared APIRouter at import time.\n\n\"\"\"\nTranslate routes package.\n\nRe-exports the shared router and all route handler modules.\nAll sub-modules register their handlers on the router at import time.\n\"\"\"\n\nfrom ._router import router\nfrom . import _job_routes # noqa: F401 — registers job handlers\nfrom . import _run_routes # noqa: F401 — registers run handlers\nfrom . import _run_list_routes # noqa: F401 — registers run list/detail/csv handlers\nfrom . import _preview_routes # noqa: F401 — registers preview handlers\nfrom . import _dictionary_routes # noqa: F401 — registers dictionary handlers\nfrom . import _schedule_routes # noqa: F401 — registers schedule handlers\nfrom . import _metrics_routes # noqa: F401 — registers metrics handlers\nfrom . import _correction_routes # noqa: F401 — registers correction handlers\n\n__all__ = [\n \"router\",\n]\n\n# #endregion TranslateRoutes\n" }, @@ -24056,11 +24864,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Term correction submission endpoints for applying user feedback to dictionary entries.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Term corrections submitted and applied to dictionary with conflict detection.", "PRE": "DB session initialized. User authenticated with translate.dictionary.edit permissions.", + "PURPOSE": "Term correction submission endpoints for applying user feedback to dictionary entries.", "SIDE_EFFECT": "Creates/updates DictionaryEntry records in DB based on correction submissions." }, "relations": [ @@ -24083,23 +24891,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateCorrectionRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,corrections]\n# @BRIEF Term correction submission endpoints for applying user feedback to dictionary entries.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE DB session initialized. User authenticated with translate.dictionary.edit permissions.\n# @POST Term corrections submitted and applied to dictionary with conflict detection.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB based on correction submissions.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.translate import (\n TermCorrectionSubmit,\n TermCorrectionBulkSubmit,\n CorrectionSubmitResponse,\n)\n\nfrom ._router import router\n\nlog = MarkerLogger(\"TranslateCorrectionRoutes\")\n\n# ============================================================\n# Corrections\n# ============================================================\n\n# #region submit_correction [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit a single term correction from a run result review.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST Correction applied with conflict detection. Result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry record in DB; logs correction origin.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n log.reason(\"Correction request\", payload={\"user\": current_user.username})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n\n\n# #region submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit multiple term corrections atomically with full conflict reporting.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST All corrections applied or none with conflict list returned.\n# @SIDE_EFFECT Creates/updates multiple DictionaryEntry records in DB atomically.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n log.reason(\"Bulk correction request\", payload={\"user\": current_user.username})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n\n# #endregion TranslateCorrectionRoutesModule\n" }, @@ -24112,10 +24904,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Submit a single term correction from a run result review.", "COMPLEXITY": 4, "POST": "Correction applied with conflict detection. Result returned.", "PRE": "User has translate.dictionary.edit permission. Dictionary ID is provided.", + "PURPOSE": "Submit a single term correction from a run result review.", "SIDE_EFFECT": "Creates/updates DictionaryEntry record in DB; logs correction origin." }, "relations": [ @@ -24126,23 +24918,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region submit_correction [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit a single term correction from a run result review.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST Correction applied with conflict detection. Result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry record in DB; logs correction origin.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n log.reason(\"Correction request\", payload={\"user\": current_user.username})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n" }, @@ -24155,10 +24931,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Submit multiple term corrections atomically with full conflict reporting.", "COMPLEXITY": 4, "POST": "All corrections applied or none with conflict list returned.", "PRE": "User has translate.dictionary.edit permission. Dictionary ID is provided.", + "PURPOSE": "Submit multiple term corrections atomically with full conflict reporting.", "SIDE_EFFECT": "Creates/updates multiple DictionaryEntry records in DB atomically." }, "relations": [ @@ -24169,23 +24945,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit multiple term corrections atomically with full conflict reporting.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST All corrections applied or none with conflict list returned.\n# @SIDE_EFFECT Creates/updates multiple DictionaryEntry records in DB atomically.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n log.reason(\"Bulk correction request\", payload={\"user\": current_user.username})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n" }, @@ -24198,11 +24958,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Terminology Dictionary CRUD, entries management, and import routes.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Dictionaries and entries CRUD completed. Import executed with conflict resolution.", "PRE": "DB session initialized. User authenticated with translate.dictionary permissions.", + "PURPOSE": "Terminology Dictionary CRUD, entries management, and import routes.", "SIDE_EFFECT": "Reads/writes TerminologyDictionary and DictionaryEntry records to DB." }, "relations": [ @@ -24225,23 +24985,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateDictionaryRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,dictionaries]\n# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE DB session initialized. User authenticated with translate.dictionary permissions.\n# @POST Dictionaries and entries CRUD completed. Import executed with conflict resolution.\n# @SIDE_EFFECT Reads/writes TerminologyDictionary and DictionaryEntry records to DB.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateDictRoutes\")\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.translate import (\n DictionaryCreate,\n DictionaryEntryCreate,\n DictionaryEntryResponse,\n DictionaryImport,\n DictionaryResponse,\n)\n\nfrom ._router import router\nfrom ._helpers import _dict_to_response, _get_dictionary_entry_counts\n\n\n# ============================================================\n# Terminology Dictionaries\n# ============================================================\n\n# #region list_dictionaries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF List all terminology dictionaries with pagination and entry counts.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns paginated list of dictionaries with total count.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n log.reason(\"List dictionaries\", payload={\"user\": current_user.username})\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n log.reflect(\"Dictionaries listed\", payload={\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n\n\n# #region get_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Get a single terminology dictionary by ID with entry count.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns the dictionary with entry_count or 404.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n log.reason(\"Get dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary found\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n\n\n# #region create_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Create a new terminology dictionary.\n# @PRE User has translate.dictionary.create permission. Payload validated.\n# @POST Dictionary created and returned with zero entries.\n# @SIDE_EFFECT Creates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n log.reason(\"Create dictionary\", payload={\"user\": current_user.username})\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n log.reflect(\"Dictionary created\", payload={\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n\n\n# #region update_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Dictionary updated and returned with current entry count.\n# @SIDE_EFFECT Updates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n log.reason(\"Update dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n\n\n# #region delete_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE User has translate.dictionary.delete permission. Dictionary exists and is not in use.\n# @POST Dictionary is deleted or 409 if attached to active jobs.\n# @SIDE_EFFECT Deletes TerminologyDictionary record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n log.reason(\"Delete dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n log.reflect(\"Dictionary deleted\", payload={\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n\n\n# ============================================================\n# Dictionary Entries CRUD\n# ============================================================\n\n# #region list_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF List entries for a dictionary with pagination.\n# @PRE User has translate.dictionary.view permission. Dictionary exists.\n# @POST Returns paginated list of entries.\n# @SIDE_EFFECT Reads DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n log.reason(\"List dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n log.reflect(\"Entries listed\", payload={\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n\n\n# #region add_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Entry created and returned.\n# @SIDE_EFFECT Creates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n log.reason(\"Add dictionary entry\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n\n\n# #region edit_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Update an existing dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry updated and returned.\n# @SIDE_EFFECT Updates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n log.reason(\"Edit dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n\n\n# #region delete_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Delete a dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry is deleted.\n# @SIDE_EFFECT Deletes DictionaryEntry record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n log.reason(\"Delete dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_entry(db, entry_id)\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n\n\n# #region import_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,import]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.\n# @POST Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n log.reason(\"Import dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n log.reflect(\"Import completed\", payload={\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n\n# #endregion TranslateDictionaryRoutesModule\n" }, @@ -24254,10 +24998,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "List all terminology dictionaries with pagination and entry counts.", "COMPLEXITY": 4, "POST": "Returns paginated list of dictionaries with total count.", "PRE": "User has translate.dictionary.view permission.", + "PURPOSE": "List all terminology dictionaries with pagination and entry counts.", "SIDE_EFFECT": "Reads TerminologyDictionary and DictionaryEntry records from DB." }, "relations": [ @@ -24268,23 +25012,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region list_dictionaries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF List all terminology dictionaries with pagination and entry counts.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns paginated list of dictionaries with total count.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n log.reason(\"List dictionaries\", payload={\"user\": current_user.username})\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n log.reflect(\"Dictionaries listed\", payload={\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n" }, @@ -24297,10 +25025,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Get a single terminology dictionary by ID with entry count.", "COMPLEXITY": 4, "POST": "Returns the dictionary with entry_count or 404.", "PRE": "User has translate.dictionary.view permission.", + "PURPOSE": "Get a single terminology dictionary by ID with entry count.", "SIDE_EFFECT": "Reads TerminologyDictionary and DictionaryEntry records from DB." }, "relations": [ @@ -24311,23 +25039,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Get a single terminology dictionary by ID with entry count.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns the dictionary with entry_count or 404.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n log.reason(\"Get dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary found\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n" }, @@ -24340,10 +25052,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Create a new terminology dictionary.", "COMPLEXITY": 4, "POST": "Dictionary created and returned with zero entries.", "PRE": "User has translate.dictionary.create permission. Payload validated.", + "PURPOSE": "Create a new terminology dictionary.", "SIDE_EFFECT": "Creates TerminologyDictionary record in DB." }, "relations": [ @@ -24354,23 +25066,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region create_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Create a new terminology dictionary.\n# @PRE User has translate.dictionary.create permission. Payload validated.\n# @POST Dictionary created and returned with zero entries.\n# @SIDE_EFFECT Creates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n log.reason(\"Create dictionary\", payload={\"user\": current_user.username})\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n log.reflect(\"Dictionary created\", payload={\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n" }, @@ -24383,10 +25079,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Update an existing terminology dictionary.", "COMPLEXITY": 4, "POST": "Dictionary updated and returned with current entry count.", "PRE": "User has translate.dictionary.edit permission. Dictionary exists.", + "PURPOSE": "Update an existing terminology dictionary.", "SIDE_EFFECT": "Updates TerminologyDictionary record in DB." }, "relations": [ @@ -24397,23 +25093,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region update_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Dictionary updated and returned with current entry count.\n# @SIDE_EFFECT Updates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n log.reason(\"Update dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n" }, @@ -24426,10 +25106,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete a terminology dictionary, blocked if attached to active/scheduled jobs.", "COMPLEXITY": 4, "POST": "Dictionary is deleted or 409 if attached to active jobs.", "PRE": "User has translate.dictionary.delete permission. Dictionary exists and is not in use.", + "PURPOSE": "Delete a terminology dictionary, blocked if attached to active/scheduled jobs.", "SIDE_EFFECT": "Deletes TerminologyDictionary record from DB." }, "relations": [ @@ -24440,23 +25120,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region delete_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE User has translate.dictionary.delete permission. Dictionary exists and is not in use.\n# @POST Dictionary is deleted or 409 if attached to active jobs.\n# @SIDE_EFFECT Deletes TerminologyDictionary record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n log.reason(\"Delete dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n log.reflect(\"Dictionary deleted\", payload={\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n" }, @@ -24469,10 +25133,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "List entries for a dictionary with pagination.", "COMPLEXITY": 4, "POST": "Returns paginated list of entries.", "PRE": "User has translate.dictionary.view permission. Dictionary exists.", + "PURPOSE": "List entries for a dictionary with pagination.", "SIDE_EFFECT": "Reads DictionaryEntry records from DB." }, "relations": [ @@ -24483,23 +25147,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region list_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF List entries for a dictionary with pagination.\n# @PRE User has translate.dictionary.view permission. Dictionary exists.\n# @POST Returns paginated list of entries.\n# @SIDE_EFFECT Reads DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n log.reason(\"List dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n log.reflect(\"Entries listed\", payload={\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n" }, @@ -24512,10 +25160,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Add a new entry to a dictionary.", "COMPLEXITY": 4, "POST": "Entry created and returned.", "PRE": "User has translate.dictionary.edit permission. Dictionary exists.", + "PURPOSE": "Add a new entry to a dictionary.", "SIDE_EFFECT": "Creates DictionaryEntry record in DB." }, "relations": [ @@ -24526,23 +25174,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region add_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Entry created and returned.\n# @SIDE_EFFECT Creates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n log.reason(\"Add dictionary entry\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n" }, @@ -24555,10 +25187,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Update an existing dictionary entry.", "COMPLEXITY": 4, "POST": "Entry updated and returned.", "PRE": "User has translate.dictionary.edit permission. Entry exists.", + "PURPOSE": "Update an existing dictionary entry.", "SIDE_EFFECT": "Updates DictionaryEntry record in DB." }, "relations": [ @@ -24569,23 +25201,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region edit_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Update an existing dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry updated and returned.\n# @SIDE_EFFECT Updates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n log.reason(\"Edit dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n" }, @@ -24598,10 +25214,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete a dictionary entry.", "COMPLEXITY": 4, "POST": "Entry is deleted.", "PRE": "User has translate.dictionary.edit permission. Entry exists.", + "PURPOSE": "Delete a dictionary entry.", "SIDE_EFFECT": "Deletes DictionaryEntry record from DB." }, "relations": [ @@ -24612,23 +25228,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region delete_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Delete a dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry is deleted.\n# @SIDE_EFFECT Deletes DictionaryEntry record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n log.reason(\"Delete dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_entry(db, entry_id)\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n" }, @@ -24641,10 +25241,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Import entries into a terminology dictionary from CSV/TSV content.", "COMPLEXITY": 4, "POST": "Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.", "PRE": "User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.", + "PURPOSE": "Import entries into a terminology dictionary from CSV/TSV content.", "SIDE_EFFECT": "Creates/updates DictionaryEntry records in DB." }, "relations": [ @@ -24655,23 +25255,7 @@ "target_ref": "[DictionaryManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region import_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,import]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.\n# @POST Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n log.reason(\"Import dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n log.reflect(\"Import completed\", payload={\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n" }, @@ -24684,28 +25268,12 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Shared helper functions for translate route handlers.", "COMPLEXITY": 2, - "LAYER": "UI" + "LAYER": "UI", + "PURPOSE": "Shared helper functions for translate route handlers." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS api,routes,translate,helpers]\n# @BRIEF Shared helper functions for translate route handlers.\n# @LAYER UI\n\nfrom typing import Any, Dict, List\nfrom sqlalchemy.orm import Session\n\n\n# #region _run_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n\n\n# #region _dict_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n\n\n# #region _get_dictionary_entry_counts [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:\n from sqlalchemy import func\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n\n# #endregion TranslateHelpersModule\n" }, @@ -24718,27 +25286,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Convert TranslationRun ORM to response dict.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Convert TranslationRun ORM to response dict." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _run_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n" }, @@ -24751,27 +25303,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Convert a TerminologyDictionary ORM model to a response dict with computed entry_count." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _dict_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n" }, @@ -24784,27 +25320,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Get entry counts for a list of dictionary IDs.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Get entry counts for a list of dictionary IDs." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region _get_dictionary_entry_counts [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:\n from sqlalchemy import func\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n" }, @@ -24817,11 +25337,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Translation Job CRUD and datasource column routes.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Translation job CRUD completed or datasource columns fetched.", "PRE": "ConfigManager and DB session initialized. User authenticated with translate.job permissions.", + "PURPOSE": "Translation Job CRUD and datasource column routes.", "SIDE_EFFECT": "Reads/writes TranslationJob records to DB; fetches datasource metadata from Superset API." }, "relations": [ @@ -24856,23 +25376,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateJobRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,jobs]\n# @BRIEF Translation Job CRUD and datasource column routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate.job permissions.\n# @POST Translation job CRUD completed or datasource columns fetched.\n# @SIDE_EFFECT Reads/writes TranslationJob records to DB; fetches datasource metadata from Superset API.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateJobRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.service import (\n TranslateJobService,\n job_to_response,\n get_datasource_columns as fetch_datasource_columns_service,\n)\nfrom ....schemas.translate import (\n TranslateJobCreate,\n TranslateJobUpdate,\n TranslateJobResponse,\n DatasourceColumnsResponse,\n DuplicateJobResponse,\n)\n\nfrom ._router import router\n\n\n# ============================================================\n# Translation Job CRUD\n# ============================================================\n\n# #region list_jobs [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF List all translation jobs with pagination and optional status filter.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs\", response_model=List[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n log.reason(\"Listing jobs\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n\n\n# #region get_job [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Get a single translation job by ID.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n log.reason(\"Getting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n\n\n# #region create_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission. Payload validated.\n# @POST Translation job created and returned.\n# @SIDE_EFFECT Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n log.reason(\"Creating job\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n\n\n# #region update_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission. Job exists.\n# @POST Translation job updated and returned.\n# @SIDE_EFFECT Updates TranslationJob record in DB; re-detects database_dialect if datasource changed.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n log.reason(\"Updating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n\n\n# #region delete_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission. Job exists.\n# @POST Translation job is deleted from DB.\n# @SIDE_EFFECT Deletes TranslationJob and associated records from DB.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n log.reason(\"Deleting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n\n\n# #region duplicate_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission. Source job exists.\n# @POST Duplicated job created with status DRAFT and returned.\n# @SIDE_EFFECT Creates a new TranslationJob record in DB as a copy of the source.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n log.reason(\"Duplicating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n\n\n# #region list_datasources [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF List all Superset datasets available for translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of datasets with metadata.\n# @SIDE_EFFECT Queries Superset API for available datasets.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: Optional[str] = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n log.explore(\"List datasources failed\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n# #endregion list_datasources\n\n\n# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission. Datasource exists in Superset.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n log.reason(\"Getting datasource columns\", payload={\n \"datasource_id\": datasource_id,\n \"env_id\": env_id,\n \"user\": current_user.username,\n })\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Get datasource columns failed\", error=str(e))\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n\n# #endregion TranslateJobRoutesModule\n" }, @@ -24885,8 +25389,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "List all translation jobs with pagination and optional status filter.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "List all translation jobs with pagination and optional status filter." }, "relations": [ { @@ -24896,23 +25400,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region list_jobs [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF List all translation jobs with pagination and optional status filter.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs\", response_model=List[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n log.reason(\"Listing jobs\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n" }, @@ -24925,8 +25413,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get a single translation job by ID.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get a single translation job by ID." }, "relations": [ { @@ -24936,23 +25424,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_job [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Get a single translation job by ID.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n log.reason(\"Getting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n" }, @@ -24965,10 +25437,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Create a new translation job.", "COMPLEXITY": 4, "POST": "Translation job created and returned.", "PRE": "User has translate.job.create permission. Payload validated.", + "PURPOSE": "Create a new translation job.", "SIDE_EFFECT": "Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect." }, "relations": [ @@ -24979,23 +25451,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region create_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission. Payload validated.\n# @POST Translation job created and returned.\n# @SIDE_EFFECT Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n log.reason(\"Creating job\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n" }, @@ -25008,10 +25464,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Update an existing translation job.", "COMPLEXITY": 4, "POST": "Translation job updated and returned.", "PRE": "User has translate.job.edit permission. Job exists.", + "PURPOSE": "Update an existing translation job.", "SIDE_EFFECT": "Updates TranslationJob record in DB; re-detects database_dialect if datasource changed." }, "relations": [ @@ -25022,23 +25478,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region update_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission. Job exists.\n# @POST Translation job updated and returned.\n# @SIDE_EFFECT Updates TranslationJob record in DB; re-detects database_dialect if datasource changed.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n log.reason(\"Updating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n" }, @@ -25051,10 +25491,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete a translation job.", "COMPLEXITY": 4, "POST": "Translation job is deleted from DB.", "PRE": "User has translate.job.delete permission. Job exists.", + "PURPOSE": "Delete a translation job.", "SIDE_EFFECT": "Deletes TranslationJob and associated records from DB." }, "relations": [ @@ -25065,23 +25505,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region delete_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission. Job exists.\n# @POST Translation job is deleted from DB.\n# @SIDE_EFFECT Deletes TranslationJob and associated records from DB.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n log.reason(\"Deleting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n" }, @@ -25094,10 +25518,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Duplicate a translation job.", "COMPLEXITY": 4, "POST": "Duplicated job created with status DRAFT and returned.", "PRE": "User has translate.job.create permission. Source job exists.", + "PURPOSE": "Duplicate a translation job.", "SIDE_EFFECT": "Creates a new TranslationJob record in DB as a copy of the source." }, "relations": [ @@ -25108,23 +25532,7 @@ "target_ref": "[TranslateJobService]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region duplicate_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission. Source job exists.\n# @POST Duplicated job created with status DRAFT and returned.\n# @SIDE_EFFECT Creates a new TranslationJob record in DB as a copy of the source.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n log.reason(\"Duplicating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n" }, @@ -25137,10 +25545,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "List all Superset datasets available for translation jobs.", "COMPLEXITY": 4, "POST": "Returns list of datasets with metadata.", "PRE": "User has translate.job.view permission.", + "PURPOSE": "List all Superset datasets available for translation jobs.", "SIDE_EFFECT": "Queries Superset API for available datasets." }, "relations": [ @@ -25157,23 +25565,7 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region list_datasources [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF List all Superset datasets available for translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of datasets with metadata.\n# @SIDE_EFFECT Queries Superset API for available datasets.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: Optional[str] = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n log.explore(\"List datasources failed\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n# #endregion list_datasources\n" }, @@ -25186,10 +25578,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Get column metadata and database dialect for a Superset datasource.", "COMPLEXITY": 4, "POST": "Returns column list with metadata and database_dialect.", "PRE": "User has translate.job.view permission. Datasource exists in Superset.", + "PURPOSE": "Get column metadata and database dialect for a Superset datasource.", "SIDE_EFFECT": "Queries Superset API for dataset detail and database info." }, "relations": [ @@ -25206,23 +25598,7 @@ "target_ref": "[ConfigManager]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission. Datasource exists in Superset.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n log.reason(\"Getting datasource columns\", payload={\n \"datasource_id\": datasource_id,\n \"env_id\": env_id,\n \"user\": current_user.username,\n })\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Get datasource columns failed\", error=str(e))\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n" }, @@ -25235,9 +25611,9 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Translation Metrics endpoints providing aggregated stats on runs and corrections.", "COMPLEXITY": 3, - "LAYER": "UI" + "LAYER": "UI", + "PURPOSE": "Translation Metrics endpoints providing aggregated stats on runs and corrections." }, "relations": [ { @@ -25259,23 +25635,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateMetricsRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,metrics]\n# @BRIEF Translation Metrics endpoints providing aggregated stats on runs and corrections.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.metrics import TranslationMetrics\n\nfrom ._router import router\n\nlog = MarkerLogger(\"TranslateMetricsRoutes\")\n\n# ============================================================\n# Metrics\n# ============================================================\n\n# #region get_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get translation metrics across all jobs, optionally filtered by job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n log.reason(\"Metrics request\", payload={\"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n\n\n# #region get_job_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get aggregated metrics for a specific translation job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n log.reason(\"Job metrics request\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n\n# #endregion TranslateMetricsRoutesModule\n" }, @@ -25288,8 +25648,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get translation metrics across all jobs, optionally filtered by job.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get translation metrics across all jobs, optionally filtered by job." }, "relations": [ { @@ -25299,23 +25659,7 @@ "target_ref": "[TranslationMetrics]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get translation metrics across all jobs, optionally filtered by job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n log.reason(\"Metrics request\", payload={\"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n" }, @@ -25328,8 +25672,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get aggregated metrics for a specific translation job.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get aggregated metrics for a specific translation job." }, "relations": [ { @@ -25339,23 +25683,7 @@ "target_ref": "[TranslationMetrics]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_job_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get aggregated metrics for a specific translation job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n log.reason(\"Job metrics request\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n" }, @@ -25368,11 +25696,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Translation Preview session management routes.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Preview sessions created, rows reviewed/accepted, records queried.", "PRE": "ConfigManager and DB session initialized. User authenticated with translate permissions.", + "PURPOSE": "Translation Preview session management routes.", "SIDE_EFFECT": "Creates/updates preview sessions and rows in DB; fetches sample data from Superset; calls LLM provider." }, "relations": [ @@ -25401,23 +25729,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslatePreviewRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,preview]\n# @BRIEF Translation Preview session management routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.\n# @POST Preview sessions created, rows reviewed/accepted, records queried.\n# @SIDE_EFFECT Creates/updates preview sessions and rows in DB; fetches sample data from Superset; calls LLM provider.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslatePreviewRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.preview import TranslationPreview\nfrom ....schemas.translate import (\n PreviewRequest,\n PreviewRowUpdate,\n PreviewAcceptResponse,\n PreviewRow,\n)\n\nfrom ._router import router\n\n\n# ============================================================\n# Preview\n# ============================================================\n\n# #region preview_translation [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Preview a translation before applying it.\n# @PRE User has translate.job.execute permission. Job exists.\n# @POST Preview session created with sample rows and cost estimation.\n# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n log.reason(\"Preview translation\", payload={\"job_id\": job_id, \"user\": current_user.username})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Preview translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n\n\n# #region update_preview_row [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE User has translate.job.execute permission. Preview row exists.\n# @POST Preview row status updated to APPROVED/REJECTED/EDITED.\n# @SIDE_EFFECT Updates PreviewRecord status in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n log.reason(\"Update preview row\", payload={\"job_id\": job_id, \"row\": row_key, \"action\": payload.action})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n\n\n# #region accept_preview_session [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST Preview session status set to APPLIED. Full execution can proceed.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n log.reason(\"Accept preview session\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n\n\n# #region apply_preview [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE User has translate.job.execute permission. Session exists.\n# @POST Preview session applied and marked as quality gate.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n log.reason(\"Apply preview\", payload={\"session_id\": session_id, \"user\": current_user.username})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n\n\n# ============================================================\n# Preview Records\n# ============================================================\n\n# #region get_preview_records [C:3] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Get records for a preview session.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n@router.get(\"/preview/{session_id}/records\", response_model=List[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n log.reason(\"Get preview records\", payload={\"session_id\": session_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Get preview records error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n\n# #endregion TranslatePreviewRoutesModule\n" }, @@ -25430,10 +25742,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Preview a translation before applying it.", "COMPLEXITY": 4, "POST": "Preview session created with sample rows and cost estimation.", "PRE": "User has translate.job.execute permission. Job exists.", + "PURPOSE": "Preview a translation before applying it.", "SIDE_EFFECT": "Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB." }, "relations": [ @@ -25450,23 +25762,7 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region preview_translation [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Preview a translation before applying it.\n# @PRE User has translate.job.execute permission. Job exists.\n# @POST Preview session created with sample rows and cost estimation.\n# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n log.reason(\"Preview translation\", payload={\"job_id\": job_id, \"user\": current_user.username})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Preview translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n" }, @@ -25479,10 +25775,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Approve, edit, or reject a preview row.", "COMPLEXITY": 4, "POST": "Preview row status updated to APPROVED/REJECTED/EDITED.", "PRE": "User has translate.job.execute permission. Preview row exists.", + "PURPOSE": "Approve, edit, or reject a preview row.", "SIDE_EFFECT": "Updates PreviewRecord status in DB." }, "relations": [ @@ -25493,23 +25789,7 @@ "target_ref": "[TranslationPreview]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region update_preview_row [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE User has translate.job.execute permission. Preview row exists.\n# @POST Preview row status updated to APPROVED/REJECTED/EDITED.\n# @SIDE_EFFECT Updates PreviewRecord status in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n log.reason(\"Update preview row\", payload={\"job_id\": job_id, \"row\": row_key, \"action\": payload.action})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n" }, @@ -25522,10 +25802,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Accept a preview session, marking it as the quality gate for full execution.", "COMPLEXITY": 4, "POST": "Preview session status set to APPLIED. Full execution can proceed.", "PRE": "User has translate.job.execute permission. Job has an ACTIVE preview session.", + "PURPOSE": "Accept a preview session, marking it as the quality gate for full execution.", "SIDE_EFFECT": "Updates PreviewSession status to APPLIED in DB." }, "relations": [ @@ -25536,23 +25816,7 @@ "target_ref": "[TranslationPreview]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region accept_preview_session [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST Preview session status set to APPLIED. Full execution can proceed.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n log.reason(\"Accept preview session\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n" }, @@ -25565,10 +25829,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Apply a preview session (alias for accept when accepting at session level).", "COMPLEXITY": 4, "POST": "Preview session applied and marked as quality gate.", "PRE": "User has translate.job.execute permission. Session exists.", + "PURPOSE": "Apply a preview session (alias for accept when accepting at session level).", "SIDE_EFFECT": "Updates PreviewSession status to APPLIED in DB." }, "relations": [ @@ -25579,23 +25843,7 @@ "target_ref": "[TranslationPreview]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region apply_preview [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE User has translate.job.execute permission. Session exists.\n# @POST Preview session applied and marked as quality gate.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n log.reason(\"Apply preview\", payload={\"session_id\": session_id, \"user\": current_user.username})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n" }, @@ -25608,8 +25856,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get records for a preview session.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get records for a preview session." }, "relations": [ { @@ -25625,23 +25873,7 @@ "target_ref": "[TranslationPreviewRecord]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_preview_records [C:3] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Get records for a preview session.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n@router.get(\"/preview/{session_id}/records\", response_model=List[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n log.reason(\"Get preview records\", payload={\"session_id\": session_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Get preview records error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n" }, @@ -25658,17 +25890,7 @@ "LAYER": "UI" }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS api,routes,translate,router]\n# @LAYER UI\n\nfrom fastapi import APIRouter\n\n# #region translate_router [C:1] [TYPE Global]\nrouter = APIRouter(prefix=\"/api/translate\", tags=[\"translate\"])\n# #endregion translate_router\n\n# #endregion TranslateRouterModule\n" }, @@ -25696,7 +25918,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -25722,9 +25946,9 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Translation Run listing, detail, and CSV download routes (cross-job).", "COMPLEXITY": 3, - "LAYER": "UI" + "LAYER": "UI", + "PURPOSE": "Translation Run listing, detail, and CSV download routes (cross-job)." }, "relations": [ { @@ -25752,23 +25976,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,runs,list,detail]\n# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateRunListRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....plugins.translate.events import TranslationEventLog\n\nfrom ._router import router\n\n\n# ============================================================\n# List Runs (cross-job)\n# ============================================================\n\n# #region list_runs [C:3] [TYPE Function] [SEMANTICS api,translate,runs,list]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: Optional[str] = Query(None),\n run_status: Optional[str] = Query(None, alias=\"status\"),\n trigger_type: Optional[str] = Query(None),\n created_by: Optional[str] = Query(None),\n date_from: Optional[str] = Query(None),\n date_to: Optional[str] = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n log.reason(\"Listing runs\", payload={\"user\": current_user.username})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n log.explore(\"List runs error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n\n\n# ============================================================\n# Run Detail\n# ============================================================\n\n# #region get_run_detail [C:3] [TYPE Function] [SEMANTICS api,translate,runs,detail]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n log.reason(\"Get run detail\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n\n\n# ============================================================\n# Download Skipped CSV\n# ============================================================\n\n# #region download_skipped_csv [C:3] [TYPE Function] [SEMANTICS api,translate,runs,csv]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n log.reason(\"Download skipped CSV\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationRecord\n from fastapi.responses import StreamingResponse\n import csv\n import io\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n log.explore(\"Download skipped CSV error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n\n# #endregion TranslateRunListRoutesModule\n" }, @@ -25781,8 +25989,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "List all runs with cross-job filtering and pagination.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "List all runs with cross-job filtering and pagination." }, "relations": [ { @@ -25792,23 +26000,7 @@ "target_ref": "[TranslationRun]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region list_runs [C:3] [TYPE Function] [SEMANTICS api,translate,runs,list]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: Optional[str] = Query(None),\n run_status: Optional[str] = Query(None, alias=\"status\"),\n trigger_type: Optional[str] = Query(None),\n created_by: Optional[str] = Query(None),\n date_from: Optional[str] = Query(None),\n date_to: Optional[str] = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n log.reason(\"Listing runs\", payload={\"user\": current_user.username})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n log.explore(\"List runs error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n" }, @@ -25821,8 +26013,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get detailed run info with config_snapshot, records, events.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get detailed run info with config_snapshot, records, events." }, "relations": [ { @@ -25844,23 +26036,7 @@ "target_ref": "[TranslationRun]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_run_detail [C:3] [TYPE Function] [SEMANTICS api,translate,runs,detail]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n log.reason(\"Get run detail\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n" }, @@ -25873,8 +26049,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Download a CSV of skipped translation records from a run.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Download a CSV of skipped translation records from a run." }, "relations": [ { @@ -25884,23 +26060,7 @@ "target_ref": "[TranslationRecord]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region download_skipped_csv [C:3] [TYPE Function] [SEMANTICS api,translate,runs,csv]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n log.reason(\"Download skipped CSV\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationRecord\n from fastapi.responses import StreamingResponse\n import csv\n import io\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n log.explore(\"Download skipped CSV error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n" }, @@ -25913,11 +26073,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Translation Run execution, history, status, records and batches routes.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Translation run executed, manipulated, or queried. Results returned to caller.", "PRE": "ConfigManager and DB session initialized. User authenticated with translate permissions.", + "PURPOSE": "Translation Run execution, history, status, records and batches routes.", "SIDE_EFFECT": "Creates/updates TranslationRun records in DB; spawns background threads for execution; queries Superset API." }, "relations": [ @@ -25946,23 +26106,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateRunRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,runs]\n# @BRIEF Translation Run execution, history, status, records and batches routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.\n# @POST Translation run executed, manipulated, or queried. Results returned to caller.\n# @SIDE_EFFECT Creates/updates TranslationRun records in DB; spawns background threads for execution; queries Superset API.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db, SessionLocal\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateRunRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....plugins.translate.events import TranslationEventLog\nfrom ....models.translate import TranslationRun\n\nfrom ._router import router\nfrom ._helpers import _run_to_response\n\n\n# ============================================================\n# Translation Run / Execute\n# ============================================================\n\n# #region run_translation [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE User has translate.job.execute permission. Job exists and preview is accepted.\n# @POST Translation run created and started in background. Run object returned.\n# @SIDE_EFFECT Creates TranslationRun record in DB; spawns background thread for translation execution.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n full_translation: bool = Query(False, description=\"If True, fetch ALL rows from Superset dataset instead of preview-only rows\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\n\n By default runs translation on preview-approved rows only.\n Set full_translation=true to fetch ALL rows from the Superset source dataset.\n \"\"\"\n log.reason(\"Run translation\", payload={\"job_id\": job_id, \"user\": current_user.username, \"full_translation\": full_translation})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # Execute asynchronously in background with a FRESH session.\n # The request-scoped session from Depends(get_db) is closed after the handler returns,\n # so the background thread must create its own session to avoid \"This transaction is closed\".\n import threading\n\n def _execute_background():\n thread_db = SessionLocal()\n try:\n thread_orch = TranslationOrchestrator(thread_db, config_manager, current_user.username)\n # Reload run from DB with the new session (the passed run object is detached)\n thread_run = thread_db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n if thread_run:\n thread_orch.execute_run(thread_run, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Background execution error\", error=str(e))\n finally:\n thread_db.close()\n\n threading.Thread(target=_execute_background, daemon=True).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Run translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n\n\n# #region retry_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE User has translate.job.execute permission. Run exists and has failed batches.\n# @POST Failed batches re-executed. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun and TranslationBatch records in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n log.reason(\"Retry run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry run error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n\n\n# #region retry_insert [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE User has translate.job.execute permission. Run completed with insert_status FAILED.\n# @POST SQL insert phase re-executed. Updated run returned.\n# @SIDE_EFFECT Re-executes SQL insert into Superset; updates TranslationRun insert_status.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n log.reason(\"Retry insert\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry insert error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n\n\n# #region cancel_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Cancel a running translation.\n# @PRE User has translate.job.execute permission. Run is in RUNNING state.\n# @POST Run is cancelled. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun status to CANCELLED in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n log.reason(\"Cancel run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n\n\n# #region get_run_history [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get run history for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n log.reason(\"Get run history\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n\n\n# #region get_run_status [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get status and statistics for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n log.reason(\"Get run status\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n\n\n# #region get_run_records [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get paginated records for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n log.reason(\"Get run records\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n\n\n# ============================================================\n# Batches\n# ============================================================\n\n# #region get_batches [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get batches for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n log.reason(\"Get batches\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n log.explore(\"Get batches error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n\n# #endregion TranslateRunRoutesModule\n" }, @@ -25975,10 +26119,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Execute a translation job (trigger a run).", "COMPLEXITY": 4, "POST": "Translation run created and started in background. Run object returned.", "PRE": "User has translate.job.execute permission. Job exists and preview is accepted.", + "PURPOSE": "Execute a translation job (trigger a run).", "SIDE_EFFECT": "Creates TranslationRun record in DB; spawns background thread for translation execution." }, "relations": [ @@ -25989,23 +26133,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region run_translation [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE User has translate.job.execute permission. Job exists and preview is accepted.\n# @POST Translation run created and started in background. Run object returned.\n# @SIDE_EFFECT Creates TranslationRun record in DB; spawns background thread for translation execution.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n full_translation: bool = Query(False, description=\"If True, fetch ALL rows from Superset dataset instead of preview-only rows\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\n\n By default runs translation on preview-approved rows only.\n Set full_translation=true to fetch ALL rows from the Superset source dataset.\n \"\"\"\n log.reason(\"Run translation\", payload={\"job_id\": job_id, \"user\": current_user.username, \"full_translation\": full_translation})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # Execute asynchronously in background with a FRESH session.\n # The request-scoped session from Depends(get_db) is closed after the handler returns,\n # so the background thread must create its own session to avoid \"This transaction is closed\".\n import threading\n\n def _execute_background():\n thread_db = SessionLocal()\n try:\n thread_orch = TranslationOrchestrator(thread_db, config_manager, current_user.username)\n # Reload run from DB with the new session (the passed run object is detached)\n thread_run = thread_db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n if thread_run:\n thread_orch.execute_run(thread_run, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Background execution error\", error=str(e))\n finally:\n thread_db.close()\n\n threading.Thread(target=_execute_background, daemon=True).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Run translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n" }, @@ -26018,10 +26146,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Retry failed batches in a translation run.", "COMPLEXITY": 4, "POST": "Failed batches re-executed. Updated run returned.", "PRE": "User has translate.job.execute permission. Run exists and has failed batches.", + "PURPOSE": "Retry failed batches in a translation run.", "SIDE_EFFECT": "Updates TranslationRun and TranslationBatch records in DB." }, "relations": [ @@ -26032,23 +26160,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region retry_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE User has translate.job.execute permission. Run exists and has failed batches.\n# @POST Failed batches re-executed. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun and TranslationBatch records in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n log.reason(\"Retry run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry run error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n" }, @@ -26061,10 +26173,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Retry the SQL insert phase for a completed run.", "COMPLEXITY": 4, "POST": "SQL insert phase re-executed. Updated run returned.", "PRE": "User has translate.job.execute permission. Run completed with insert_status FAILED.", + "PURPOSE": "Retry the SQL insert phase for a completed run.", "SIDE_EFFECT": "Re-executes SQL insert into Superset; updates TranslationRun insert_status." }, "relations": [ @@ -26075,23 +26187,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region retry_insert [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE User has translate.job.execute permission. Run completed with insert_status FAILED.\n# @POST SQL insert phase re-executed. Updated run returned.\n# @SIDE_EFFECT Re-executes SQL insert into Superset; updates TranslationRun insert_status.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n log.reason(\"Retry insert\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry insert error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n" }, @@ -26104,10 +26200,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Cancel a running translation.", "COMPLEXITY": 4, "POST": "Run is cancelled. Updated run returned.", "PRE": "User has translate.job.execute permission. Run is in RUNNING state.", + "PURPOSE": "Cancel a running translation.", "SIDE_EFFECT": "Updates TranslationRun status to CANCELLED in DB." }, "relations": [ @@ -26118,23 +26214,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region cancel_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Cancel a running translation.\n# @PRE User has translate.job.execute permission. Run is in RUNNING state.\n# @POST Run is cancelled. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun status to CANCELLED in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n log.reason(\"Cancel run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n" }, @@ -26147,8 +26227,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get run history for a translation job.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get run history for a translation job." }, "relations": [ { @@ -26158,23 +26238,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_run_history [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get run history for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n log.reason(\"Get run history\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n" }, @@ -26187,8 +26251,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get status and statistics for a translation run.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get status and statistics for a translation run." }, "relations": [ { @@ -26198,23 +26262,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_run_status [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get status and statistics for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n log.reason(\"Get run status\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n" }, @@ -26227,8 +26275,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get paginated records for a translation run.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get paginated records for a translation run." }, "relations": [ { @@ -26238,23 +26286,7 @@ "target_ref": "[TranslationOrchestrator]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_run_records [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get paginated records for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n log.reason(\"Get run records\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n" }, @@ -26267,8 +26299,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get batches for a translation run.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get batches for a translation run." }, "relations": [ { @@ -26278,23 +26310,7 @@ "target_ref": "[TranslationBatch]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_batches [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get batches for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n log.reason(\"Get batches\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n log.explore(\"Get batches error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n" }, @@ -26307,11 +26323,11 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Translation Schedule management routes.", "COMPLEXITY": 4, "LAYER": "UI", "POST": "Schedule CRUD operations completed. APScheduler jobs registered/unregistered.", "PRE": "ConfigManager and DB session initialized. User authenticated with translate.schedule permissions.", + "PURPOSE": "Translation Schedule management routes.", "SIDE_EFFECT": "Creates/updates/deletes TranslationSchedule records in DB; registers/removes jobs in APScheduler." }, "relations": [ @@ -26340,23 +26356,7 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateScheduleRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,schedule]\n# @BRIEF Translation Schedule management routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate.schedule permissions.\n# @POST Schedule CRUD operations completed. APScheduler jobs registered/unregistered.\n# @SIDE_EFFECT Creates/updates/deletes TranslationSchedule records in DB; registers/removes jobs in APScheduler.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateScheduleRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.scheduler import TranslationScheduler\nfrom ....schemas.translate import ScheduleConfig, ScheduleResponse\n\nfrom ._router import router\n\n\n# ============================================================\n# Schedule\n# ============================================================\n\n# #region get_schedule [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Get the schedule configuration for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n log.reason(\"Get schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n\n\n# #region set_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Job exists.\n# @POST Schedule created or updated. APScheduler job registered.\n# @SIDE_EFFECT Creates/updates TranslationSchedule record in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n log.reason(\"Set schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n\n\n# #region enable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is enabled. APScheduler job registered.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to True in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n log.reason(\"Enable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n\n\n# #region disable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is disabled. APScheduler job removed.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to False in DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n log.reason(\"Disable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n\n\n# #region delete_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is removed. APScheduler job removed.\n# @SIDE_EFFECT Deletes TranslationSchedule record from DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n log.reason(f\"Delete schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n\n\n# #region get_next_executions [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Preview next N execution times for a job's schedule.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n log.reason(f\"Get next executions\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n\n# #endregion TranslateScheduleRoutesModule\n" }, @@ -26369,8 +26369,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Get the schedule configuration for a translation job.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Get the schedule configuration for a translation job." }, "relations": [ { @@ -26380,23 +26380,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_schedule [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Get the schedule configuration for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n log.reason(\"Get schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n" }, @@ -26409,10 +26393,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Set or update the schedule for a translation job.", "COMPLEXITY": 4, "POST": "Schedule created or updated. APScheduler job registered.", "PRE": "User has translate.schedule.manage permission. Job exists.", + "PURPOSE": "Set or update the schedule for a translation job.", "SIDE_EFFECT": "Creates/updates TranslationSchedule record in DB; registers job in APScheduler." }, "relations": [ @@ -26423,23 +26407,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region set_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Job exists.\n# @POST Schedule created or updated. APScheduler job registered.\n# @SIDE_EFFECT Creates/updates TranslationSchedule record in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n log.reason(\"Set schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n" }, @@ -26452,10 +26420,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Enable a schedule for a translation job.", "COMPLEXITY": 4, "POST": "Schedule is enabled. APScheduler job registered.", "PRE": "User has translate.schedule.manage permission. Schedule exists.", + "PURPOSE": "Enable a schedule for a translation job.", "SIDE_EFFECT": "Updates TranslationSchedule is_active to True in DB; registers job in APScheduler." }, "relations": [ @@ -26466,23 +26434,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region enable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is enabled. APScheduler job registered.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to True in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n log.reason(\"Enable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n" }, @@ -26495,10 +26447,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Disable a schedule for a translation job.", "COMPLEXITY": 4, "POST": "Schedule is disabled. APScheduler job removed.", "PRE": "User has translate.schedule.manage permission. Schedule exists.", + "PURPOSE": "Disable a schedule for a translation job.", "SIDE_EFFECT": "Updates TranslationSchedule is_active to False in DB; removes job from APScheduler." }, "relations": [ @@ -26509,23 +26461,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region disable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is disabled. APScheduler job removed.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to False in DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n log.reason(\"Disable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n" }, @@ -26538,10 +26474,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Delete the schedule for a translation job.", "COMPLEXITY": 4, "POST": "Schedule is removed. APScheduler job removed.", "PRE": "User has translate.schedule.manage permission. Schedule exists.", + "PURPOSE": "Delete the schedule for a translation job.", "SIDE_EFFECT": "Deletes TranslationSchedule record from DB; removes job from APScheduler." }, "relations": [ @@ -26552,23 +26488,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region delete_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is removed. APScheduler job removed.\n# @SIDE_EFFECT Deletes TranslationSchedule record from DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n log.reason(f\"Delete schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n" }, @@ -26581,8 +26501,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Preview next N execution times for a job's schedule.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Preview next N execution times for a job's schedule." }, "relations": [ { @@ -26592,23 +26512,7 @@ "target_ref": "[TranslationScheduler]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_next_executions [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Preview next N execution times for a job's schedule.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n log.reason(f\"Get next executions\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n" }, @@ -26621,7 +26525,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "[HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]", "INVARIANT": "All WebSocket connections must be properly cleaned up on disconnect.", "LAYER": "UI (API)", @@ -26652,17 +26556,21 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "UI (API)" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -26677,6 +26585,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -26694,6 +26603,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -26712,7 +26622,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Canonical FastAPI application instance for route, middleware, and websocket registration.", "SEMANTICS": [ "app", @@ -26747,7 +26657,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -26772,7 +26684,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -26792,7 +26706,10 @@ "detail": { "actual_type": "Global", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -26827,7 +26744,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Ensures initial admin user exists when bootstrap env flags are enabled." }, "relations": [ @@ -26851,7 +26768,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Scheduler is started.", "PRE": "None.", "PURPOSE": "Handles application startup tasks, such as starting the scheduler." @@ -26895,6 +26812,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -26913,7 +26831,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Scheduler is stopped.", "PRE": "None.", "PURPOSE": "Handles application shutdown tasks, such as stopping the scheduler." @@ -26957,6 +26875,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -26978,7 +26897,17 @@ "PURPOSE": "Configure application-wide middleware (Session, CORS, TraceContext)." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Block' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Block" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:app_middleware:Block]\n# @PURPOSE: Configure application-wide middleware (Session, CORS, TraceContext).\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\napp.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)\n\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Adjust this in production\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# Configure Trace Context Middleware (seeds trace_id per request)\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# [/DEF:app_middleware:Block]\n" }, @@ -26991,7 +26920,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PARAM": "exc (NetworkError) - The exception instance.", "POST": "Returns 503 HTTP Exception.", "PRE": "request is a FastAPI Request object.", @@ -27022,6 +26951,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -27036,7 +26974,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "call_next (Callable) - The next middleware or route handler.", "POST": "Logs request and response details.", "PRE": "request is a FastAPI Request object.", @@ -27087,6 +27025,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -27105,7 +27044,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Register all FastAPI route groups exposed by the application entrypoint." }, "relations": [ @@ -27189,7 +27128,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "API", "PURPOSE": "Registers all API routers with the FastAPI application.", "SEMANTICS": [ @@ -27211,7 +27150,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -27231,7 +27172,9 @@ "detail": { "actual_type": "Action", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -27244,20 +27187,6 @@ "contract_type": "Action" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'API' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "API" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -27270,7 +27199,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -27290,7 +27221,10 @@ "detail": { "actual_type": "Action", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -27316,7 +27250,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "[task_id: str, source: str, level: str] -> [JSON log entry objects]", "INVARIANT": "Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.", "POST": "WebSocket connection is managed and logs are streamed until disconnect.", @@ -27342,10 +27276,42 @@ ], "schema_warnings": [ { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } }, { "code": "invalid_relation_predicate", @@ -27359,6 +27325,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -27376,6 +27343,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -27394,7 +27362,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Mounts the frontend build directory to serve static assets.", "SEMANTICS": [ "static", @@ -27415,7 +27383,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -27440,7 +27410,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -27460,7 +27432,10 @@ "detail": { "actual_type": "Mount", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -27486,7 +27461,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns the requested file or index.html.", "PRE": "frontend_path exists.", "PURPOSE": "Serves the SPA frontend for any path not matched by API routes." @@ -27510,6 +27485,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -27524,7 +27508,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns a JSON message indicating API status.", "PRE": "None.", "PURPOSE": "A simple root endpoint to confirm that the API is running when frontend is missing." @@ -27548,6 +27532,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -27578,7 +27571,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -27670,7 +27665,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "all() always returns [existing_record]; no parameterization or filtering.", "PURPOSE": "Minimal query stub returning hardcoded existing environment record list for sync tests." }, @@ -27692,6 +27687,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -27714,7 +27718,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "query() always returns _FakeQuery; no real DB interaction.", "PURPOSE": "Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions." }, @@ -27736,6 +27740,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -27800,15 +27813,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -27996,7 +28000,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients.", "SEMANTICS": [ @@ -28029,6 +28033,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[BINDS_TO]" @@ -28056,15 +28061,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -28096,15 +28092,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -28457,7 +28444,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Verifies Superset profile lookup adapter payload normalization and fallback error precedence.", "SEMANTICS": [ @@ -28490,6 +28477,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -28508,7 +28496,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Each request consumes one scripted response in call order and persists call metadata.", "PURPOSE": "Records request payloads and returns scripted responses for deterministic adapter tests." }, @@ -28711,7 +28699,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Unit tests for ThrottledSchedulerConfigurator distribution logic." }, "relations": [ @@ -28723,15 +28711,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -28744,6 +28723,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -28960,7 +28940,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Parse a Superset dashboard URL and extract native filter state asynchronously.", "SEMANTICS": [ @@ -28974,20 +28954,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -29010,7 +28976,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Async sibling of SupersetClient for dashboard read paths." }, "relations": [ @@ -29046,6 +29012,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[INHERITS]" @@ -29063,6 +29030,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -29080,6 +29048,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29098,7 +29067,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[Environment] -> self.network[AsyncAPIClient]", "POST": "Client uses async network transport and inherited projection helpers.", "PRE": "env is valid Environment instance.", @@ -29152,6 +29121,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -29170,7 +29140,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Underlying AsyncAPIClient is closed.", "PURPOSE": "Close async transport resources.", "SIDE_EFFECT": "Closes network sockets." @@ -29214,6 +29184,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29232,7 +29203,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]", "POST": "Returns total count and page result list.", "PURPOSE": "Fetch one dashboards page asynchronously." @@ -29276,6 +29247,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29294,7 +29266,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[dashboard_id: int] -> Output[Dict]", "POST": "Returns raw dashboard payload from Superset API.", "PURPOSE": "Fetch one dashboard payload asynchronously." @@ -29338,6 +29310,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29356,7 +29329,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[chart_id: int] -> Output[Dict]", "POST": "Returns raw chart payload from Superset API.", "PURPOSE": "Fetch one chart payload asynchronously." @@ -29400,6 +29373,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29418,7 +29392,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[dashboard_id: int] -> Output[Dict]", "POST": "Returns dashboard detail payload for overview page.", "PURPOSE": "Fetch dashboard detail asynchronously with concurrent charts/datasets requests." @@ -29468,6 +29442,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29485,6 +29460,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29503,7 +29479,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[permalink_key: str] -> Output[Dict]", "POST": "Returns dashboard permalink state payload from Superset API.", "PURPOSE": "Fetch stored dashboard permalink state asynchronously." @@ -29541,7 +29517,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]", "POST": "Returns native filter state payload from Superset API.", "PURPOSE": "Fetch stored native filter state asynchronously." @@ -29579,7 +29555,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[permalink_key: str] -> Output[Dict]", "POST": "Returns extracted dataMask with filter states.", "PURPOSE": "Extract native filters dataMask from a permalink key asynchronously." @@ -29623,6 +29599,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29641,7 +29618,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]", "POST": "Returns extracted filter state with extraFormData.", "PURPOSE": "Extract native filters from a native_filters_key URL parameter asynchronously." @@ -29685,6 +29662,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29703,7 +29681,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[url: str] -> Output[Dict]", "POST": "Returns extracted filter state or empty dict if no filters found.", "PURPOSE": "Parse a Superset dashboard URL and extract native filter state asynchronously." @@ -29753,6 +29731,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29770,6 +29749,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -29804,7 +29784,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -30193,7 +30175,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "All sensitive configuration must have defaults or be loaded from environment.", "LAYER": "Core", "PURPOSE": "Centralized configuration for authentication and authorization.", @@ -30223,20 +30205,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30290,6 +30258,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30335,7 +30312,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -30370,7 +30349,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Tokens must include expiration time and user identifier.", "LAYER": "Core", "PURPOSE": "JWT token generation and validation logic.", @@ -30405,20 +30384,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -30431,6 +30396,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -30488,6 +30454,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -30556,6 +30531,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -30590,7 +30574,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Must not log sensitive data like passwords or full tokens.", "LAYER": "Core", "PURPOSE": "Audit logging for security-related events.", @@ -30619,20 +30603,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -30645,6 +30615,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -30701,6 +30672,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30722,6 +30702,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -30740,7 +30721,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Must use secure OIDC flows.", "LAYER": "Core", "PURPOSE": "ADFS OIDC configuration and client using Authlib.", @@ -30775,20 +30756,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30810,6 +30777,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -30851,7 +30819,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -30923,6 +30893,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30944,6 +30923,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -30961,6 +30941,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -31011,6 +30992,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -31038,6 +31028,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -31056,7 +31047,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access]", "INVARIANT": "All database read/write operations must execute via the injected SQLAlchemy session boundary.", "LAYER": "Domain", @@ -31093,7 +31084,26 @@ "target_ref": "[belief_scope]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:AuthRepositoryModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: auth, repository, database, user, role, permission\n# @PURPOSE: Data access layer for authentication and user preference entities.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [AuthModels]\n# @RELATION: DEPENDS_ON -> [ProfileModels]\n# @RELATION: DEPENDS_ON -> [belief_scope]\n# @INVARIANT: All database read/write operations must execute via the injected SQLAlchemy session boundary.\n# @DATA_CONTRACT: Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access]\n# @PRE: Database connection is active.\n# @POST: Provides valid access to identity data.\n# @SIDE_EFFECT: Executes database read queries through the injected SQLAlchemy session boundary.\n\n# [SECTION: IMPORTS]\nfrom typing import List, Optional\nfrom sqlalchemy.orm import Session, selectinload\nfrom ...models.auth import Permission, Role, User, ADGroupMapping\nfrom ...models.profile import UserDashboardPreference\nfrom ..logger import belief_scope, logger\n# [/SECTION]\n\n\n# [DEF:AuthRepository:Class]\n# @PURPOSE: Provides low-level CRUD operations for identity and authorization records.\n# @PRE: Database session is bound.\n# @POST: Entity instances returned safely.\n# @SIDE_EFFECT: Performs database reads.\n# @RELATION: DEPENDS_ON -> [AuthModels]\nclass AuthRepository:\n def __init__(self, db: Session):\n self.db = db\n\n # [DEF:get_user_by_id:Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_id:Function]\n\n # [DEF:get_user_by_username:Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_username:Function]\n\n # [DEF:get_role_by_id:Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n\n # [/DEF:get_role_by_id:Function]\n\n # [DEF:get_role_by_name:Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n\n # [/DEF:get_role_by_name:Function]\n\n # [DEF:get_permission_by_id:Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n\n # [/DEF:get_permission_by_id:Function]\n\n # [DEF:get_permission_by_resource_action:Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n\n # [/DEF:get_permission_by_resource_action:Function]\n\n # [DEF:list_permissions:Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> List[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n\n # [/DEF:list_permissions:Function]\n\n # [DEF:get_user_dashboard_preference:Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> Optional[UserDashboardPreference]:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n\n # [/DEF:get_user_dashboard_preference:Function]\n\n # [DEF:get_roles_by_ad_groups:Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n\n # [/DEF:get_roles_by_ad_groups:Function]\n\n\n# [/DEF:AuthRepository:Class]\n\n# [/DEF:AuthRepositoryModule:Module]\n" }, @@ -31138,6 +31148,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -31200,6 +31219,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31253,6 +31281,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31292,6 +31329,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31325,6 +31371,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31358,6 +31413,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31391,6 +31455,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31424,6 +31497,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31457,6 +31539,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31516,6 +31607,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31538,7 +31638,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Uses bcrypt for hashing with standard work factor.", "LAYER": "Core", "PURPOSE": "Utility for password hashing and verification using Passlib.", @@ -31567,20 +31667,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31642,6 +31728,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -31709,6 +31804,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -31737,7 +31841,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[json, record] -> Model[AppConfig]", "INVARIANT": "Configuration must always be representable by AppConfig and persisted under global record id.", "LAYER": "Domain", @@ -31786,6 +31890,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -31798,6 +31920,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -31815,6 +31938,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -31832,6 +31956,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -31849,6 +31974,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -31866,6 +31992,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -31890,6 +32017,24 @@ }, "relations": [], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -31915,7 +32060,17 @@ "PURPOSE": "Build default application configuration fallback." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_default_config:Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n log.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n\n # [/DEF:_default_config:Function]\n" }, @@ -31931,7 +32086,17 @@ "PURPOSE": "Merge typed AppConfig state into raw payload while preserving unsupported legacy sections." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_sync_raw_payload_from_config:Function]\n # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections.\n def _sync_raw_payload_from_config(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._sync_raw_payload_from_config\"):\n typed_payload = self.config.model_dump()\n merged_payload = dict(self.raw_payload or {})\n merged_payload[\"environments\"] = typed_payload.get(\"environments\", [])\n merged_payload[\"settings\"] = typed_payload.get(\"settings\", {})\n self.raw_payload = merged_payload\n log.reason(\n \"Synchronized raw payload from typed config\",\n payload={\n \"environments_count\": len(\n merged_payload.get(\"environments\", []) or []\n ),\n \"has_settings\": \"settings\" in merged_payload,\n \"extra_sections\": sorted(\n key\n for key in merged_payload.keys()\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n\n # [/DEF:_sync_raw_payload_from_config:Function]\n" }, @@ -31947,7 +32112,17 @@ "PURPOSE": "Load legacy JSON configuration for migration fallback path." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_load_from_legacy_file:Function]\n # @PURPOSE: Load legacy JSON configuration for migration fallback path.\n def _load_from_legacy_file(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._load_from_legacy_file\"):\n if not self.config_path.exists():\n log.reason(\n \"Legacy config file not found; using default payload\",\n payload={\"path\": str(self.config_path)},\n )\n return {}\n\n log.reason(\n \"Loading legacy config file\", payload={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n\n if not isinstance(payload, dict):\n log.explore(\"Legacy config payload is not a JSON object\", error=f\"Expected dict, got {type(payload).__name__}\", payload={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n })\n raise ValueError(\"Legacy config payload must be a JSON object\")\n\n log.reason(\n \"Legacy config file loaded successfully\",\n payload={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n\n # [/DEF:_load_from_legacy_file:Function]\n" }, @@ -31963,7 +32138,17 @@ "PURPOSE": "Resolve global configuration record from DB." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_get_record:Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> Optional[AppConfigRecord]:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n log.reason(\n \"Resolved app config record\", payload={\"exists\": record is not None}\n )\n return record\n\n # [/DEF:_get_record:Function]\n" }, @@ -31979,7 +32164,17 @@ "PURPOSE": "Load configuration from DB or perform one-time migration from legacy JSON." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_load_config:Function]\n # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON.\n def _load_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._load_config\"):\n session = SessionLocal()\n try:\n record = self._get_record(session)\n if record and isinstance(record.payload, dict):\n log.reason(\n \"Loading configuration from database\",\n payload={\"record_id\": record.id},\n )\n self.raw_payload = dict(record.payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._sync_environment_records(session, config)\n session.commit()\n log.reason(\n \"Database configuration validated successfully\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n\n log.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n payload={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\n\n if legacy_payload:\n self.raw_payload = dict(legacy_payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._apply_features_from_env(config.settings)\n log.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n self._save_config_to_db(config, session=session)\n return config\n\n log.reason(\n \"No persisted config found; falling back to default configuration\"\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config, session=session)\n return config\n except (json.JSONDecodeError, TypeError, ValueError) as exc:\n log.explore(\"Recoverable config load failure; falling back to default configuration\", error=str(exc), payload={\"legacy_path\": str(self.config_path)})\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n log.explore(\"Critical config load failure; re-raising persistence or validation error\", error=str(exc))\n raise\n finally:\n session.close()\n\n # [/DEF:_load_config:Function]\n" }, @@ -31995,7 +32190,17 @@ "PURPOSE": "Mirror configured environments into the relational environments table used by FK-backed domain models." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_sync_environment_records:Function]\n # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models.\n def _sync_environment_records(self, session: Session, config: AppConfig) -> None:\n with belief_scope(\"ConfigManager._sync_environment_records\"):\n configured_envs = list(config.environments or [])\n persisted_records = session.query(EnvironmentRecord).all()\n persisted_by_id = {\n str(record.id or \"\").strip(): record for record in persisted_records\n }\n\n for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\n\n display_name = (\n str(environment.name or normalized_id).strip() or normalized_id\n )\n normalized_url = str(environment.url or \"\").strip()\n credentials_id = (\n str(environment.username or \"\").strip() or normalized_id\n )\n\n record = persisted_by_id.get(normalized_id)\n if record is None:\n log.reason(\n \"Creating relational environment record from typed config\",\n payload={\n \"environment_id\": normalized_id,\n \"environment_name\": display_name,\n },\n )\n session.add(\n EnvironmentRecord(\n id=normalized_id,\n name=display_name,\n url=normalized_url,\n credentials_id=credentials_id,\n )\n )\n continue\n\n record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n\n # [/DEF:_sync_environment_records:Function]\n" }, @@ -32011,7 +32216,17 @@ "PURPOSE": "Persist provided AppConfig into the global DB configuration record." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_save_config_to_db:Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n def _save_config_to_db(\n self, config: AppConfig, session: Optional[Session] = None\n ) -> None:\n with belief_scope(\"ConfigManager._save_config_to_db\"):\n owns_session = session is None\n db = session or SessionLocal()\n try:\n self.config = config\n payload = self._sync_raw_payload_from_config()\n record = self._get_record(db)\n if record is None:\n log.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n log.reason(\n \"Updating existing global app config record\",\n payload={\"record_id\": record.id},\n )\n record.payload = payload\n\n self._sync_environment_records(db, config)\n\n db.commit()\n log.reason(\n \"Configuration persisted to database\",\n payload={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n except Exception:\n db.rollback()\n log.explore(\"Database save failed; transaction rolled back\", error=\"Database transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n\n # [/DEF:_save_config_to_db:Function]\n" }, @@ -32027,7 +32242,17 @@ "PURPOSE": "Persist current in-memory configuration state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:save:Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n log.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n\n # [/DEF:save:Function]\n" }, @@ -32043,7 +32268,17 @@ "PURPOSE": "Return current in-memory configuration snapshot." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:get_config:Function]\n # @PURPOSE: Return current in-memory configuration snapshot.\n def get_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager.get_config\"):\n return self.config\n\n # [/DEF:get_config:Function]\n" }, @@ -32059,7 +32294,17 @@ "PURPOSE": "Return full persisted payload including sections outside typed AppConfig schema." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:get_payload:Function]\n # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema.\n def get_payload(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager.get_payload\"):\n return self._sync_raw_payload_from_config()\n\n # [/DEF:get_payload:Function]\n" }, @@ -32075,7 +32320,17 @@ "PURPOSE": "Persist configuration provided either as typed AppConfig or raw payload dict." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:save_config:Function]\n # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict.\n def save_config(self, config: Any) -> AppConfig:\n with belief_scope(\"ConfigManager.save_config\"):\n if isinstance(config, AppConfig):\n log.reason(\"Saving typed AppConfig payload\")\n self.config = config\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config)\n return self.config\n\n if isinstance(config, dict):\n log.reason(\n \"Saving raw config payload\",\n payload={\"keys\": sorted(config.keys())},\n )\n self.raw_payload = dict(config)\n typed_config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self.config = typed_config\n self._save_config_to_db(typed_config)\n return self.config\n\n log.explore(\"Unsupported config type supplied to save_config\", error=f\"Expected AppConfig or dict, got {type(config).__name__}\", payload={\"type\": type(config).__name__})\n raise TypeError(\"config must be AppConfig or dict\")\n\n # [/DEF:save_config:Function]\n" }, @@ -32091,7 +32346,17 @@ "PURPOSE": "Replace global settings and persist the resulting configuration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:update_global_settings:Function]\n # @PURPOSE: Replace global settings and persist the resulting configuration.\n def update_global_settings(self, settings: GlobalSettings) -> AppConfig:\n with belief_scope(\"ConfigManager.update_global_settings\"):\n log.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n\n # [/DEF:update_global_settings:Function]\n" }, @@ -32107,7 +32372,17 @@ "PURPOSE": "Validate that path exists and is writable, creating it when absent." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:validate_path:Function]\n # @PURPOSE: Validate that path exists and is writable, creating it when absent.\n def validate_path(self, path: str) -> tuple[bool, str]:\n with belief_scope(\"ConfigManager.validate_path\", f\"path={path}\"):\n try:\n target = Path(path).expanduser()\n target.mkdir(parents=True, exist_ok=True)\n\n if not target.exists():\n return False, f\"Path does not exist: {target}\"\n\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\n\n test_file = target / \".write_test\"\n with test_file.open(\"w\", encoding=\"utf-8\") as fh:\n fh.write(\"ok\")\n test_file.unlink(missing_ok=True)\n\n log.reason(\"Path validation succeeded\", payload={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n log.explore(\"Path validation failed\", error=str(exc), payload={\"path\": path})\n return False, str(exc)\n\n # [/DEF:validate_path:Function]\n" }, @@ -32123,7 +32398,17 @@ "PURPOSE": "Return all configured environments." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:get_environments:Function]\n # @PURPOSE: Return all configured environments.\n def get_environments(self) -> List[Environment]:\n with belief_scope(\"ConfigManager.get_environments\"):\n return list(self.config.environments)\n\n # [/DEF:get_environments:Function]\n" }, @@ -32139,7 +32424,17 @@ "PURPOSE": "Check whether at least one environment exists in configuration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:has_environments:Function]\n # @PURPOSE: Check whether at least one environment exists in configuration.\n def has_environments(self) -> bool:\n with belief_scope(\"ConfigManager.has_environments\"):\n return len(self.config.environments) > 0\n\n # [/DEF:has_environments:Function]\n" }, @@ -32155,7 +32450,17 @@ "PURPOSE": "Resolve a configured environment by identifier." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:get_environment:Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Optional[Environment]:\n with belief_scope(\"ConfigManager.get_environment\", f\"env_id={env_id}\"):\n normalized = str(env_id or \"\").strip()\n if not normalized:\n return None\n\n for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n\n # [/DEF:get_environment:Function]\n" }, @@ -32171,7 +32476,17 @@ "PURPOSE": "Upsert environment by id into configuration and persist." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:add_environment:Function]\n # @PURPOSE: Upsert environment by id into configuration and persist.\n def add_environment(self, env: Environment) -> AppConfig:\n with belief_scope(\"ConfigManager.add_environment\", f\"env_id={env.id}\"):\n existing_index = next(\n (\n i\n for i, item in enumerate(self.config.environments)\n if item.id == env.id\n ),\n None,\n )\n if env.is_default:\n for item in self.config.environments:\n item.is_default = False\n\n if existing_index is None:\n log.reason(\"Appending new environment\", payload={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n log.reason(\n \"Replacing existing environment during add\",\n payload={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\n\n if len(self.config.environments) == 1 and not any(\n item.is_default for item in self.config.environments\n ):\n self.config.environments[0].is_default = True\n\n self.save()\n return self.config\n\n # [/DEF:add_environment:Function]\n" }, @@ -32187,7 +32502,17 @@ "PURPOSE": "Update existing environment by id and preserve masked password placeholder behavior." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:update_environment:Function]\n # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior.\n def update_environment(self, env_id: str, env: Environment) -> bool:\n with belief_scope(\"ConfigManager.update_environment\", f\"env_id={env_id}\"):\n for index, existing in enumerate(self.config.environments):\n if existing.id != env_id:\n continue\n\n update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n\n updated = Environment.model_validate(update_data)\n\n if updated.is_default:\n for item in self.config.environments:\n item.is_default = False\n elif existing.is_default and not updated.is_default:\n updated.is_default = True\n\n self.config.environments[index] = updated\n log.reason(\"Environment updated\", payload={\"env_id\": env_id})\n self.save()\n return True\n\n log.explore(\"Environment update skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n # [/DEF:update_environment:Function]\n" }, @@ -32203,7 +32528,17 @@ "PURPOSE": "Delete environment by id and persist when deletion occurs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:delete_environment:Function]\n # @PURPOSE: Delete environment by id and persist when deletion occurs.\n def delete_environment(self, env_id: str) -> bool:\n with belief_scope(\"ConfigManager.delete_environment\", f\"env_id={env_id}\"):\n before = len(self.config.environments)\n removed = [env for env in self.config.environments if env.id == env_id]\n self.config.environments = [\n env for env in self.config.environments if env.id != env_id\n ]\n\n if len(self.config.environments) == before:\n log.explore(\"Environment delete skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\n\n if self.config.settings.default_environment_id == env_id:\n replacement = next(\n (env.id for env in self.config.environments if env.is_default), None\n )\n self.config.settings.default_environment_id = replacement\n\n log.reason(\n \"Environment deleted\",\n payload={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n\n # [/DEF:delete_environment:Function]\n" }, @@ -32216,7 +32551,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Defines the data models for application configuration using Pydantic.", "SEMANTICS": [ @@ -32239,22 +32574,7 @@ "target_ref": "[ConnectionContracts]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ConfigModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: config, models, pydantic\n# @PURPOSE: Defines the data models for application configuration using Pydantic.\n# @LAYER: Core\n# @RELATION: IMPLEMENTS -> [CoreContracts]\n# @RELATION: IMPLEMENTS -> [ConnectionContracts]\n\nfrom pydantic import BaseModel, Field\nfrom typing import List, Optional\nfrom ..models.storage import StorageConfig\nfrom ..services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROMPTS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n)\n\n\n# [DEF:Schedule:DataClass]\n# @PURPOSE: Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# [/DEF:Schedule:DataClass]\n\n\n# [DEF:Environment:DataClass]\n# @PURPOSE: Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# [/DEF:Environment:DataClass]\n\n\n# [DEF:LoggingConfig:DataClass]\n# @PURPOSE: Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: Optional[str] = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# [/DEF:LoggingConfig:DataClass]\n\n\n# [DEF:CleanReleaseConfig:DataClass]\n# @PURPOSE: Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: Optional[str] = None\n active_registry_id: Optional[str] = None\n\n\n# [/DEF:CleanReleaseConfig:DataClass]\n\n\n# [DEF:FeaturesConfig:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# [/DEF:FeaturesConfig:DataClass]\n\n\n# [DEF:GlobalSettings:DataClass]\n# @PURPOSE: Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: Optional[str] = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: List[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# [/DEF:GlobalSettings:DataClass]\n\n\n# [DEF:AppConfig:DataClass]\n# @PURPOSE: The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: List[Environment] = []\n settings: GlobalSettings\n\n\n# [/DEF:AppConfig:DataClass]\n\n# [/DEF:ConfigModels:Module]\n" }, @@ -32283,7 +32603,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32325,7 +32647,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32367,7 +32691,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32409,7 +32735,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32435,7 +32763,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Top-level feature flags that toggle entire project features on/off.", "RATIONALE": "Features are read from environment variables on bootstrap and persisted in DB." }, @@ -32452,7 +32780,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -32477,7 +32807,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32500,9 +32832,11 @@ "Module", "Function", "Class", - "ADR", "Component", - "Block" + "Block", + "ADR", + "Skill", + "Agent" ] } }, @@ -32544,7 +32878,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32586,7 +32922,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32612,7 +32950,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.", "SEMANTICS": [ "logging", @@ -32625,15 +32963,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Module" - } - }, { "code": "missing_required_tag", "tag": "POST", @@ -32683,7 +33012,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "ContextVars for trace ID and span ID propagation across async boundaries.", "SEMANTICS": [ "contextvar", @@ -32705,7 +33034,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -32730,7 +33061,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32750,7 +33083,10 @@ "detail": { "actual_type": "Data", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -32776,7 +33112,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Dedicated Python logger for all CoT (molecular) log output.", "SEMANTICS": [ "logger", @@ -32796,7 +33132,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -32821,7 +33159,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -32841,7 +33181,10 @@ "detail": { "actual_type": "Data", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -32867,8 +33210,8 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Generate a new UUID4 trace_id, set it in ContextVar, and return it.", - "COMPLEXITY": 1, + "COMPLEXITY": "1", + "PURPOSE": "Generate a new UUID4 trace_id, set it in ContextVar, and return it.", "SEMANTICS": [ "trace_id", "uuid", @@ -32879,10 +33222,13 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", @@ -32891,7 +33237,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -32903,15 +33252,6 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -32926,8 +33266,8 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Set an explicit trace_id into the ContextVar (e.g. from an incoming header).", - "COMPLEXITY": 1, + "COMPLEXITY": "1", + "PURPOSE": "Set an explicit trace_id into the ContextVar (e.g. from an incoming header).", "SEMANTICS": [ "trace_id", "contextvar", @@ -32938,10 +33278,13 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", @@ -32950,7 +33293,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -32962,15 +33308,6 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -32985,8 +33322,8 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Get the current trace_id from ContextVar.", - "COMPLEXITY": 1, + "COMPLEXITY": "1", + "PURPOSE": "Get the current trace_id from ContextVar.", "SEMANTICS": [ "trace_id", "contextvar", @@ -32997,10 +33334,13 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", @@ -33009,7 +33349,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -33021,15 +33364,6 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -33044,8 +33378,8 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Set a new span_id in ContextVar and return the previous span_id for later restoration.", - "COMPLEXITY": 1, + "COMPLEXITY": "1", + "PURPOSE": "Set a new span_id in ContextVar and return the previous span_id for later restoration.", "SEMANTICS": [ "span_id", "contextvar", @@ -33055,10 +33389,13 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", @@ -33067,7 +33404,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -33079,15 +33419,6 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -33102,8 +33433,8 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Restore a previous span_id into the ContextVar.", - "COMPLEXITY": 1, + "COMPLEXITY": "1", + "PURPOSE": "Restore a previous span_id into the ContextVar.", "SEMANTICS": [ "span_id", "contextvar", @@ -33113,10 +33444,13 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", @@ -33125,7 +33459,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -33137,15 +33474,6 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -33160,8 +33488,8 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Core structured logging function that emits a single-line JSON record.", - "COMPLEXITY": 2, + "COMPLEXITY": "2", + "PURPOSE": "Core structured logging function that emits a single-line JSON record.", "SEMANTICS": [ "log", "json", @@ -33171,12 +33499,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -33184,7 +33506,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -33196,15 +33521,6 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -33219,8 +33535,8 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.", - "COMPLEXITY": 2, + "COMPLEXITY": "2", + "PURPOSE": "Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.", "SEMANTICS": [ "logger", "proxy", @@ -33230,12 +33546,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -33243,7 +33553,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -33255,15 +33568,6 @@ "actual_complexity": 2, "contract_type": "Class" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } } ], "anchor_syntax": "def", @@ -33278,20 +33582,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Store the module/component name used as the 'src' field in all log calls." + "PURPOSE": "Store the module/component name used as the 'src' field in all log calls." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", + "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, "contract_type": "Function" @@ -33310,20 +33608,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Log a REASON marker — strict deduction, core logic." + "PURPOSE": "Log a REASON marker — strict deduction, core logic." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", + "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, "contract_type": "Function" @@ -33342,20 +33634,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Log a REFLECT marker — self-check, structural validation." + "PURPOSE": "Log a REFLECT marker — self-check, structural validation." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", + "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, "contract_type": "Function" @@ -33374,20 +33660,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Log an EXPLORE marker — searching, alternatives, violated assumptions." + "PURPOSE": "Log an EXPLORE marker — searching, alternatives, violated assumptions." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", + "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, "contract_type": "Function" @@ -33406,7 +33686,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "A single engine instance is used for the entire application.", "LAYER": "Core", "PURPOSE": "Configures database connection and session management (PostgreSQL-first).", @@ -33448,20 +33728,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -33474,6 +33740,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -33491,6 +33758,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -33508,6 +33776,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -33526,7 +33795,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Base directory for the backend." }, "relations": [], @@ -33542,7 +33811,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33567,7 +33838,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33593,7 +33866,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "URL for the main application database." }, "relations": [], @@ -33609,7 +33882,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33634,7 +33909,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33660,7 +33937,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "URL for the tasks execution database." }, "relations": [], @@ -33676,7 +33953,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33701,7 +33980,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33727,7 +34008,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "URL for the authentication database." }, "relations": [], @@ -33743,7 +34024,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33768,7 +34051,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33794,7 +34079,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "SQLAlchemy engine for mappings database.", "SIDE_EFFECT": "Creates database engine and manages connection pool." }, @@ -33811,7 +34096,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33836,7 +34123,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33859,7 +34148,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -33885,7 +34175,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "SQLAlchemy engine for tasks database." }, "relations": [], @@ -33901,7 +34191,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33926,7 +34218,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -33952,7 +34246,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "SQLAlchemy engine for authentication database." }, "relations": [], @@ -33968,7 +34262,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -33993,7 +34289,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -34019,7 +34317,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PRE": "engine is initialized.", "PURPOSE": "A session factory for the main mappings database." }, @@ -34033,6 +34331,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -34047,7 +34354,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PRE": "tasks_engine is initialized.", "PURPOSE": "A session factory for the tasks execution database." }, @@ -34061,6 +34368,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -34075,7 +34391,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PRE": "auth_engine is initialized.", "PURPOSE": "A session factory for the authentication database." }, @@ -34089,6 +34405,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -34103,7 +34428,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to application database where profile table is stored.", "PURPOSE": "Applies additive schema upgrades for user_dashboard_preferences table." @@ -34147,6 +34472,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34165,7 +34491,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Applies additive schema upgrades for user_dashboard_preferences table (health fields)." }, "relations": [ @@ -34189,6 +34515,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34207,7 +34534,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Applies additive schema upgrades for llm_validation_results table." }, "relations": [ @@ -34231,6 +34558,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34249,7 +34577,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to application database.", "PURPOSE": "Applies additive schema upgrades for git_server_configs table." @@ -34293,6 +34621,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34311,7 +34640,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to authentication database.", "PURPOSE": "Applies additive schema upgrades for auth users table." @@ -34355,6 +34684,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34373,7 +34703,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "connection_configs table exists without dropping existing data.", "PRE": "bind_engine points to the application database.", "PURPOSE": "Ensures the external connection registry table exists in the main database." @@ -34417,6 +34747,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34435,7 +34766,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "New enum values are available without data loss.", "PRE": "bind_engine points to application database with imported_filters table.", "PURPOSE": "Adds missing FilterSource enum values to the PostgreSQL native filtersource type." @@ -34479,6 +34810,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34497,7 +34829,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "Missing additive columns across legacy dataset review tables are created without removing existing data.", "PRE": "bind_engine points to the application database where dataset review tables are stored.", "PURPOSE": "Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.", @@ -34530,6 +34862,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34547,6 +34880,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34590,6 +34924,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -34612,7 +34955,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Database tables created in all databases.", "PRE": "engine, tasks_engine and auth_engine are initialized.", "PURPOSE": "Initializes the database by creating all tables.", @@ -34678,6 +35021,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -34695,6 +35039,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -34712,6 +35057,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -34730,7 +35076,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Session is closed after use.", "PRE": "SessionLocal is initialized.", "PURPOSE": "Dependency for getting a database session.", @@ -34781,6 +35127,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34799,7 +35146,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Session is closed after use.", "PRE": "TasksSessionLocal is initialized.", "PURPOSE": "Dependency for getting a tasks database session.", @@ -34850,6 +35197,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34868,7 +35216,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "None -> Output[sqlalchemy.orm.Session]", "POST": "Session is closed after use.", "PRE": "AuthSessionLocal is initialized.", @@ -34929,6 +35277,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -34947,7 +35296,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[env_file_path] -> Output[encryption_key]", "INVARIANT": "Runtime key resolution never falls back to an ephemeral secret.", "LAYER": "Infra", @@ -34971,7 +35320,26 @@ "target_ref": "[LoggerModule]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:EncryptionKeyModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: encryption, key, bootstrap, environment, startup\n# @PURPOSE: Resolve and persist the Fernet encryption key required by runtime services.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Runtime key resolution never falls back to an ephemeral secret.\n# @PRE: Runtime environment can read process variables and target .env path is writable when key generation is required.\n# @POST: A valid Fernet key is available to runtime services via ENCRYPTION_KEY.\n# @SIDE_EFFECT: May append ENCRYPTION_KEY entry into backend .env file and set process environment variable.\n# @DATA_CONTRACT: Input[env_file_path] -> Output[encryption_key]\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nfrom cryptography.fernet import Fernet\n\nfrom .logger import logger, belief_scope\n\nDEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / \".env\"\n\n\n# [DEF:ensure_encryption_key:Function]\n# @PURPOSE: Ensure backend runtime has a persistent valid Fernet key.\n# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment.\n# @POST: Returns a valid Fernet key and guarantees it is present in process environment.\n# @SIDE_EFFECT: May create or append backend/.env when key is missing.\ndef ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str:\n with belief_scope(\"ensure_encryption_key\", f\"env_file_path={env_file_path}\"):\n existing_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if existing_key:\n Fernet(existing_key.encode())\n logger.reason(\"Using ENCRYPTION_KEY from process environment.\")\n return existing_key\n\n if env_file_path.exists():\n for raw_line in env_file_path.read_text(encoding=\"utf-8\").splitlines():\n if raw_line.startswith(\"ENCRYPTION_KEY=\"):\n persisted_key = raw_line.partition(\"=\")[2].strip()\n if persisted_key:\n Fernet(persisted_key.encode())\n os.environ[\"ENCRYPTION_KEY\"] = persisted_key\n logger.reason(f\"Loaded ENCRYPTION_KEY from {env_file_path}.\")\n return persisted_key\n\n generated_key = Fernet.generate_key().decode()\n with env_file_path.open(\"a\", encoding=\"utf-8\") as env_file:\n if env_file.tell() > 0:\n env_file.write(\"\\n\")\n env_file.write(f\"ENCRYPTION_KEY={generated_key}\\n\")\n\n os.environ[\"ENCRYPTION_KEY\"] = generated_key\n logger.reason(f\"Generated ENCRYPTION_KEY and persisted it to {env_file_path}.\")\n logger.reflect(\"Encryption key is available for runtime services.\")\n return generated_key\n\n\n# [/DEF:ensure_encryption_key:Function]\n\n# [/DEF:EncryptionKeyModule:Module]\n" }, @@ -35009,6 +35377,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -35031,13 +35408,13 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "BRIEF": "Application logging system with CotJsonFormatter producing molecular CoT JSON output.", "COMPLEXITY": 5, "DATA_CONTRACT": "All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}", "INVARIANT": "CotJsonFormatter.format() always returns valid single-line JSON string.", "LAYER": "Core", "POST": "All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.", "PRE": "Python 3.7+ with cot_logger ContextVars available.", + "PURPOSE": "Application logging system with CotJsonFormatter producing molecular CoT JSON output.", "RATIONALE": "Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter produces structured JSON consistent with cot_logger.py MarkerLogger protocol.", "REJECTED": "Keeping both formatters side-by-side was rejected (two inconsistent output formats would confuse log consumers).", "SIDE_EFFECT": "Configures global logger handlers and formatters; seeds global _enable_belief_state and _task_log_level; writes JSON to console/files." @@ -35063,35 +35440,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -35104,6 +35452,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -35122,8 +35471,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id." }, "relations": [ { @@ -35133,23 +35482,7 @@ "target_ref": "[CotJsonFormat]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # For records that already come as JSON strings (e.g. from cot_logger.log()),\n # detect and pass through the message directly as intent.\n if not marker:\n marker = \"REASON\" # default for plain messages\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(timezone.utc).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n\n\n# #endregion CotJsonFormatter\n" }, @@ -35162,27 +35495,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # For records that already come as JSON strings (e.g. from cot_logger.log()),\n # detect and pass through the message directly as intent.\n if not marker:\n marker = \"REASON\" # default for plain messages\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(timezone.utc).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n" }, @@ -35195,10 +35512,10 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.", "COMPLEXITY": 4, "POST": "Logger levels, handlers, formatters, belief state flag, and task log level are updated. The 'cot' logger has CotJsonFormatter with propagation disabled.", "PRE": "config is a valid LoggingConfig instance.", + "PURPOSE": "Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.", "SIDE_EFFECT": "Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories." }, "relations": [ @@ -35215,23 +35532,7 @@ "target_ref": "[CotLoggerModule]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]\n# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.\n# @RELATION CALLS -> [CotJsonFormatter]\n# @RELATION CALLS -> [CotLoggerModule]\n# @PRE config is a valid LoggingConfig instance.\n# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated. The 'cot' logger has CotJsonFormatter with propagation disabled.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.\ndef configure_logger(config):\n global _enable_belief_state, _task_log_level\n _enable_belief_state = config.enable_belief_state\n _task_log_level = config.task_log_level.upper()\n\n # Set logger level\n level = getattr(logging, config.level.upper(), logging.INFO)\n logger.setLevel(level)\n\n # Remove existing file handlers\n handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n for h in handlers_to_remove:\n logger.removeHandler(h)\n h.close()\n\n # Add file handler if file_path is set\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n\n file_handler = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n file_handler.setFormatter(CotJsonFormatter())\n logger.addHandler(file_handler)\n\n # Update existing handlers' formatters to CotJsonFormatter\n for handler in logger.handlers:\n if not isinstance(handler, RotatingFileHandler):\n handler.setFormatter(CotJsonFormatter())\n\n # Also configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n # Remove all existing handlers from the cot logger\n for h in list(cot_logger.handlers):\n cot_logger.removeHandler(h)\n h.close()\n # Add a console handler with CotJsonFormatter (if file path set, add file handler too)\n cot_console = logging.StreamHandler()\n cot_console.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_console)\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n cot_file = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n cot_file.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_file)\n # Disable propagation so cot messages don't double-emit via root loggers\n cot_logger.propagate = False\n\n# #endregion configure_logger\n" }, @@ -35247,17 +35548,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\ndef get_task_log_level() -> str:\n \"\"\"Returns the current task log level filter.\"\"\"\n return _task_log_level\n\n# #endregion get_task_log_level\n" }, @@ -35270,27 +35561,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Checks if a log level should be recorded based on the current task log level threshold.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Checks if a log level should be recorded based on the current task log level threshold." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region should_log_task_level [C:2] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on the current task log level threshold.\ndef should_log_task_level(level: str) -> bool:\n \"\"\"Checks if a log level should be recorded based on task_log_level setting.\"\"\"\n level_order = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n current_level = _task_log_level.upper()\n check_level = level.upper()\n\n current_order = level_order.get(current_level, 1) # Default to INFO\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n\n# #endregion should_log_task_level\n" }, @@ -35303,17 +35578,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler." }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "COMPLEXITY", @@ -35325,7 +35594,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -35337,6 +35608,33 @@ "actual_complexity": 2, "contract_type": "Global" } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Global'", + "detail": { + "actual_type": "Global", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Global' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Global" + } } ], "anchor_syntax": "region", @@ -35351,27 +35649,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n\n# #endregion explore\n" }, @@ -35384,27 +35666,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,info]\n# @BRIEF Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n\n# #endregion reason\n" }, @@ -35417,27 +35683,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n\n# #endregion reflect\n" }, @@ -35450,27 +35700,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Decorator that wraps a function in a belief_scope context manager.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Decorator that wraps a function in a belief_scope context manager." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief_scope,wrapper]\n# @BRIEF Decorator that wraps a function in a belief_scope context manager.\ndef believed(anchor_id: str):\n # #region believed.decorator [C:1] [TYPE Function] [SEMANTICS decorator,inner]\n def decorator(func):\n # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n return wrapper\n # #endregion believed.decorator\n return decorator\n\n# #endregion believed\n" }, @@ -35486,17 +35720,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region believed.decorator [C:1] [TYPE Function] [SEMANTICS decorator,inner]\n def decorator(func):\n # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n return wrapper\n # #endregion believed.decorator\n" }, @@ -35512,17 +35736,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n" }, @@ -35535,7 +35749,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Unit tests for logger module — CoT JSON format markers." }, @@ -36116,7 +36330,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None]", "INVARIANT": "sync_environment must handle remote API failures gracefully.", "LAYER": "Core", @@ -36148,25 +36362,29 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "unknown_tag", "tag": "TEST_DATA", "message": "@TEST_DATA is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -36181,7 +36399,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[db_session] -> Output[IdMappingService]", "INVARIANT": "self.db remains the authoritative session for all mapping operations.", "POST": "Service instance provides scheduler control and environment-scoped mapping synchronization APIs.", @@ -36225,6 +36443,24 @@ "actual_complexity": 5, "contract_type": "Class" } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -36249,6 +36485,15 @@ "tag": "PARAM", "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -36293,6 +36538,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -36319,6 +36573,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -36350,6 +36613,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -36369,7 +36641,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "FastAPI/Starlette middleware package for request-level context and tracing.", "SEMANTICS": [ "middleware", @@ -36391,7 +36663,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -36416,7 +36690,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -36436,7 +36712,10 @@ "detail": { "actual_type": "Package", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -36462,7 +36741,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request.", "SEMANTICS": [ "middleware", @@ -36474,15 +36753,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "missing_required_tag", "tag": "RELATION", @@ -36505,8 +36775,8 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Starlette BaseHTTPMiddleware that seeds a trace_id per request.", - "COMPLEXITY": 2, + "COMPLEXITY": "2", + "PURPOSE": "Starlette BaseHTTPMiddleware that seeds a trace_id per request.", "SEMANTICS": [ "middleware", "trace", @@ -36516,12 +36786,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -36529,7 +36793,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -36541,15 +36808,6 @@ "actual_complexity": 2, "contract_type": "Class" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } } ], "anchor_syntax": "def", @@ -36564,20 +36822,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Standard BaseHTTPMiddleware initialiser." + "PURPOSE": "Standard BaseHTTPMiddleware initialiser." }, "relations": [], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", + "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, "contract_type": "Function" @@ -36596,8 +36848,8 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Dispatch handler that seeds trace_id before passing to the next middleware.", - "COMPLEXITY": 2, + "COMPLEXITY": "2", + "PURPOSE": "Dispatch handler that seeds trace_id before passing to the next middleware.", "SEMANTICS": [ "dispatch", "trace", @@ -36608,12 +36860,6 @@ }, "relations": [], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -36621,7 +36867,10 @@ "detail": { "actual_type": "Function", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -36633,15 +36882,6 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } } ], "anchor_syntax": "def", @@ -36657,26 +36897,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:MigrationPackage:Module]\n\nfrom .dry_run_orchestrator import MigrationDryRunService\nfrom .archive_parser import MigrationArchiveParser\n\n__all__ = [\"MigrationDryRunService\", \"MigrationArchiveParser\"]\n\n# [/DEF:MigrationPackage:Module]\n" }, @@ -36689,7 +36910,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Parsing is read-only and never mutates archive files.", "LAYER": "Core", "PURPOSE": "Parse Superset export ZIP archives into normalized object catalogs for diffing.", @@ -36718,20 +36939,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } } ], "anchor_syntax": "def", @@ -36769,6 +36976,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -36790,6 +37006,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -36807,6 +37024,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -36824,6 +37042,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -36874,6 +37093,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -36933,6 +37161,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -36978,6 +37215,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -36992,7 +37238,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Dry run is informative only and must not mutate target environment.", "LAYER": "Core", "PURPOSE": "Compute pre-flight migration diff and risk scoring without apply.", @@ -37039,20 +37285,6 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } } ], "anchor_syntax": "def", @@ -37120,6 +37352,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -37141,6 +37382,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37158,6 +37400,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37175,6 +37418,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37192,6 +37436,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37209,6 +37454,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37226,6 +37472,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37243,6 +37490,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37260,6 +37508,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37281,7 +37530,17 @@ "PURPOSE": "Resolve UUID mapping for optional DB config replacement." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_load_db_mapping:Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> Dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n\n # [/DEF:_load_db_mapping:Function]\n" }, @@ -37297,7 +37556,17 @@ "PURPOSE": "Merge extracted resources by UUID to avoid duplicates." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_accumulate_objects:Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: Dict[str, Dict[str, Dict[str, Any]]],\n source: Dict[str, List[Dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n\n # [/DEF:_accumulate_objects:Function]\n" }, @@ -37313,7 +37582,17 @@ "PURPOSE": "Build UUID-index map for normalized resources." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_index_by_uuid:Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: List[Dict[str, Any]]\n ) -> Dict[str, Dict[str, Any]]:\n indexed: Dict[str, Dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n\n # [/DEF:_index_by_uuid:Function]\n" }, @@ -37337,6 +37616,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -37362,7 +37650,17 @@ "PURPOSE": "Pull target metadata and normalize it into comparable signatures." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_build_target_signatures:Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> Dict[str, List[Dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n\n # [/DEF:_build_target_signatures:Function]\n" }, @@ -37378,7 +37676,17 @@ "PURPOSE": "Build risk items for missing datasource, broken refs, overwrite, owner mismatch." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:_build_risks:Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: Dict[str, List[Dict[str, Any]]],\n target_objects: Dict[str, List[Dict[str, Any]]],\n diff: Dict[str, Dict[str, List[Dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> List[Dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n\n # [/DEF:_build_risks:Function]\n" }, @@ -37391,7 +37699,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Module[build_risks, score_risks]", "INVARIANT": "Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.", "LAYER": "Domain", @@ -37491,40 +37799,123 @@ } }, { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "TEST_SCENARIO", + "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_REACTIVITY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_REACTIVITY", + "message": "@UX_REACTIVITY is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_RECOVERY is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } }, { "code": "invalid_relation_predicate", @@ -37538,6 +37929,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37555,6 +37947,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37572,6 +37965,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37589,6 +37983,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -37642,6 +38037,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -37699,6 +38103,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -37769,6 +38182,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -37835,6 +38257,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -37857,7 +38288,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]", "INVARIANT": "ZIP structure and non-targeted metadata must remain valid after transformation.", "LAYER": "Domain", @@ -37902,6 +38333,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -37914,6 +38363,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -37931,6 +38381,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -37948,6 +38399,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -37965,6 +38417,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -37994,6 +38447,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -38015,6 +38477,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "CONTAINS" @@ -38083,6 +38546,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38162,6 +38634,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -38227,6 +38708,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38297,6 +38787,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -38354,7 +38853,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -38368,17 +38869,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" + "actual_complexity": 1, + "contract_type": "Class" } }, { @@ -38388,7 +38884,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -38448,6 +38947,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38492,6 +39000,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38536,6 +39053,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38580,6 +39106,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38624,6 +39159,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38668,6 +39212,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38712,6 +39265,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -38761,6 +39323,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -38800,7 +39371,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -38814,17 +39387,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" + "actual_complexity": 1, + "contract_type": "Class" } }, { @@ -38834,7 +39402,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -38869,7 +39440,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.", "SEMANTICS": [ @@ -38895,7 +39466,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -38908,20 +39481,6 @@ "contract_type": "Class" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -38929,7 +39488,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -38978,6 +39540,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39022,6 +39593,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39066,6 +39646,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39112,6 +39701,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -39156,6 +39754,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -39207,6 +39814,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -39226,7 +39842,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Manages scheduled tasks using APScheduler.", "SEMANTICS": [ @@ -39244,22 +39860,7 @@ "target_ref": "TaskManager" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:SchedulerModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, apscheduler, cron, backup\n# @PURPOSE: Manages scheduled tasks using APScheduler.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> TaskManager\n\n# [SECTION: IMPORTS]\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\nfrom .logger import logger, belief_scope\nfrom .config_manager import ConfigManager\nfrom .cot_logger import MarkerLogger, get_trace_id, set_trace_id\n\nlog = MarkerLogger(\"SchedulerService\")\nlog_tsc = MarkerLogger(\"ThrottledSchedulerConfigurator\")\nimport asyncio\nfrom datetime import datetime, time, timedelta, date\n# [/SECTION]\n\n\n# [DEF:SchedulerService:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, service, apscheduler\n# @PURPOSE: Provides a service to manage scheduled backup tasks.\n# @RELATION: DEPENDS_ON -> [ThrottledSchedulerConfigurator]\nclass SchedulerService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the scheduler service with task and config managers.\n # @PRE: task_manager and config_manager must be provided.\n # @POST: Scheduler instance is created but not started.\n def __init__(self, task_manager, config_manager: ConfigManager):\n with belief_scope(\"SchedulerService.__init__\"):\n self.task_manager = task_manager\n self.config_manager = config_manager\n self.scheduler = BackgroundScheduler()\n self.loop = asyncio.get_event_loop()\n\n # [/DEF:__init__:Function]\n\n # [DEF:start:Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n log.reflect(\"Scheduler started\")\n self.load_schedules()\n\n # [/DEF:start:Function]\n\n # [DEF:stop:Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n log.reflect(\"Scheduler stopped\")\n\n # [/DEF:stop:Function]\n\n # [DEF:load_schedules:Function]\n # @PURPOSE: Loads backup schedules from configuration and registers them.\n # @PRE: config_manager must have valid configuration.\n # @POST: All enabled backup jobs are added to the scheduler.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n\n # [/DEF:load_schedules:Function]\n\n # [DEF:add_backup_job:Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n log.reflect(\n \"Scheduled backup job added\",\n payload={\"env_id\": env_id, \"cron\": cron_expression},\n )\n except Exception as e:\n log.explore(\"Failed to add backup job\", error=str(e), payload={\"env_id\": env_id, \"cron\": cron_expression})\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n\n # [/DEF:add_backup_job:Function]\n\n # [DEF:_trigger_backup:Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n log.reason(\"Triggering scheduled backup\", payload={\"env_id\": env_id})\n\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n log.explore(\"Backup already running, skipping scheduled run\", error=\"Duplicate backup prevented\", payload={\"env_id\": env_id})\n return\n\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n trace_id = get_trace_id()\n\n async def _backup_task():\n if trace_id:\n set_trace_id(trace_id)\n await self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n )\n\n asyncio.run_coroutine_threadsafe(_backup_task(), self.loop)\n\n # [/DEF:_trigger_backup:Function]\n\n\n# [/DEF:SchedulerService:Class]\n\n\n# [DEF:ThrottledSchedulerConfigurator:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: scheduler, throttling, distribution\n# @PURPOSE: Distributes validation tasks evenly within an execution window.\n# @PRE: Validation policies provide a finite dashboard list and a valid execution window.\n# @POST: Produces deterministic per-dashboard run timestamps within the configured window.\n# @RELATION: DEPENDS_ON -> SchedulerModule\n# @INVARIANT: Returned schedule size always matches number of dashboard IDs.\n# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows.\n# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]\nclass ThrottledSchedulerConfigurator:\n # [DEF:calculate_schedule:Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n\n total_seconds = (end_dt - start_dt).total_seconds()\n\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n log_tsc.explore(\"Window size is zero or negative, falling back to start time\", error=f\"total_seconds={total_seconds}\", payload={\"n\": n})\n return [start_dt] * n\n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n\n if n == 1:\n return [start_dt]\n\n interval = total_seconds / (n - 1) if n > 1 else 0\n\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n log_tsc.explore(\"Window too small for task distribution\", error=f\"interval={interval:.2f}s\", payload={\"n\": n})\n\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n\n return scheduled_times\n\n # [/DEF:calculate_schedule:Function]\n\n\n# [/DEF:ThrottledSchedulerConfigurator:Class]\n\n# [/DEF:SchedulerModule:Module]\n" }, @@ -39272,7 +39873,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Provides a service to manage scheduled backup tasks.", "SEMANTICS": [ "scheduler", @@ -39296,7 +39897,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -39345,6 +39949,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39382,6 +39995,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39419,6 +40041,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39463,6 +40094,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39507,6 +40147,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39521,7 +40170,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]", "INVARIANT": "Returned schedule size always matches number of dashboard IDs.", "POST": "Produces deterministic per-dashboard run timestamps within the configured window.", @@ -39550,7 +40199,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -39562,6 +40214,24 @@ "actual_complexity": 5, "contract_type": "Class" } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -39609,6 +40279,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -39623,7 +40302,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "All network operations must use the internal APIClient instance.", "LAYER": "Infra", "PUBLIC_API": "SupersetClient", @@ -39688,6 +40367,15 @@ "tag": "PUBLIC_API", "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -39702,7 +40390,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами." }, "relations": [ @@ -39798,7 +40486,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers.", "SEMANTICS": [ @@ -39851,7 +40539,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент." }, "relations": [ @@ -39881,7 +40569,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Authenticates the client using the configured credentials." }, "relations": [ @@ -39905,11 +40593,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Возвращает базовые HTTP-заголовки, используемые сетевым клиентом." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientHeaders:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.\n def headers(self) -> dict:\n with belief_scope(\"headers\"):\n return self.network.headers\n\n # [/DEF:SupersetClientHeaders:Function]\n" }, @@ -39922,11 +40620,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Ensures query parameters have default page and page_size." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientValidateQueryParams:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Ensures query parameters have default page and page_size.\n def _validate_query_params(self, query: Optional[Dict]) -> Dict:\n with belief_scope(\"_validate_query_params\"):\n # Superset list endpoints commonly cap page_size at 100.\n # Using 100 avoids partial fetches when larger values are silently truncated.\n base_query = {\"page\": 0, \"page_size\": 100}\n return {**base_query, **(query or {})}\n\n # [/DEF:SupersetClientValidateQueryParams:Function]\n" }, @@ -39939,7 +40647,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Fetches the total number of items for a given endpoint." }, "relations": [ @@ -39951,6 +40659,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -39973,7 +40690,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Iterates through all pages to collect all data items." }, "relations": [ @@ -39985,6 +40702,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -40007,7 +40733,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Performs the actual multipart upload for import." }, "relations": [ @@ -40019,6 +40745,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -40041,11 +40776,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Validates that the export response is a non-empty ZIP archive." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientValidateExportResponse:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the export response is a non-empty ZIP archive.\n def _validate_export_response(self, response: Response, dashboard_id: int) -> None:\n with belief_scope(\"_validate_export_response\"):\n content_type = response.headers.get(\"Content-Type\", \"\")\n if \"application/zip\" not in content_type:\n raise SupersetAPIError(\n f\"Получен не ZIP-архив (Content-Type: {content_type})\"\n )\n if not response.content:\n raise SupersetAPIError(\"Получены пустые данные при экспорте\")\n\n # [/DEF:SupersetClientValidateExportResponse:Function]\n" }, @@ -40058,11 +40803,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Determines the filename for an exported dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientResolveExportFilename:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Determines the filename for an exported dashboard.\n def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:\n with belief_scope(\"_resolve_export_filename\"):\n filename = get_filename_from_headers(dict(response.headers))\n if not filename:\n from datetime import datetime\n\n timestamp = datetime.now().strftime(\"%Y%m%dT%H%M%S\")\n filename = f\"dashboard_export_{dashboard_id}_{timestamp}.zip\"\n log.reflect(\n \"Export filename not found in headers, using generated name\",\n payload={\"dashboard_id\": dashboard_id, \"generated_filename\": filename},\n )\n return filename\n\n # [/DEF:SupersetClientResolveExportFilename:Function]\n" }, @@ -40075,11 +40830,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Validates that the file to be imported is a valid ZIP with metadata.yaml." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientValidateImportFile:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml.\n def _validate_import_file(self, zip_path: Union[str, Path]) -> None:\n with belief_scope(\"_validate_import_file\"):\n path = Path(zip_path)\n if not path.exists():\n raise FileNotFoundError(f\"Файл {zip_path} не существует\")\n if not zipfile.is_zipfile(path):\n raise SupersetAPIError(f\"Файл {zip_path} не является ZIP-архивом\")\n with zipfile.ZipFile(path, \"r\") as zf:\n if not any(n.endswith(\"metadata.yaml\") for n in zf.namelist()):\n raise SupersetAPIError(\n f\"Архив {zip_path} не содержит 'metadata.yaml'\"\n )\n\n # [/DEF:SupersetClientValidateImportFile:Function]\n" }, @@ -40092,7 +40857,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Resolves a dashboard ID from either an ID or a slug." }, "relations": [ @@ -40104,6 +40869,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -40126,7 +40900,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches all resources of a given type with id, uuid, and name columns." }, "relations": [ @@ -40150,7 +40924,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Chart domain mixin for SupersetClient — list, get, extract IDs from layout.", "SEMANTICS": [ @@ -40182,7 +40956,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches a single chart by ID." }, "relations": [ @@ -40206,7 +40980,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches all charts with pagination support." }, "relations": [ @@ -40230,11 +41004,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Traverses dashboard layout metadata and extracts chart IDs from common keys." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientExtractChartIdsFromLayout:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys.\n def _extract_chart_ids_from_layout(\n self, payload: Any\n ) -> set:\n with belief_scope(\"_extract_chart_ids_from_layout\"):\n found = set()\n\n def walk(node):\n if isinstance(node, dict):\n for key, value in node.items():\n if key in (\"chartId\", \"chart_id\", \"slice_id\", \"sliceId\"):\n try:\n found.add(int(value))\n except (TypeError, ValueError):\n pass\n if key == \"id\" and isinstance(value, str):\n match = re.match(r\"^CHART-(\\d+)$\", value)\n if match:\n try:\n found.add(int(match.group(1)))\n except ValueError:\n pass\n walk(value)\n elif isinstance(node, list):\n for item in node:\n walk(item)\n\n walk(payload)\n return found\n\n # [/DEF:SupersetClientExtractChartIdsFromLayout:Function]\n" }, @@ -40247,7 +41031,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Dashboard CRUD mixin for SupersetClient — detail, export, import, delete.", "SEMANTICS": [ @@ -40293,7 +41077,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches detailed dashboard information including related charts and datasets." }, "relations": [ @@ -40324,17 +41108,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " # [DEF:extract_dataset_id_from_form_data:Function]\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n # [/DEF:extract_dataset_id_from_form_data:Function]\n" }, @@ -40347,7 +41121,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Экспортирует дашборд в виде ZIP-архива.", "SIDE_EFFECT": "Performs network I/O to download archive." }, @@ -40382,7 +41156,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Импортирует дашборд из ZIP-файла.", "SIDE_EFFECT": "Performs network I/O to upload archive." }, @@ -40423,7 +41197,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Удаляет дашборд по его ID или slug.", "SIDE_EFFECT": "Deletes resource from upstream Superset environment." }, @@ -40458,7 +41232,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Dashboard native filter extraction mixin for SupersetClient.", "SEMANTICS": [ @@ -40490,7 +41264,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches a single dashboard by ID or slug." }, "relations": [ @@ -40514,7 +41288,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Fetches stored dashboard permalink state by permalink key." }, "relations": [ @@ -40548,7 +41322,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Fetches stored native filter state by filter state key." }, "relations": [ @@ -40582,7 +41356,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Extract native filters dataMask from a permalink key." }, "relations": [ @@ -40606,7 +41380,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Extract native filters from a native_filters_key URL parameter." }, "relations": [ @@ -40630,7 +41404,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Parse a Superset dashboard URL and extract native filter state if present." }, "relations": [ @@ -40660,7 +41434,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Dashboard listing mixin for SupersetClient — paginated list, summary projection.", "SEMANTICS": [ @@ -40698,7 +41472,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Получает полный список дашбордов, автоматически обрабатывая пагинацию." }, "relations": [ @@ -40722,7 +41496,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches a single dashboards page from Superset without iterating all pages." }, "relations": [ @@ -40746,7 +41520,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches dashboard metadata optimized for the grid." }, "relations": [ @@ -40770,7 +41544,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches one page of dashboard metadata optimized for the grid." }, "relations": [ @@ -40794,7 +41568,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Database domain mixin for SupersetClient — list, get, summary, by_uuid.", "SEMANTICS": [ @@ -40827,7 +41601,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Получает полный список баз данных." }, "relations": [ @@ -40851,7 +41625,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Получает информацию о конкретной базе данных по её ID." }, "relations": [ @@ -40875,7 +41649,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetch a summary of databases including uuid, name, and engine." }, "relations": [ @@ -40899,7 +41673,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Find a database by its UUID." }, "relations": [ @@ -40923,7 +41697,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Dataset domain mixin for SupersetClient — list, get, detail, update.", "SEMANTICS": [ @@ -40956,7 +41730,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Получает полный список датасетов, автоматически обрабатывая пагинацию." }, "relations": [ @@ -40980,7 +41754,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches dataset metadata optimized for the Dataset Hub grid." }, "relations": [ @@ -41004,7 +41778,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Fetches detailed dataset information including columns and linked dashboards." }, "relations": [ @@ -41034,7 +41808,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Получает информацию о конкретном датасете по его ID." }, "relations": [ @@ -41058,7 +41832,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Обновляет данные датасета по его ID.", "SIDE_EFFECT": "Modifies resource in upstream Superset environment." }, @@ -41093,7 +41867,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Infra", "PURPOSE": "Dataset preview compilation mixin for SupersetClient — build query context, compile SQL.", "SEMANTICS": [ @@ -41160,7 +41934,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output." }, "relations": [], @@ -41214,7 +41988,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic." }, "relations": [], @@ -41268,7 +42042,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "PURPOSE": "Build a reduced-scope chart-data query context for deterministic dataset preview compilation." }, "relations": [], @@ -41322,7 +42096,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Filter normalization and SQL extraction helpers for dataset preview compilation.", "SEMANTICS": [ @@ -41362,7 +42136,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Convert execution mappings into Superset chart-data filter objects." }, "relations": [], @@ -41389,7 +42163,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Normalize compiled SQL from either chart-data or legacy form_data preview responses." }, "relations": [], @@ -41416,7 +42190,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Infra", "PURPOSE": "User/owner payload normalization helpers for Superset client responses.", "SEMANTICS": [ @@ -41458,7 +42232,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Mixin providing user/owner payload normalization for Superset API responses." }, "relations": [ @@ -41492,11 +42266,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Normalize dashboard owners payload to stable display labels." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientExtractOwnerLabels:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> List[str]:\n if owners_payload is None:\n return []\n\n owners_list: List[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n\n normalized: List[str] = []\n for owner in owners_list:\n label: Optional[str] = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n\n # [/DEF:SupersetClientExtractOwnerLabels:Function]\n" }, @@ -41509,11 +42293,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Normalize user payload to a stable display name." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientExtractUserDisplay:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: Optional[str], user_payload: Optional[Dict]\n ) -> Optional[str]:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n\n # [/DEF:SupersetClientExtractUserDisplay:Function]\n" }, @@ -41526,11 +42320,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Convert scalar value to non-empty user-facing text." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetClientSanitizeUserText:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:SupersetClientSanitizeUserText:Function]\n" }, @@ -41543,7 +42347,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Adapter never leaks raw upstream payload shape to API consumers.", "LAYER": "Core", "PURPOSE": "Provides environment-scoped Superset account lookup adapter with stable normalized output.", @@ -41586,20 +42390,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -41612,6 +42402,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -41629,6 +42420,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -41646,6 +42438,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -41664,7 +42457,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Lookup Superset users and normalize candidates for profile binding." }, "relations": [ @@ -41694,6 +42487,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -41711,6 +42505,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -41752,6 +42547,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -41791,6 +42595,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -41835,6 +42648,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -41879,6 +42701,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -41899,26 +42730,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TaskManagerPackage:Module]\n\nfrom .models import Task, TaskStatus, LogEntry\nfrom .manager import TaskManager\n\n__all__ = [\"TaskManager\", \"Task\", \"TaskStatus\", \"LogEntry\"]\n\n# [/DEF:TaskManagerPackage:Module]\n" }, @@ -41931,7 +42743,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Verify TaskContext preserves optional background task scheduler across sub-context creation.", "SEMANTICS": [ "tests", @@ -41949,15 +42761,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -41970,6 +42773,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -42053,9 +42857,9 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -42231,12 +43035,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -42292,7 +43090,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Implements task cleanup and retention policies, including associated logs.", "SEMANTICS": [ @@ -42322,22 +43120,7 @@ "target_ref": "[ConfigManager]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TaskCleanupModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: task, cleanup, retention, logs\n# @PURPOSE: Implements task cleanup and retention policies, including associated logs.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n\nfrom typing import List\nfrom .persistence import TaskPersistenceService, TaskLogPersistenceService\nfrom ..logger import logger, belief_scope\nfrom ..config_manager import ConfigManager\n\n# [DEF:TaskCleanupService:Class]\n# @PURPOSE: Provides methods to clean up old task records and their associated logs.\n# @COMPLEXITY: 3\n# @RELATION: DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\nclass TaskCleanupService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the cleanup service with dependencies.\n # @PRE: persistence_service and config_manager are valid.\n # @POST: Cleanup service is ready.\n def __init__(\n self,\n persistence_service: TaskPersistenceService,\n log_persistence_service: TaskLogPersistenceService,\n config_manager: ConfigManager\n ):\n self.persistence_service = persistence_service\n self.log_persistence_service = log_persistence_service\n self.config_manager = config_manager\n # [/DEF:__init__:Function]\n\n # [DEF:run_cleanup:Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n \n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n \n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: List[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n \n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n \n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n \n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # [/DEF:run_cleanup:Function]\n \n # [DEF:delete_task_with_logs:Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n \n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n \n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # [/DEF:delete_task_with_logs:Function]\n\n# [/DEF:TaskCleanupService:Class]\n# [/DEF:TaskCleanupModule:Module]\n" }, @@ -42350,7 +43133,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Provides methods to clean up old task records and their associated logs." }, "relations": [ @@ -42409,6 +43192,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -42453,6 +43245,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -42467,7 +43268,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]", "INVARIANT": "Each TaskContext is bound to a single task execution.", "LAYER": "Core", @@ -42499,17 +43300,21 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } } ], @@ -42525,7 +43330,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]", "INVARIANT": "logger is always a valid TaskLogger instance.", "POST": "Instance exposes immutable task identity with logger, params, and optional background task access.", @@ -42557,7 +43362,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -42592,10 +43400,42 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -42635,6 +43475,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -42679,6 +43528,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -42723,6 +43581,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -42765,6 +43632,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -42811,6 +43687,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -42862,6 +43747,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -42881,7 +43775,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[plugin_id, params] -> Model[Task, LogEntry]", "INVARIANT": "Task IDs are unique.", "LAYER": "Core", @@ -42943,20 +43837,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -42978,6 +43858,24 @@ "contract_type": "Module" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -42990,6 +43888,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43007,6 +43906,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43024,6 +43924,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43041,6 +43942,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43058,6 +43960,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43075,6 +43978,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43092,6 +43996,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43110,7 +44015,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[plugin_id, params] -> Output[Task]", "INVARIANT": "Log entries are never deleted after being added to a task.", "LAYER": "Core", @@ -43178,7 +44083,9 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Skill", + "Agent" ] } }, @@ -43191,20 +44098,6 @@ "contract_type": "Class" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -43212,7 +44105,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -43225,6 +44121,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -43237,6 +44151,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43254,6 +44169,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43271,6 +44187,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43288,6 +44205,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43305,6 +44223,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43322,6 +44241,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43339,6 +44259,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43357,7 +44278,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]]", "INVARIANT": "Registry membership is keyed by unique task id and survives log streaming side channels.", "POST": "Each live task id resolves to at most one active Task node and optional pause future.", @@ -43390,7 +44311,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43403,29 +44325,6 @@ "contract_type": "Block" } }, - { - "code": "tag_not_for_contract_type", - "tag": "INVARIANT", - "message": "@INVARIANT is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, { "code": "tag_not_for_contract_type", "tag": "POST", @@ -43436,7 +44335,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43459,7 +44359,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43482,7 +44383,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43495,6 +44397,24 @@ "contract_type": "Block" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -43507,6 +44427,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43524,6 +44445,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43542,7 +44464,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events]", "INVARIANT": "Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events.", "POST": "Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order.", @@ -43575,7 +44497,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43588,29 +44511,6 @@ "contract_type": "Block" } }, - { - "code": "tag_not_for_contract_type", - "tag": "INVARIANT", - "message": "@INVARIANT is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, { "code": "tag_not_for_contract_type", "tag": "POST", @@ -43621,7 +44521,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43644,7 +44545,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43667,7 +44569,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43680,6 +44583,24 @@ "contract_type": "Block" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -43692,6 +44613,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43709,6 +44631,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43727,7 +44650,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result]", "INVARIANT": "A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created.", "POST": "Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state.", @@ -43772,7 +44695,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43785,29 +44709,6 @@ "contract_type": "Block" } }, - { - "code": "tag_not_for_contract_type", - "tag": "INVARIANT", - "message": "@INVARIANT is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, { "code": "tag_not_for_contract_type", "tag": "POST", @@ -43818,7 +44719,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43841,7 +44743,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43864,7 +44767,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -43877,6 +44781,24 @@ "contract_type": "Block" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Block' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Block" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -43889,6 +44811,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43906,6 +44829,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43923,6 +44847,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43940,6 +44865,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -43958,7 +44884,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.", "PRE": "TaskManager is initialized.", "PURPOSE": "Background thread that periodically flushes log buffer to database." @@ -44008,6 +44934,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44025,6 +44952,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44043,7 +44971,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "All buffered logs are written to task_logs table.", "PRE": "None.", "PURPOSE": "Flush all buffered logs to the database." @@ -44093,6 +45021,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44110,6 +45039,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44128,7 +45058,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The task ID.", "POST": "Task's buffered logs are written to database.", "PRE": "task_id exists.", @@ -44185,6 +45115,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44202,6 +45133,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44220,7 +45152,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "user_id (Optional[str]) - ID of the user requesting the task.", "POST": "Task is created, added to registry, and scheduled for execution.", "PRE": "Plugin with plugin_id exists. Params are valid.", @@ -44297,6 +45229,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44314,6 +45247,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44331,6 +45265,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44349,7 +45284,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The ID of the task to run.", "POST": "Task is executed, status updated to SUCCESS or FAILED.", "PRE": "Task exists in registry.", @@ -44418,6 +45353,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44435,6 +45371,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44452,6 +45389,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44469,6 +45407,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44487,7 +45426,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "resolution_params (Dict[str, Any]) - Params to resolve the wait.", "POST": "Task status updated to RUNNING, params updated, execution resumed.", "PRE": "Task exists and is in AWAITING_MAPPING state.", @@ -44551,6 +45490,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44568,6 +45508,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44586,7 +45527,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The ID of the task.", "POST": "Execution pauses until future is set.", "PRE": "Task exists.", @@ -44643,6 +45584,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -44660,6 +45602,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44678,7 +45621,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The ID of the task.", "POST": "Execution pauses until future is set via resume_task_with_password.", "PRE": "Task exists.", @@ -44729,6 +45672,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44747,7 +45691,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - ID of the task.", "POST": "Returns Task object or None.", "PRE": "task_id is a string.", @@ -44805,6 +45749,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44823,7 +45768,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns list of all Task objects.", "PRE": "None.", "PURPOSE": "Retrieves all registered tasks.", @@ -44874,6 +45819,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44892,7 +45838,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "status (Optional[TaskStatus]) - Filter by task status.", "POST": "Returns a list of tasks sorted by start_time descending.", "PRE": "limit and offset are non-negative integers.", @@ -44950,6 +45896,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -44968,7 +45915,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "log_filter (Optional[LogFilter]) - Filter parameters.", "POST": "Returns list of LogEntry or TaskLog objects.", "PRE": "task_id is a string.", @@ -45032,6 +45979,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45049,6 +45997,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45067,7 +46016,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The task ID.", "POST": "Returns LogStats with counts by level and source.", "PRE": "task_id is a valid task ID.", @@ -45131,6 +46080,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45148,6 +46098,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45166,7 +46117,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The task ID.", "POST": "Returns list of unique source strings.", "PRE": "task_id is a valid task ID.", @@ -45230,6 +46181,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45247,6 +46199,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45265,7 +46218,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "context (Optional[Dict]) - Legacy context (for backward compatibility).", "POST": "Log added to buffer and pushed to queues (if level meets task_log_level filter).", "PRE": "Task exists.", @@ -45322,6 +46275,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45339,6 +46293,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DISPATCHES]" @@ -45357,7 +46312,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - ID of the task.", "POST": "Returns an asyncio.Queue for log entries.", "PRE": "task_id is a string.", @@ -45415,6 +46370,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45433,7 +46389,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "queue (asyncio.Queue) - Queue to remove.", "POST": "Queue removed from subscribers.", "PRE": "task_id is a string, queue is asyncio.Queue.", @@ -45484,6 +46440,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45502,7 +46459,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Persisted tasks loaded into self.tasks.", "PRE": "None.", "PURPOSE": "Load persisted tasks using persistence service." @@ -45552,6 +46509,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45569,6 +46527,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45587,7 +46546,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "input_request (Dict) - Details about required input.", "POST": "Task status changed to AWAITING_INPUT, input_request set, persisted.", "PRE": "Task exists and is in RUNNING state.", @@ -45651,6 +46610,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45668,6 +46628,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45686,7 +46647,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "passwords (Dict[str, str]) - Mapping of database name to password.", "POST": "Task status changed to RUNNING, passwords injected, task resumed.", "PRE": "Task exists and is in AWAITING_INPUT state.", @@ -45750,6 +46711,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45767,6 +46729,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45785,7 +46748,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "status (Optional[TaskStatus]) - Filter by task status.", "POST": "Tasks matching filter (or all non-active) cleared from registry and database.", "PRE": "status is Optional[TaskStatus].", @@ -45855,6 +46818,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -45872,6 +46836,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45889,6 +46854,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -45907,7 +46873,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "CONSTRAINT": "Must use Pydantic for data validation.", "INVARIANT": "Task IDs are immutable once created.", "LAYER": "Core", @@ -45950,20 +46916,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -45976,6 +46928,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[USED_BY]" @@ -45993,6 +46946,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[USED_BY]" @@ -46039,7 +46993,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "A Pydantic model representing a persisted log entry from the database.", "SEMANTICS": [ "task", @@ -46064,7 +47018,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -46089,6 +47046,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46108,17 +47066,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:LogFilter:Class]\nclass LogFilter(BaseModel):\n level: Optional[str] = None # Filter by log level\n source: Optional[str] = None # Filter by source component\n search: Optional[str] = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n\n\n# [/DEF:LogFilter:Class]\n" }, @@ -46131,7 +47079,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Statistics about log entries for a task.", "SEMANTICS": [ "log", @@ -46149,6 +47097,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -46156,7 +47113,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -46190,6 +47150,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46208,7 +47169,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.", "SEMANTICS": [ "task", @@ -46246,7 +47207,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -46271,6 +47235,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46288,6 +47253,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46305,6 +47271,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46323,7 +47290,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]", "INVARIANT": "Database schema must match the TaskRecord model structure.", "LAYER": "Core", @@ -46361,17 +47328,21 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -46386,6 +47357,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46403,6 +47375,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46420,6 +47393,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46438,7 +47412,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]", "INVARIANT": "Persistence must handle potentially missing task fields natively.", "POST": "Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.", @@ -46493,7 +47467,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -46527,6 +47504,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -46539,6 +47534,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46556,6 +47552,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46573,6 +47570,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46590,6 +47588,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46607,6 +47606,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46625,7 +47625,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns datetime object or None", "PRE": "value is an ISO string or datetime object", "PURPOSE": "Safely parse a datetime string from the database" @@ -46649,6 +47649,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -46663,7 +47672,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[env_id: Optional[str]] -> Output[Optional[str]]", "POST": "Returns existing environments.id or None when unresolved.", "PRE": "Session is active", @@ -46717,6 +47726,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -46735,7 +47745,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[Task] -> Model[TaskRecord]", "PARAM": "task (Task) - The task object to persist.", "POST": "Task record created or updated in database.", @@ -46806,6 +47816,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -46824,7 +47835,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "tasks (List[Task]) - The list of tasks to persist.", "POST": "All tasks in list are persisted.", "PRE": "isinstance(tasks, list)", @@ -46875,6 +47886,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -46893,7 +47905,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[TaskRecord] -> Output[List[Task]]", "PARAM": "status (Optional[TaskStatus]) - Filter by status.", "POST": "Returns list of Task objects.", @@ -46967,6 +47979,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -46984,6 +47997,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -47002,7 +48016,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_ids (List[str]) - List of task IDs to delete.", "POST": "Specified task records deleted from database.", "PRE": "task_ids is a list of strings.", @@ -47063,6 +48077,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47081,7 +48096,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]", "INVARIANT": "Log entries are batch-inserted for performance.", "POST": "add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.", @@ -47131,7 +48146,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -47165,6 +48183,24 @@ "contract_type": "Class" } }, + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -47177,6 +48213,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47194,6 +48231,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47211,6 +48249,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47228,6 +48267,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47246,7 +48286,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[List[LogEntry]] -> Model[TaskLogRecord]", "PARAM": "logs (List[LogEntry]) - Log entries to insert.", "POST": "All logs inserted into task_logs table.", @@ -47317,6 +48357,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47335,7 +48376,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[List[TaskLog]]", "PARAM": "log_filter (LogFilter) - Filter parameters.", "POST": "Returns list of TaskLog objects matching filters.", @@ -47415,6 +48456,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47432,6 +48474,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47449,6 +48492,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47467,7 +48511,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[LogStats]", "PARAM": "task_id (str) - The task ID.", "POST": "Returns LogStats with counts by level and source.", @@ -47541,6 +48585,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47558,6 +48603,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47576,7 +48622,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[List[str]]", "PARAM": "task_id (str) - The task ID.", "POST": "Returns list of unique source strings.", @@ -47644,6 +48690,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47662,7 +48709,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_id (str) - The task ID.", "POST": "All logs for the task are deleted.", "PRE": "task_id is a valid task ID.", @@ -47723,6 +48770,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47741,7 +48789,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "task_ids (List[str]) - List of task IDs.", "POST": "All logs for the tasks are deleted.", "PRE": "task_ids is a list of task IDs.", @@ -47802,6 +48850,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47820,7 +48869,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Each TaskLogger instance is bound to a specific task_id and default source.", "LAYER": "Core", "PURPOSE": "Provides a dedicated logger for tasks with automatic source attribution.", @@ -47856,20 +48905,6 @@ "contract_type": "Module" } }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -47891,6 +48926,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47908,6 +48944,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -47926,7 +48963,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "All log calls include the task_id and source.", "PURPOSE": "A wrapper around TaskManager._add_log that carries task_id and source context.", "SEMANTICS": [ @@ -47975,7 +49012,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -48010,10 +49050,24 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Class'", + "detail": { + "actual_type": "Class", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Class' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Class" + } }, { "code": "tag_forbidden_by_complexity", @@ -48036,6 +49090,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -48053,6 +49108,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -48070,6 +49126,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[USED_BY]" @@ -48120,6 +49177,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -48172,10 +49238,33 @@ } }, { - "code": "unknown_tag", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48220,6 +49309,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48264,6 +49362,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48308,6 +49415,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48352,6 +49468,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48396,6 +49521,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48426,7 +49560,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -48452,7 +49588,7 @@ "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": 5, + "COMPLEXITY": "5", "DATA_CONTRACT": "Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]", "INVARIANT": "Async client reuses cached auth tokens per environment credentials and invalidates on 401.", "LAYER": "Infra", @@ -48478,6 +49614,24 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -48490,6 +49644,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -48508,7 +49663,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Async Superset API client backed by httpx.AsyncClient with shared auth cache." }, "relations": [ @@ -48544,6 +49699,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -48561,6 +49717,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48578,6 +49735,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48596,7 +49754,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[config: Dict[str, Any]] -> self._auth_cache_key[str]", "POST": "Client is ready for async request/authentication flow.", "PRE": "config contains base_url and auth payload.", @@ -48656,6 +49814,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48673,6 +49832,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -48691,7 +49851,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.", "PURPOSE": "Normalize base URL for Superset API root construction." }, @@ -48705,6 +49865,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48719,7 +49888,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns absolute URL for upstream request.", "PURPOSE": "Build full API URL from relative Superset endpoint." }, @@ -48733,6 +49902,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48747,7 +49925,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns stable asyncio.Lock instance.", "PURPOSE": "Return per-cache-key async lock to serialize fresh login attempts." }, @@ -48761,6 +49939,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -48775,7 +49962,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "None -> Output[Dict[str, str]]", "POST": "Client tokens are populated and reusable across requests.", "PURPOSE": "Authenticate against Superset and cache access/csrf tokens.", @@ -48841,6 +50028,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48858,6 +50046,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48875,6 +50064,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48893,7 +50083,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Headers include Authorization and CSRF tokens.", "PURPOSE": "Return authenticated Superset headers for async requests." }, @@ -48927,6 +50117,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -48945,7 +50136,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Returns JSON payload or raw httpx.Response when raw_response=true.", "PURPOSE": "Perform one authenticated async Superset API request.", "SIDE_EFFECT": "Performs network I/O." @@ -49001,6 +50192,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -49018,6 +50210,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -49035,6 +50228,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -49053,7 +50247,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[httpx.HTTPStatusError] -> Exception", "POST": "Raises domain-specific exception for caller flow control.", "PURPOSE": "Translate upstream HTTP errors into stable domain exceptions." @@ -49127,6 +50321,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -49144,6 +50339,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49161,6 +50357,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49178,6 +50375,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49195,6 +50393,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49212,6 +50411,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49230,7 +50430,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "Returns true only for dashboard-specific endpoints.", "PURPOSE": "Determine whether an API endpoint represents a dashboard resource for 404 translation." }, @@ -49258,7 +50458,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Input[httpx.HTTPError] -> NetworkError", "POST": "Raises NetworkError with URL context.", "PURPOSE": "Translate generic httpx errors into NetworkError." @@ -49302,6 +50502,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49320,7 +50521,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Client resources are released.", "PURPOSE": "Close underlying httpx client.", "SIDE_EFFECT": "Closes network connections." @@ -49364,6 +50565,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -49420,6 +50622,15 @@ "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -49445,7 +50656,17 @@ "PURPOSE": "Класс для меппинга и обновления verbose_map в датасетах Superset." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DatasetMapper:Class]\n# @PURPOSE: Класс для меппинга и обновления verbose_map в датасетах Superset.\nclass DatasetMapper:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the mapper.\n # @POST: Объект DatasetMapper инициализирован.\n def __init__(self):\n pass\n # [/DEF:__init__:Function]\n\n # [DEF:get_postgres_comments:Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: Dict, table_name: str, table_schema: str) -> Dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n log.reason(\"Fetching comments from PostgreSQL\", payload={\"table_schema\": table_schema, \"table_name\": table_name})\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n log.reflect(\"PostgreSQL comments fetched\", payload={\"count\": len(comments)})\n except Exception as e:\n log.explore(\"Failed to fetch PostgreSQL comments\", payload={\"table_schema\": table_schema, \"table_name\": table_name}, error=str(e))\n raise\n return comments\n # [/DEF:get_postgres_comments:Function]\n\n # [DEF:load_excel_mappings:Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> Dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n log.reason(\"Loading mappings from Excel\", payload={\"file_path\": file_path})\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n log.reflect(\"Excel mappings loaded\", payload={\"count\": len(mappings)})\n return mappings\n except Exception as e:\n log.explore(\"Failed to load Excel mappings\", payload={\"file_path\": file_path}, error=str(e))\n raise\n # [/DEF:load_excel_mappings:Function]\n\n # [DEF:run_mapping:Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: Optional[Dict] = None, excel_path: Optional[str] = None, table_name: Optional[str] = None, table_schema: Optional[str] = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n log.reason(\"Starting dataset mapping\", payload={\"dataset_id\": dataset_id, \"source\": source})\n mappings: Dict[str, str] = {}\n \n try:\n if source in ['postgres', 'both']:\n if not (postgres_config and table_name and table_schema):\n raise ValueError(\"Postgres config is required.\")\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n if not excel_path:\n raise ValueError(\"Excel path is required.\")\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n log.explore(\"Invalid mapping source\", payload={\"source\": source}, error=f\"Invalid source: {source}\")\n return\n\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n \n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n\n for column in original_columns:\n col_name = column.get('column_name')\n \n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n \n new_column = {k: v for k, v in new_column.items() if v is not None}\n\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n \n updated_columns.append(new_column)\n\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n \n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n\n superset_client.update_dataset(dataset_id, payload_for_update)\n log.reflect(\"Dataset verbose_name updated\", payload={\"dataset_id\": dataset_id})\n else:\n log.reflect(\"No changes in verbose_name, skipping update\", payload={\"dataset_id\": dataset_id})\n\n except (FileNotFoundError, Exception) as e:\n log.explore(\"Dataset mapping failed\", payload={\"dataset_id\": dataset_id, \"source\": source}, error=str(e))\n return\n # [/DEF:run_mapping:Function]\n# [/DEF:DatasetMapper:Class]\n" }, @@ -49491,6 +50712,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -49549,6 +50779,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -49630,6 +50869,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -49681,6 +50929,21 @@ "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "unknown_tag", + "tag": "TIER", + "message": "@TIER is not defined in axiom_config.yaml tags", + "detail": null + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -49706,7 +50969,17 @@ "PURPOSE": "Exception raised when a file is not a valid ZIP archive." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:InvalidZipFormatError:Class]\n# @PURPOSE: Exception raised when a file is not a valid ZIP archive.\nclass InvalidZipFormatError(Exception):\n pass\n# [/DEF:InvalidZipFormatError:Class]\n" }, @@ -49752,6 +51025,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "THROW", @@ -49809,6 +51091,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -49861,6 +51152,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -49919,6 +51219,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -49960,7 +51269,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -50030,6 +51341,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -50084,6 +51404,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50136,6 +51465,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50200,6 +51538,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "THROW", @@ -50258,6 +51605,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -50295,6 +51651,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -50341,6 +51706,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50392,6 +51766,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50443,6 +51826,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50494,6 +51886,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "THROW", @@ -50543,17 +51944,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -50610,6 +52006,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -50629,7 +52034,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PUBLIC_API": "APIClient", "PURPOSE": "Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.", @@ -50670,6 +52075,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -50688,11 +52094,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Base exception for all Superset API related errors." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SupersetAPIError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base exception for all Superset API related errors.\nclass SupersetAPIError(Exception):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the exception with a message and context.\n # @PRE: message is a string, context is a dict.\n # @POST: Exception is initialized with context.\n def __init__(self, message: str = \"Superset API error\", **context: Any):\n with belief_scope(\"SupersetAPIError.__init__\"):\n self.context = context\n super().__init__(f\"[API_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:__init__:Function]\n# [/DEF:SupersetAPIError:Class]\n" }, @@ -50705,11 +52121,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Exception raised when authentication fails." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:AuthenticationError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Exception raised when authentication fails.\nclass AuthenticationError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication error.\n # @PRE: message is a string, context is a dict.\n # @POST: AuthenticationError is initialized.\n def __init__(self, message: str = \"Authentication failed\", **context: Any):\n with belief_scope(\"AuthenticationError.__init__\"):\n super().__init__(message, type=\"authentication\", **context)\n # [/DEF:__init__:Function]\n# [/DEF:AuthenticationError:Class]\n" }, @@ -50725,7 +52151,17 @@ "PURPOSE": "Exception raised when access is denied." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PermissionDeniedError:Class]\n# @PURPOSE: Exception raised when access is denied.\nclass PermissionDeniedError(AuthenticationError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the permission denied error.\n # @PRE: message is a string, context is a dict.\n # @POST: PermissionDeniedError is initialized.\n def __init__(self, message: str = \"Permission denied\", **context: Any):\n with belief_scope(\"PermissionDeniedError.__init__\"):\n super().__init__(message, **context)\n # [/DEF:__init__:Function]\n# [/DEF:PermissionDeniedError:Class]\n" }, @@ -50741,7 +52177,17 @@ "PURPOSE": "Exception raised when a dashboard cannot be found." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DashboardNotFoundError:Class]\n# @PURPOSE: Exception raised when a dashboard cannot be found.\nclass DashboardNotFoundError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the not found error with resource ID.\n # @PRE: resource_id is provided.\n # @POST: DashboardNotFoundError is initialized.\n def __init__(self, resource_id: Union[int, str], message: str = \"Dashboard not found\", **context: Any):\n with belief_scope(\"DashboardNotFoundError.__init__\"):\n super().__init__(f\"Dashboard '{resource_id}' {message}\", subtype=\"not_found\", resource_id=resource_id, **context)\n # [/DEF:__init__:Function]\n# [/DEF:DashboardNotFoundError:Class]\n" }, @@ -50757,7 +52203,17 @@ "PURPOSE": "Exception raised when a network level error occurs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:NetworkError:Class]\n# @PURPOSE: Exception raised when a network level error occurs.\nclass NetworkError(Exception):\n # [DEF:NetworkError.__init__:Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:NetworkError.__init__:Function]\n# [/DEF:NetworkError:Class]\n" }, @@ -50793,6 +52249,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -50830,6 +52295,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -50845,17 +52319,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " # [DEF:SupersetAuthCache.get:Function]\n def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # [/DEF:SupersetAuthCache.get:Function]\n" }, @@ -50869,17 +52333,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": " # [DEF:SupersetAuthCache.set:Function]\n def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # [/DEF:SupersetAuthCache.set:Function]\n" }, @@ -50892,7 +52346,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Synchronous Superset API client with process-local auth token caching." }, "relations": [ @@ -50922,6 +52376,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -50939,6 +52394,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -50987,6 +52443,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -51026,6 +52491,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51070,6 +52544,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51114,6 +52597,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51173,6 +52665,24 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51206,6 +52716,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -51223,6 +52734,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -51264,6 +52776,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -51311,6 +52832,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51366,6 +52896,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -51403,6 +52942,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -51447,6 +52995,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -51494,6 +53051,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51551,6 +53117,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51602,6 +53177,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51653,6 +53237,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -51672,7 +53265,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.", "LAYER": "Infra", "POST": "preview and launch calls return Superset-originated artifacts or explicit errors.", @@ -51723,6 +53316,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -51740,6 +53334,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -51759,17 +53354,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:SupersetCompilationAdapter.imports:Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.dataset_review import CompiledPreview, PreviewStatus\n# [/DEF:SupersetCompilationAdapter.imports:Block]\n" }, @@ -51782,7 +53367,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed preview payload for Superset-side compilation." }, "relations": [], @@ -51799,7 +53384,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Typed SQL Lab payload for audited launch handoff." }, "relations": [], @@ -51816,7 +53401,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Bind adapter to one Superset environment and client instance." }, "relations": [], @@ -51833,7 +53418,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Detect explicitly implemented client capabilities without treating loose mocks as real methods." }, "relations": [], @@ -51850,7 +53435,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[PreviewCompilationPayload] -> Output[CompiledPreview]", "POST": "returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.", "PRE": "dataset_id and effective inputs are available for the current session.", @@ -51887,6 +53472,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -51905,7 +53491,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "POST": "preview status becomes stale without fabricating a replacement artifact.", "PRE": "preview is a persisted preview artifact or current in-memory snapshot.", "PURPOSE": "Invalidate previous preview after mapping or value changes." @@ -51943,7 +53529,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SqlLabLaunchPayload] -> Output[str]", "POST": "returns one canonical SQL Lab session reference from Superset.", "PRE": "compiled_sql is Superset-originated and launch gates are already satisfied.", @@ -51980,6 +53566,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -51998,7 +53585,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]", "POST": "returns one normalized upstream compilation response including the chosen strategy metadata.", "PRE": "payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.", @@ -52035,6 +53622,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -52053,7 +53641,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]", "POST": "returns the first successful SQL Lab execution response from Superset.", "PRE": "payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.", @@ -52090,6 +53678,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[CALLS]" @@ -52108,7 +53697,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Normalize candidate Superset preview responses into one compiled-sql structure." }, "relations": [ @@ -52132,6 +53721,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -52150,11 +53740,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Serialize Superset request payload deterministically for network transport." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "def", "body": " # [DEF:SupersetCompilationAdapter._dump_json:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Serialize Superset request payload deterministically for network transport.\n def _dump_json(self, payload: Dict[str, Any]) -> str:\n import json\n\n return json.dumps(payload, sort_keys=True, default=str)\n\n # [/DEF:SupersetCompilationAdapter._dump_json:Function]\n" }, @@ -52167,7 +53767,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.", "LAYER": "Infra", "POST": "Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.", @@ -52212,6 +53812,15 @@ "actual_complexity": 4, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } } ], "anchor_syntax": "def", @@ -52226,7 +53835,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "POST": "extractor instance is ready to parse links against one Superset environment.", "PRE": "constructor receives a configured environment with a usable Superset base URL.", "PURPOSE": "Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.", @@ -52283,7 +53892,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "INVARIANT": "Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.", "LAYER": "Infra", "POST": "Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.", @@ -52342,17 +53951,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_base_imports:Block]\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom copy import deepcopy\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, List, Optional, Set, cast\nfrom urllib.parse import parse_qs, unquote, urlparse\n\nfrom ...config_models import Environment\nfrom ...logger import belief_scope, logger\nfrom ...superset_client import SupersetClient\n# [/DEF:_base_imports:Block]\n" }, @@ -52365,7 +53964,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalized output of Superset link parsing for session intake and recovery." }, "relations": [], @@ -52382,7 +53981,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Bind extractor to one Superset environment and client instance." }, "relations": [], @@ -52399,7 +53998,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Summarize recovered, partial, and unresolved context for session state and UX." }, "relations": [], @@ -52416,7 +54015,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a numeric identifier from a REST-like Superset URL path." }, "relations": [], @@ -52433,7 +54032,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a dashboard id-or-slug reference from a Superset URL path." }, "relations": [], @@ -52450,7 +54049,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a dashboard permalink key from a Superset URL path." }, "relations": [], @@ -52467,7 +54066,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a dashboard identifier from returned permalink state when present." }, "relations": [], @@ -52484,7 +54083,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a chart identifier from returned permalink state when dashboard id is absent." }, "relations": [], @@ -52501,7 +54100,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Recursively search nested dict/list payloads for the first numeric value under a candidate key set." }, "relations": [ @@ -52525,7 +54124,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Decode query-string structures used by Superset URL state transport." }, "relations": [], @@ -52542,7 +54141,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data).", "SEMANTICS": [ @@ -52576,17 +54175,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_filters_imports:Block]\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, cast\n\nfrom ...logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# [/DEF:_filters_imports:Block]\n" }, @@ -52599,7 +54188,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize imported filters from decoded query state without fabricating missing values." }, "relations": [], @@ -52616,7 +54205,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Infra", "PURPOSE": "Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.", "SEMANTICS": [ @@ -52683,17 +54272,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_parsing_imports:Block]\nfrom __future__ import annotations\n\nfrom typing import Any, Dict, List, Optional, cast\nfrom urllib.parse import parse_qs, urlparse\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# [/DEF:_parsing_imports:Block]\n" }, @@ -52706,7 +54285,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[link:str] -> Output[SupersetParsedContext]", "POST": "returns resolved dataset/dashboard context, preserving explicit partial-recovery state if some identifiers cannot be confirmed.", "PRE": "link is a non-empty Superset URL compatible with the configured environment.", @@ -52744,7 +54323,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Recover a dataset binding from resolved dashboard context while preserving explicit unresolved markers." }, "relations": [ @@ -52768,7 +54347,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers.", "SEMANTICS": [ @@ -52804,17 +54383,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_pii_imports:Block]\nimport re\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Tuple\n# [/DEF:_pii_imports:Block]\n" }, @@ -52827,7 +54396,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context." }, "relations": [], @@ -52844,7 +54413,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected." }, "relations": [], @@ -52861,7 +54430,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape." }, "relations": [], @@ -52878,7 +54447,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "LAYER": "Infra", "PURPOSE": "Recover imported filters from Superset parsed context and dashboard metadata.", "SEMANTICS": [ @@ -52944,17 +54513,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_recovery_imports:Block]\nfrom __future__ import annotations\n\nimport json\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Set, cast\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# [/DEF:_recovery_imports:Block]\n" }, @@ -52967,7 +54526,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[SupersetParsedContext] -> Output[List[Dict[str,Any]]]", "POST": "returns explicit recovered and partial filter entries with preserved provenance and confirmation requirements.", "PRE": "parsed_context comes from a successful Superset link parse for one environment.", @@ -53005,7 +54564,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize one imported-filter payload with explicit provenance and confirmation state." }, "relations": [], @@ -53022,7 +54581,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Infra", "PURPOSE": "Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution.", "SEMANTICS": [ @@ -53061,17 +54620,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Block' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Block" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:_templates_imports:Block]\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, Dict, List, Optional, Set, cast\n\nfrom ...logger import belief_scope, logger\n\nlogger = cast(Any, logger)\n# [/DEF:_templates_imports:Block]\n" }, @@ -53084,7 +54633,7 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": "4", "DATA_CONTRACT": "Input[dataset_payload:Dict[str,Any]] -> Output[List[Dict[str,Any]]]", "POST": "returns deduplicated explicit variable records without executing Jinja or fabricating runtime values.", "PRE": "dataset_payload is a Superset dataset-detail style payload with query-bearing fields when available.", @@ -53124,7 +54673,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Collect SQL and expression-bearing dataset fields for deterministic template-variable discovery." }, "relations": [ @@ -53148,7 +54697,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Append one deduplicated template-variable descriptor." }, "relations": [], @@ -53165,7 +54714,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Extract a deterministic primary identifier from a Jinja expression without executing it." }, "relations": [], @@ -53182,7 +54731,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Normalize literal default fragments from template helper calls into JSON-safe values." }, "relations": [], @@ -53199,8 +54748,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming." }, "relations": [ { @@ -53217,30 +54766,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -53253,6 +54778,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "COMPOSES" @@ -53274,17 +54800,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: Optional[Dict[str, Any]] = None\n# #endregion LogEntry\n" }, @@ -53297,8 +54813,8 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.", - "COMPLEXITY": 3 + "COMPLEXITY": 3, + "PURPOSE": "Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming." }, "relations": [ { @@ -53315,21 +54831,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -53342,6 +54843,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "COMPOSES" @@ -53363,17 +54865,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region WebSocketLogHandler.__init__ [C:1] [TYPE Function] [SEMANTICS init,handler,buffer]\n def __init__(self, capacity: int = 1000):\n super().__init__()\n self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)\n # #endregion WebSocketLogHandler.__init__\n" }, @@ -53386,27 +54878,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Captures a log record, formats it, and stores it in the buffer as a LogEntry.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Captures a log record, formats it, and stores it in the buffer as a LogEntry." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region WebSocketLogHandler.emit [C:2] [TYPE Function] [SEMANTICS log,capture,format,buffer]\n # @BRIEF Captures a log record, formats it, and stores it in the buffer as a LogEntry.\n def emit(self, record: logging.LogRecord):\n try:\n log_entry = LogEntry(\n level=record.levelname,\n message=self.format(record),\n context={\n \"name\": record.name,\n \"pathname\": record.pathname,\n \"lineno\": record.lineno,\n \"funcName\": record.funcName,\n \"process\": record.process,\n \"thread\": record.thread,\n }\n )\n self.log_buffer.append(log_entry)\n except Exception:\n self.handleError(record)\n # #endregion WebSocketLogHandler.emit\n" }, @@ -53419,27 +54895,11 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Returns a list of recent log entries from the buffer.", - "COMPLEXITY": 2 + "COMPLEXITY": 2, + "PURPOSE": "Returns a list of recent log entries from the buffer." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": " # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> List[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n" }, @@ -53452,7 +54912,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Core", "PURPOSE": "Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.", "SEMANTICS": [ @@ -53526,22 +54986,7 @@ "target_ref": "[init_db]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Core' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Core" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:AppDependencies:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: dependency, injection, singleton, factory, auth, jwt\n# @PURPOSE: Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.\n# @LAYER: Core\n# @RELATION: Used by main app and API routers to get access to shared instances.\n# @RELATION: CALLS ->[CleanReleaseRepository]\n# @RELATION: CALLS ->[ConfigManager]\n# @RELATION: CALLS ->[PluginLoader]\n# @RELATION: CALLS ->[SchedulerService]\n# @RELATION: CALLS ->[TaskManager]\n# @RELATION: CALLS ->[get_all_plugin_configs]\n# @RELATION: CALLS ->[get_db]\n# @RELATION: CALLS ->[info]\n# @RELATION: CALLS ->[init_db]\n\nfrom pathlib import Path\nfrom typing import Optional\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError\nfrom .core.plugin_loader import PluginLoader\nfrom .core.task_manager import TaskManager\nfrom .core.config_manager import ConfigManager\nfrom .core.scheduler import SchedulerService\nfrom .services.resource_service import ResourceService\nfrom .services.mapping_service import MappingService\nfrom .services.clean_release.repositories import (\n CandidateRepository,\n ArtifactRepository,\n ManifestRepository,\n PolicyRepository,\n ComplianceRepository,\n ReportRepository,\n ApprovalRepository,\n PublicationRepository,\n AuditRepository,\n CleanReleaseAuditLog,\n)\nfrom .services.clean_release.repository import CleanReleaseRepository\nfrom .services.clean_release.facade import CleanReleaseFacade\nfrom .services.reports.report_service import ReportsService\nfrom .core.database import init_db, get_auth_db, get_db\nfrom .core.cot_logger import MarkerLogger\nfrom .core.auth.jwt import decode_token\nfrom .core.auth.repository import AuthRepository\nfrom .models.auth import User\n\nlog = MarkerLogger(\"Dependencies\")\n\n# Initialize singletons lazily to avoid import-time DB side effects during test collection.\n# Use absolute path relative to this file to ensure plugins are found regardless of CWD\nproject_root = Path(__file__).parent.parent.parent\nconfig_path = project_root / \"config.json\"\n\nconfig_manager: Optional[ConfigManager] = None\nplugin_loader: Optional[PluginLoader] = None\ntask_manager: Optional[TaskManager] = None\nscheduler_service: Optional[SchedulerService] = None\nresource_service: Optional[ResourceService] = None\n\n\n# [DEF:get_config_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ConfigManager.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns shared ConfigManager instance.\n# @RETURN: ConfigManager - The shared config manager instance.\ndef get_config_manager() -> ConfigManager:\n \"\"\"Dependency injector for ConfigManager.\"\"\"\n global config_manager\n if config_manager is None:\n init_db()\n config_manager = ConfigManager(config_path=str(config_path))\n return config_manager\n\n\n# [/DEF:get_config_manager:Function]\n\nplugin_dir = Path(__file__).parent / \"plugins\"\n\n# Clean Release Redesign Singletons\n# Note: These use get_db() which is a generator, so we need a way to provide a session.\n# For singletons in dependencies.py, we might need a different approach or\n# initialize them inside the dependency functions.\n\n\n# [DEF:get_plugin_loader:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for PluginLoader.\n# @PRE: Global plugin_loader must be initialized.\n# @POST: Returns shared PluginLoader instance.\n# @RETURN: PluginLoader - The shared plugin loader instance.\ndef get_plugin_loader() -> PluginLoader:\n \"\"\"Dependency injector for PluginLoader.\"\"\"\n global plugin_loader\n if plugin_loader is None:\n plugin_loader = PluginLoader(plugin_dir=str(plugin_dir))\n log.reason(f\"PluginLoader initialized with directory: {plugin_dir}\")\n log.reason(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# [/DEF:get_plugin_loader:Function]\n\n\n# [DEF:get_task_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for TaskManager.\n# @PRE: Global task_manager must be initialized.\n# @POST: Returns shared TaskManager instance.\n# @RETURN: TaskManager - The shared task manager instance.\ndef get_task_manager() -> TaskManager:\n \"\"\"Dependency injector for TaskManager.\"\"\"\n global task_manager\n if task_manager is None:\n task_manager = TaskManager(get_plugin_loader())\n log.reason(\"TaskManager initialized\")\n return task_manager\n\n\n# [/DEF:get_task_manager:Function]\n\n\n# [DEF:get_scheduler_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for SchedulerService.\n# @PRE: Global scheduler_service must be initialized.\n# @POST: Returns shared SchedulerService instance.\n# @RETURN: SchedulerService - The shared scheduler service instance.\ndef get_scheduler_service() -> SchedulerService:\n \"\"\"Dependency injector for SchedulerService.\"\"\"\n global scheduler_service\n if scheduler_service is None:\n scheduler_service = SchedulerService(get_task_manager(), get_config_manager())\n log.reason(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# [/DEF:get_scheduler_service:Function]\n\n\n# [DEF:get_resource_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ResourceService.\n# @PRE: Global resource_service must be initialized.\n# @POST: Returns shared ResourceService instance.\n# @RETURN: ResourceService - The shared resource service instance.\ndef get_resource_service() -> ResourceService:\n \"\"\"Dependency injector for ResourceService.\"\"\"\n global resource_service\n if resource_service is None:\n resource_service = ResourceService()\n log.reason(\"ResourceService initialized\")\n return resource_service\n\n\n# [/DEF:get_resource_service:Function]\n\n\n# [DEF:get_mapping_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for MappingService.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns new MappingService instance.\n# @RETURN: MappingService - A new mapping service instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# [/DEF:get_mapping_service:Function]\n\n\n_clean_release_repository = CleanReleaseRepository()\n\n\n# [DEF:get_clean_release_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Legacy compatibility shim for CleanReleaseRepository.\n# @POST: Returns a shared CleanReleaseRepository instance.\ndef get_clean_release_repository() -> CleanReleaseRepository:\n \"\"\"Legacy compatibility shim for CleanReleaseRepository.\"\"\"\n return _clean_release_repository\n\n\n# [/DEF:get_clean_release_repository:Function]\n\n\n# [DEF:get_clean_release_facade:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for CleanReleaseFacade.\n# @POST: Returns a facade instance with a fresh DB session.\ndef get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade:\n candidate_repo = CandidateRepository(db)\n artifact_repo = ArtifactRepository(db)\n manifest_repo = ManifestRepository(db)\n policy_repo = PolicyRepository(db)\n compliance_repo = ComplianceRepository(db)\n report_repo = ReportRepository(db)\n approval_repo = ApprovalRepository(db)\n publication_repo = PublicationRepository(db)\n audit_repo = AuditRepository(db)\n\n return CleanReleaseFacade(\n candidate_repo=candidate_repo,\n artifact_repo=artifact_repo,\n manifest_repo=manifest_repo,\n policy_repo=policy_repo,\n compliance_repo=compliance_repo,\n report_repo=report_repo,\n approval_repo=approval_repo,\n publication_repo=publication_repo,\n audit_repo=audit_repo,\n config_manager=get_config_manager(),\n )\n\n\n# [/DEF:get_clean_release_facade:Function]\n\n# [DEF:oauth2_scheme:Variable]\n# @RELATION: DEPENDS_ON -> OAuth2PasswordBearer\n# @COMPLEXITY: 1\n# @PURPOSE: OAuth2 password bearer scheme for token extraction.\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\")\n# [/DEF:oauth2_scheme:Variable]\n\n\n# [DEF:get_current_user:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for retrieving currently authenticated user from a JWT.\n# @PRE: JWT token provided in Authorization header.\n# @POST: Returns User object if token is valid.\n# @THROW: HTTPException 401 if token is invalid or user not found.\n# @PARAM: token (str) - Extracted JWT token.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: User - The authenticated user.\ndef get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = decode_token(token)\n username_value = payload.get(\"sub\")\n if not isinstance(username_value, str) or not username_value:\n raise credentials_exception\n username = username_value\n except JWTError:\n raise credentials_exception\n\n repo = AuthRepository(db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise credentials_exception\n return user\n\n\n# [/DEF:get_current_user:Function]\n\n\n# [DEF:has_permission:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for checking if the current user has a specific permission.\n# @PRE: User is authenticated.\n# @POST: Returns True if user has permission.\n# @THROW: HTTPException 403 if permission is denied.\n# @PARAM: resource (str) - The resource identifier.\n# @PARAM: action (str) - The action identifier (READ, EXECUTE, WRITE).\n# @RETURN: User - The authenticated user if permission granted.\ndef has_permission(resource: str, action: str):\n def permission_checker(current_user: User = Depends(get_current_user)):\n # Union of all permissions across all roles\n for role in current_user.roles:\n for perm in role.permissions:\n if perm.resource == resource and perm.action == action:\n return current_user\n\n # Special case for Admin role (full access)\n if any(role.name == \"Admin\" for role in current_user.roles):\n return current_user\n\n from .core.auth.logger import log_security_event\n\n log_security_event(\n \"PERMISSION_DENIED\",\n str(getattr(current_user, \"username\", \"unknown\")),\n {\"resource\": resource, \"action\": action},\n )\n\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {resource}:{action}\",\n )\n\n return permission_checker\n\n\n# [/DEF:has_permission:Function]\n\n# [/DEF:AppDependencies:Module]\n" }, @@ -53554,7 +54999,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns shared ConfigManager instance.", "PRE": "Global config_manager must be initialized.", "PURPOSE": "Dependency injector for ConfigManager.", @@ -53580,6 +55025,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53599,7 +55053,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns shared PluginLoader instance.", "PRE": "Global plugin_loader must be initialized.", "PURPOSE": "Dependency injector for PluginLoader.", @@ -53625,6 +55079,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53644,7 +55107,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns shared TaskManager instance.", "PRE": "Global task_manager must be initialized.", "PURPOSE": "Dependency injector for TaskManager.", @@ -53670,6 +55133,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53689,7 +55161,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns shared SchedulerService instance.", "PRE": "Global scheduler_service must be initialized.", "PURPOSE": "Dependency injector for SchedulerService.", @@ -53715,6 +55187,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53734,7 +55215,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns shared ResourceService instance.", "PRE": "Global resource_service must be initialized.", "PURPOSE": "Dependency injector for ResourceService.", @@ -53760,6 +55241,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53779,7 +55269,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns new MappingService instance.", "PRE": "Global config_manager must be initialized.", "PURPOSE": "Dependency injector for MappingService.", @@ -53805,6 +55295,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -53824,7 +55323,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns a shared CleanReleaseRepository instance.", "PURPOSE": "Legacy compatibility shim for CleanReleaseRepository." }, @@ -53838,6 +55337,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -53852,7 +55360,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "POST": "Returns a facade instance with a fresh DB session.", "PURPOSE": "Dependency injector for CleanReleaseFacade." }, @@ -53866,6 +55374,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -53880,7 +55397,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "OAuth2 password bearer scheme for token extraction." }, "relations": [ @@ -53903,7 +55420,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -53928,7 +55447,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -53963,7 +55484,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "db (Session) - Auth database session.", "POST": "Returns User object if token is valid.", "PRE": "JWT token provided in Authorization header.", @@ -54029,7 +55550,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PARAM": "action (str) - The action identifier (READ, EXECUTE, WRITE).", "POST": "Returns True if user has permission.", "PRE": "User is authenticated.", @@ -54110,7 +55631,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -54148,9 +55671,9 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { "actual_complexity": 1, "contract_type": "Module" @@ -54346,12 +55869,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "TEST_INVARIANT", - "message": "@TEST_INVARIANT is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -54473,7 +55990,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "LAYER": "Domain", "PURPOSE": "Unit tests for data models" }, @@ -54486,6 +56003,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -54561,7 +56087,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for report Pydantic models and their validators" }, @@ -54586,6 +56112,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -54604,7 +56131,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Assistant records preserve immutable ids and creation timestamps.", "LAYER": "Domain", "PURPOSE": "SQLAlchemy models for assistant audit trail and confirmation tokens.", @@ -54646,7 +56173,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Audit payload remains available for compliance and debugging.", "PRE": "user_id must identify the actor for every record.", "PURPOSE": "Store audit decisions and outcomes produced by assistant command handling." @@ -54691,7 +56218,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "Message row can be queried in chronological order.", "PRE": "user_id, conversation_id, role and text must be present.", "PURPOSE": "Persist chat history entries for assistant conversations." @@ -54736,7 +56263,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "POST": "State transitions can be tracked and audited.", "PRE": "intent/dispatch and expiry timestamp must be provided.", "PURPOSE": "Persist risky operation confirmation tokens with lifecycle state." @@ -54781,7 +56308,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Usernames and emails must be unique.", "LAYER": "Domain", "PURPOSE": "SQLAlchemy models for multi-user authentication and authorization.", @@ -54824,6 +56351,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "INHERITS_FROM" @@ -54877,7 +56405,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -54947,7 +56477,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -54999,6 +56531,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -55020,6 +56561,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "HAS_MANY" @@ -55037,6 +56579,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "HAS_MANY" @@ -55066,6 +56609,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -55087,6 +56639,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "HAS_MANY" @@ -55116,6 +56669,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -55138,7 +56700,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]", "INVARIANT": "Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.", "LAYER": "Domain", @@ -55225,7 +56787,17 @@ "PURPOSE": "Backward-compatible execution mode enum for legacy TUI/orchestrator tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ExecutionMode:Class]\n# @PURPOSE: Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# [/DEF:ExecutionMode:Class]\n" }, @@ -55241,7 +56813,17 @@ "PURPOSE": "Backward-compatible final status enum for legacy TUI/orchestrator tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CheckFinalStatus:Class]\n# @PURPOSE: Backward-compatible final status enum for legacy TUI/orchestrator tests.\nclass CheckFinalStatus(str, Enum):\n COMPLIANT = \"COMPLIANT\"\n BLOCKED = \"BLOCKED\"\n FAILED = \"FAILED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckFinalStatus:Class]\n" }, @@ -55257,7 +56839,17 @@ "PURPOSE": "Backward-compatible stage name enum for legacy TUI/orchestrator tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CheckStageName:Class]\n# @PURPOSE: Backward-compatible stage name enum for legacy TUI/orchestrator tests.\nclass CheckStageName(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n# [/DEF:CheckStageName:Class]\n" }, @@ -55273,7 +56865,17 @@ "PURPOSE": "Backward-compatible stage status enum for legacy TUI/orchestrator tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CheckStageStatus:Class]\n# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests.\nclass CheckStageStatus(str, Enum):\n PASS = \"PASS\"\n FAIL = \"FAIL\"\n SKIPPED = \"SKIPPED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckStageStatus:Class]\n" }, @@ -55289,7 +56891,17 @@ "PURPOSE": "Backward-compatible stage result container for legacy TUI/orchestrator tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CheckStageResult:Class]\n# @PURPOSE: Backward-compatible stage result container for legacy TUI/orchestrator tests.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CheckStageResult:\n stage: CheckStageName\n status: CheckStageStatus\n details: str = \"\"\n# [/DEF:CheckStageResult:Class]\n" }, @@ -55305,7 +56917,17 @@ "PURPOSE": "Backward-compatible profile enum for legacy TUI bootstrap logic." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ProfileType:Class]\n# @PURPOSE: Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# [/DEF:ProfileType:Class]\n" }, @@ -55321,7 +56943,17 @@ "PURPOSE": "Backward-compatible registry status enum for legacy TUI bootstrap logic." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RegistryStatus:Class]\n# @PURPOSE: Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# [/DEF:RegistryStatus:Class]\n" }, @@ -55337,7 +56969,17 @@ "PURPOSE": "Backward-compatible release candidate status enum for legacy TUI." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ReleaseCandidateStatus:Class]\n# @PURPOSE: Backward-compatible release candidate status enum for legacy TUI.\nclass ReleaseCandidateStatus(str, Enum):\n DRAFT = CandidateStatus.DRAFT.value\n PREPARED = CandidateStatus.PREPARED.value\n MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value\n CHECK_PENDING = CandidateStatus.CHECK_PENDING.value\n CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value\n CHECK_PASSED = CandidateStatus.CHECK_PASSED.value\n CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n CHECK_ERROR = CandidateStatus.CHECK_ERROR.value\n APPROVED = CandidateStatus.APPROVED.value\n PUBLISHED = CandidateStatus.PUBLISHED.value\n REVOKED = CandidateStatus.REVOKED.value\n# [/DEF:ReleaseCandidateStatus:Class]\n" }, @@ -55353,7 +56995,17 @@ "PURPOSE": "Backward-compatible source entry model for legacy TUI bootstrap logic." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ResourceSourceEntry:Class]\n# @PURPOSE: Backward-compatible source entry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceEntry:\n source_id: str\n host: str\n protocol: str\n purpose: str\n enabled: bool = True\n# [/DEF:ResourceSourceEntry:Class]\n" }, @@ -55369,7 +57021,17 @@ "PURPOSE": "Backward-compatible source registry model for legacy TUI bootstrap logic." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ResourceSourceRegistry:Class]\n# @PURPOSE: Backward-compatible source registry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceRegistry:\n registry_id: str\n name: str\n entries: List[ResourceSourceEntry]\n updated_at: datetime\n updated_by: str\n status: str = \"ACTIVE\"\n immutable: bool = True\n allowed_hosts: Optional[List[str]] = None\n allowed_schemes: Optional[List[str]] = None\n allowed_source_types: Optional[List[str]] = None\n\n @model_validator(mode=\"after\")\n def populate_legacy_allowlists(self):\n enabled_entries = [entry for entry in self.entries if getattr(entry, \"enabled\", True)]\n if self.allowed_hosts is None:\n self.allowed_hosts = [entry.host for entry in enabled_entries]\n if self.allowed_schemes is None:\n self.allowed_schemes = [entry.protocol for entry in enabled_entries]\n if self.allowed_source_types is None:\n self.allowed_source_types = [entry.purpose for entry in enabled_entries]\n return self\n\n @property\n def id(self) -> str:\n return self.registry_id\n# [/DEF:ResourceSourceRegistry:Class]\n" }, @@ -55385,7 +57047,17 @@ "PURPOSE": "Backward-compatible policy model for legacy TUI bootstrap logic." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CleanProfilePolicy:Class]\n# @PURPOSE: Backward-compatible policy model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CleanProfilePolicy:\n policy_id: str\n policy_version: str\n profile: ProfileType\n active: bool\n internal_source_registry_ref: str\n prohibited_artifact_categories: List[str]\n effective_from: datetime\n required_system_categories: Optional[List[str]] = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: Optional[Dict[str, Any]] = None\n\n @model_validator(mode=\"after\")\n def validate_enterprise_policy(self):\n if self.profile == ProfileType.ENTERPRISE_CLEAN:\n if not self.prohibited_artifact_categories:\n raise ValueError(\"enterprise-clean policy requires prohibited_artifact_categories\")\n if self.external_source_forbidden is not True:\n raise ValueError(\"enterprise-clean policy requires external_source_forbidden=true\")\n if self.content_json is None:\n self.content_json = {\n \"profile\": self.profile.value,\n \"prohibited_artifact_categories\": list(self.prohibited_artifact_categories or []),\n \"required_system_categories\": list(self.required_system_categories or []),\n \"external_source_forbidden\": self.external_source_forbidden,\n }\n return self\n\n @property\n def id(self) -> str:\n return self.policy_id\n\n @property\n def registry_snapshot_id(self) -> str:\n return self.internal_source_registry_ref\n# [/DEF:CleanProfilePolicy:Class]\n" }, @@ -55401,7 +57073,17 @@ "PURPOSE": "Backward-compatible run model for legacy TUI typing/import compatibility." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceCheckRun:Class]\n# @PURPOSE: Backward-compatible run model for legacy TUI typing/import compatibility.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ComplianceCheckRun:\n check_run_id: str\n candidate_id: str\n policy_id: str\n started_at: datetime\n triggered_by: str\n execution_mode: ExecutionMode\n final_status: CheckFinalStatus\n checks: List[CheckStageResult]\n finished_at: Optional[datetime] = None\n\n @model_validator(mode=\"after\")\n def validate_final_status_alignment(self):\n mandatory_stages = {\n CheckStageName.DATA_PURITY,\n CheckStageName.INTERNAL_SOURCES_ONLY,\n CheckStageName.NO_EXTERNAL_ENDPOINTS,\n CheckStageName.MANIFEST_CONSISTENCY,\n }\n if self.final_status == CheckFinalStatus.COMPLIANT:\n observed_stages = {check.stage for check in self.checks}\n if observed_stages != mandatory_stages:\n raise ValueError(\"compliant run requires all mandatory stages\")\n if any(check.status != CheckStageStatus.PASS for check in self.checks):\n raise ValueError(\"compliant run requires PASS on all mandatory stages\")\n return self\n\n @property\n def id(self) -> str:\n return self.check_run_id\n\n @property\n def run_id(self) -> str:\n return self.check_run_id\n\n @property\n def status(self) -> RunStatus:\n if self.final_status == CheckFinalStatus.RUNNING:\n return RunStatus.RUNNING\n if self.final_status == CheckFinalStatus.BLOCKED:\n return RunStatus.FAILED\n return RunStatus.SUCCEEDED\n# [/DEF:ComplianceCheckRun:Class]\n" }, @@ -55437,6 +57119,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -55454,7 +57145,17 @@ "PURPOSE": "Represents one artifact associated with a release candidate." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CandidateArtifact:Class]\n# @PURPOSE: Represents one artifact associated with a release candidate.\nclass CandidateArtifact(Base):\n __tablename__ = \"clean_release_artifacts\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n path = Column(String, nullable=False)\n sha256 = Column(String, nullable=False)\n size = Column(Integer, nullable=False)\n detected_category = Column(String, nullable=True)\n declared_category = Column(String, nullable=True)\n source_uri = Column(String, nullable=True)\n source_host = Column(String, nullable=True)\n metadata_json = Column(JSON, default=dict)\n# [/DEF:CandidateArtifact:Class]\n" }, @@ -55468,17 +57169,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ManifestItem:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestItem:\n path: str\n category: str\n classification: ClassificationType\n reason: str\n checksum: Optional[str] = None\n# [/DEF:ManifestItem:Class]\n" }, @@ -55492,17 +57183,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:ManifestSummary:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestSummary:\n included_count: int\n excluded_count: int\n prohibited_detected_count: int\n# [/DEF:ManifestSummary:Class]\n" }, @@ -55528,6 +57209,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -55545,7 +57235,17 @@ "PURPOSE": "Immutable registry snapshot for allowed sources." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SourceRegistrySnapshot:Class]\n# @PURPOSE: Immutable registry snapshot for allowed sources.\nclass SourceRegistrySnapshot(Base):\n __tablename__ = \"clean_release_registry_snapshots\"\n \n id = Column(String, primary_key=True)\n registry_id = Column(String, nullable=False)\n registry_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n allowed_hosts = Column(JSON, nullable=False) # List[str]\n allowed_schemes = Column(JSON, nullable=False) # List[str]\n allowed_source_types = Column(JSON, nullable=False) # List[str]\n immutable = Column(Boolean, default=True)\n# [/DEF:SourceRegistrySnapshot:Class]\n" }, @@ -55561,7 +57261,17 @@ "PURPOSE": "Immutable policy snapshot used to evaluate a run." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CleanPolicySnapshot:Class]\n# @PURPOSE: Immutable policy snapshot used to evaluate a run.\nclass CleanPolicySnapshot(Base):\n __tablename__ = \"clean_release_policy_snapshots\"\n \n id = Column(String, primary_key=True)\n policy_id = Column(String, nullable=False)\n policy_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n content_json = Column(JSON, nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n immutable = Column(Boolean, default=True)\n# [/DEF:CleanPolicySnapshot:Class]\n" }, @@ -55577,7 +57287,17 @@ "PURPOSE": "Operational record for one compliance execution." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceRun:Class]\n# @PURPOSE: Operational record for one compliance execution.\nclass ComplianceRun(Base):\n __tablename__ = \"clean_release_compliance_runs\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_id = Column(String, ForeignKey(\"clean_release_manifests.id\"), nullable=False)\n manifest_digest = Column(String, nullable=False)\n policy_snapshot_id = Column(String, ForeignKey(\"clean_release_policy_snapshots.id\"), nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n requested_by = Column(String, nullable=False)\n requested_at = Column(DateTime, default=datetime.utcnow)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n status = Column(String, default=RunStatus.PENDING)\n final_status = Column(String, nullable=True) # ComplianceDecision\n failure_reason = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n\n @property\n def check_run_id(self) -> str:\n return self.id\n# [/DEF:ComplianceRun:Class]\n" }, @@ -55593,7 +57313,17 @@ "PURPOSE": "Stage-level execution record inside a run." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceStageRun:Class]\n# @PURPOSE: Stage-level execution record inside a run.\nclass ComplianceStageRun(Base):\n __tablename__ = \"clean_release_compliance_stage_runs\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n status = Column(String, nullable=False)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n decision = Column(String, nullable=True) # ComplianceDecision\n details_json = Column(JSON, default=dict)\n# [/DEF:ComplianceStageRun:Class]\n" }, @@ -55609,7 +57339,17 @@ "PURPOSE": "Backward-compatible violation severity enum for legacy clean-release tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ViolationSeverity:Class]\n# @PURPOSE: Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# [/DEF:ViolationSeverity:Class]\n" }, @@ -55625,7 +57365,17 @@ "PURPOSE": "Backward-compatible violation category enum for legacy clean-release tests." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ViolationCategory:Class]\n# @PURPOSE: Backward-compatible violation category enum for legacy clean-release tests.\nclass ViolationCategory(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n EXTERNAL_SOURCE = \"EXTERNAL_SOURCE\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n# [/DEF:ViolationCategory:Class]\n" }, @@ -55641,7 +57391,17 @@ "PURPOSE": "Violation produced by a stage." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ComplianceViolation:Class]\n# @PURPOSE: Violation produced by a stage.\nclass ComplianceViolation(Base):\n __tablename__ = \"clean_release_compliance_violations\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n code = Column(String, nullable=False)\n severity = Column(String, nullable=False)\n artifact_path = Column(String, nullable=True)\n artifact_sha256 = Column(String, nullable=True)\n message = Column(String, nullable=False)\n evidence_json = Column(JSON, default=dict)\n\n def __init__(self, **kwargs):\n if \"violation_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"violation_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n if \"category\" in kwargs:\n category = kwargs.pop(\"category\")\n kwargs[\"stage_name\"] = category.value if isinstance(category, ViolationCategory) else str(category)\n if \"location\" in kwargs:\n kwargs[\"artifact_path\"] = kwargs.pop(\"location\")\n if \"remediation\" in kwargs:\n remediation = kwargs.pop(\"remediation\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"remediation\"] = remediation\n kwargs[\"evidence_json\"] = evidence\n if \"blocked_release\" in kwargs:\n blocked_release = kwargs.pop(\"blocked_release\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"blocked_release\"] = blocked_release\n kwargs[\"evidence_json\"] = evidence\n if \"detected_at\" in kwargs:\n kwargs.pop(\"detected_at\")\n if \"code\" not in kwargs:\n kwargs[\"code\"] = \"LEGACY_VIOLATION\"\n if \"message\" not in kwargs:\n kwargs[\"message\"] = kwargs.get(\"stage_name\", \"LEGACY_VIOLATION\")\n super().__init__(**kwargs)\n\n @property\n def violation_id(self) -> str:\n return self.id\n\n @violation_id.setter\n def violation_id(self, value: str) -> None:\n self.id = value\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def category(self) -> ViolationCategory:\n return ViolationCategory(self.stage_name)\n\n @category.setter\n def category(self, value: ViolationCategory) -> None:\n self.stage_name = value.value if isinstance(value, ViolationCategory) else str(value)\n\n @property\n def location(self) -> Optional[str]:\n return self.artifact_path\n\n @property\n def remediation(self) -> Optional[str]:\n return (self.evidence_json or {}).get(\"remediation\")\n\n @property\n def blocked_release(self) -> bool:\n return bool((self.evidence_json or {}).get(\"blocked_release\", False))\n# [/DEF:ComplianceViolation:Class]\n" }, @@ -55667,6 +57427,15 @@ "actual_complexity": 1, "contract_type": "Class" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } } ], "anchor_syntax": "def", @@ -55684,7 +57453,17 @@ "PURPOSE": "Approval or rejection bound to a candidate and report." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ApprovalDecision:Class]\n# @PURPOSE: Approval or rejection bound to a candidate and report.\nclass ApprovalDecision(Base):\n __tablename__ = \"clean_release_approval_decisions\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n decision = Column(String, nullable=False) # ApprovalDecisionType\n decided_by = Column(String, nullable=False)\n decided_at = Column(DateTime, default=datetime.utcnow)\n comment = Column(String, nullable=True)\n# [/DEF:ApprovalDecision:Class]\n" }, @@ -55700,7 +57479,17 @@ "PURPOSE": "Publication or revocation record." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PublicationRecord:Class]\n# @PURPOSE: Publication or revocation record.\nclass PublicationRecord(Base):\n __tablename__ = \"clean_release_publication_records\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n published_by = Column(String, nullable=False)\n published_at = Column(DateTime, default=datetime.utcnow)\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# [/DEF:PublicationRecord:Class]\n" }, @@ -55716,7 +57505,17 @@ "PURPOSE": "Represents a persistent audit log entry for clean release actions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CleanReleaseAuditLog:Class]\n# @PURPOSE: Represents a persistent audit log entry for clean release actions.\nimport uuid\nclass CleanReleaseAuditLog(Base):\n __tablename__ = \"clean_release_audit_logs\"\n \n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n candidate_id = Column(String, index=True, nullable=True)\n action = Column(String, nullable=False) # e.g. \"TRANSITION\", \"APPROVE\", \"PUBLISH\"\n actor = Column(String, nullable=False)\n timestamp = Column(DateTime, default=datetime.utcnow)\n details_json = Column(JSON, default=dict)\n# [/DEF:CleanReleaseAuditLog:Class]\n" }, @@ -55764,6 +57563,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -55821,6 +57629,15 @@ "contract_type": "Class" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -55843,7 +57660,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", "PURPOSE": "Defines the database schema for external database connection configurations.", @@ -55873,6 +57690,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -55895,11 +57721,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Stores credentials for external databases used for column mapping." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ConnectionConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Stores credentials for external databases used for column mapping.\nclass ConnectionConfig(Base):\n __tablename__ = \"connection_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n type = Column(String, nullable=False) # e.g., \"postgres\"\n host = Column(String, nullable=True)\n port = Column(Integer, nullable=True)\n database = Column(String, nullable=True)\n username = Column(String, nullable=True)\n password = Column(String, nullable=True) # Encrypted/Obfuscated password\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# [/DEF:ConnectionConfig:Class]\n" }, @@ -55912,7 +57748,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Model", "PURPOSE": "Defines data models for dashboard metadata and selection.", "SEMANTICS": [ @@ -55931,20 +57767,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Model' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Model" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -55957,6 +57779,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USED_BY" @@ -55975,11 +57798,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents a dashboard available for migration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DashboardMetadata:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a dashboard available for migration.\nclass DashboardMetadata(BaseModel):\n id: int\n title: str\n last_modified: str\n status: str\n# [/DEF:DashboardMetadata:Class]\n" }, @@ -55992,11 +57825,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents the user's selection of dashboards to migrate." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DashboardSelection:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the user's selection of dashboards to migrate.\nclass DashboardSelection(BaseModel):\n selected_ids: List[int]\n source_env_id: str\n target_env_id: str\n replace_db_config: bool = False\n fix_cross_filters: bool = True\n# [/DEF:DashboardSelection:Class]\n" }, @@ -56009,7 +57852,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "All public model classes and enums remain importable from `src.models.dataset_review` without changes.", "LAYER": "Domain", "PURPOSE": "Thin facade re-exporting all dataset review domain models from the decomposed sub-package.", @@ -56092,6 +57935,24 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -56113,6 +57974,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56130,6 +57992,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56147,6 +58010,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56164,6 +58028,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56181,6 +58046,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56198,6 +58064,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56215,6 +58082,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56232,6 +58100,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56249,6 +58118,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "EXPORTS" @@ -56267,7 +58137,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Only one active clarification question may exist at a time per session.", "LAYER": "Domain", "PURPOSE": "Clarification session, question, option, and answer models for guided review flow." @@ -56309,7 +58179,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One clarification session aggregate owning questions and tracking resolution progress." }, "relations": [ @@ -56343,7 +58213,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One clarification question with priority ordering, options, and state machine." }, "relations": [ @@ -56377,7 +58247,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "One selectable option for a clarification question with recommendation flag." }, "relations": [ @@ -56389,6 +58259,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -56411,7 +58290,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One persisted clarification answer with impact summary and feedback tracking." }, "relations": [ @@ -56445,7 +58324,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Enum values are string-based for JSON serialization compatibility.", "LAYER": "Domain", "PURPOSE": "All enumeration types for the dataset review domain, grouped for stable cross-module reuse." @@ -56474,11 +58353,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status of a dataset review session." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SessionStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status of a dataset review session.\nclass SessionStatus(str, enum.Enum):\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n ARCHIVED = \"archived\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:SessionStatus:Class]\n" }, @@ -56491,11 +58380,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Ordered phase progression for dataset review orchestration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SessionPhase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Ordered phase progression for dataset review orchestration.\nclass SessionPhase(str, enum.Enum):\n INTAKE = \"intake\"\n RECOVERY = \"recovery\"\n REVIEW = \"review\"\n SEMANTIC_REVIEW = \"semantic_review\"\n CLARIFICATION = \"clarification\"\n MAPPING_REVIEW = \"mapping_review\"\n PREVIEW = \"preview\"\n LAUNCH = \"launch\"\n POST_RUN = \"post_run\"\n\n\n# [/DEF:SessionPhase:Class]\n" }, @@ -56508,11 +58407,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Granular readiness indicator driving the recommended-action UX flow." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ReadinessState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Granular readiness indicator driving the recommended-action UX flow.\nclass ReadinessState(str, enum.Enum):\n EMPTY = \"empty\"\n IMPORTING = \"importing\"\n REVIEW_READY = \"review_ready\"\n SEMANTIC_SOURCE_REVIEW_NEEDED = \"semantic_source_review_needed\"\n CLARIFICATION_NEEDED = \"clarification_needed\"\n CLARIFICATION_ACTIVE = \"clarification_active\"\n MAPPING_REVIEW_NEEDED = \"mapping_review_needed\"\n COMPILED_PREVIEW_READY = \"compiled_preview_ready\"\n PARTIALLY_READY = \"partially_ready\"\n RUN_READY = \"run_ready\"\n RUN_IN_PROGRESS = \"run_in_progress\"\n COMPLETED = \"completed\"\n RECOVERY_REQUIRED = \"recovery_required\"\n\n\n# [/DEF:ReadinessState:Class]\n" }, @@ -56525,11 +58434,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Next-action guidance derived from the current readiness state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:RecommendedAction:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Next-action guidance derived from the current readiness state.\nclass RecommendedAction(str, enum.Enum):\n IMPORT_FROM_SUPERSET = \"import_from_superset\"\n REVIEW_DOCUMENTATION = \"review_documentation\"\n APPLY_SEMANTIC_SOURCE = \"apply_semantic_source\"\n START_CLARIFICATION = \"start_clarification\"\n ANSWER_NEXT_QUESTION = \"answer_next_question\"\n APPROVE_MAPPING = \"approve_mapping\"\n GENERATE_SQL_PREVIEW = \"generate_sql_preview\"\n COMPLETE_REQUIRED_VALUES = \"complete_required_values\"\n LAUNCH_DATASET = \"launch_dataset\"\n RESUME_SESSION = \"resume_session\"\n EXPORT_OUTPUTS = \"export_outputs\"\n\n\n# [/DEF:RecommendedAction:Class]\n" }, @@ -56542,11 +58461,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "RBAC role for session collaborators." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SessionCollaboratorRole:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: RBAC role for session collaborators.\nclass SessionCollaboratorRole(str, enum.Enum):\n VIEWER = \"viewer\"\n REVIEWER = \"reviewer\"\n APPROVER = \"approver\"\n\n\n# [/DEF:SessionCollaboratorRole:Class]\n" }, @@ -56559,11 +58488,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Provenance of the dataset business summary text." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:BusinessSummarySource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance of the dataset business summary text.\nclass BusinessSummarySource(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:BusinessSummarySource:Class]\n" }, @@ -56576,11 +58515,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Confidence level for dataset profile completeness." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence level for dataset profile completeness.\nclass ConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n MOSTLY_CONFIRMED = \"mostly_confirmed\"\n MIXED = \"mixed\"\n LOW_CONFIDENCE = \"low_confidence\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:ConfidenceState:Class]\n" }, @@ -56593,11 +58542,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Domain area classification for validation findings." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FindingArea:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Domain area classification for validation findings.\nclass FindingArea(str, enum.Enum):\n SOURCE_INTAKE = \"source_intake\"\n DATASET_PROFILE = \"dataset_profile\"\n SEMANTIC_ENRICHMENT = \"semantic_enrichment\"\n CLARIFICATION = \"clarification\"\n FILTER_RECOVERY = \"filter_recovery\"\n TEMPLATE_MAPPING = \"template_mapping\"\n COMPILED_PREVIEW = \"compiled_preview\"\n LAUNCH = \"launch\"\n AUDIT = \"audit\"\n\n\n# [/DEF:FindingArea:Class]\n" }, @@ -56610,11 +58569,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Severity classification for validation findings." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FindingSeverity:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Severity classification for validation findings.\nclass FindingSeverity(str, enum.Enum):\n BLOCKING = \"blocking\"\n WARNING = \"warning\"\n INFORMATIONAL = \"informational\"\n\n\n# [/DEF:FindingSeverity:Class]\n" }, @@ -56627,11 +58596,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Resolution status for validation findings and clarification items." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ResolutionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolution status for validation findings and clarification items.\nclass ResolutionState(str, enum.Enum):\n OPEN = \"open\"\n RESOLVED = \"resolved\"\n APPROVED = \"approved\"\n SKIPPED = \"skipped\"\n DEFERRED = \"deferred\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:ResolutionState:Class]\n" }, @@ -56644,11 +58623,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Classification of semantic enrichment source origins." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SemanticSourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of semantic enrichment source origins.\nclass SemanticSourceType(str, enum.Enum):\n UPLOADED_FILE = \"uploaded_file\"\n CONNECTED_DICTIONARY = \"connected_dictionary\"\n REFERENCE_DATASET = \"reference_dataset\"\n NEIGHBOR_DATASET = \"neighbor_dataset\"\n AI_GENERATED = \"ai_generated\"\n\n\n# [/DEF:SemanticSourceType:Class]\n" }, @@ -56661,11 +58650,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Trust classification for semantic source reliability." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:TrustLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Trust classification for semantic source reliability.\nclass TrustLevel(str, enum.Enum):\n TRUSTED = \"trusted\"\n RECOMMENDED = \"recommended\"\n CANDIDATE = \"candidate\"\n GENERATED = \"generated\"\n\n\n# [/DEF:TrustLevel:Class]\n" }, @@ -56678,11 +58677,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status for semantic source application." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:SemanticSourceStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic source application.\nclass SemanticSourceStatus(str, enum.Enum):\n AVAILABLE = \"available\"\n SELECTED = \"selected\"\n APPLIED = \"applied\"\n REJECTED = \"rejected\"\n PARTIAL = \"partial\"\n FAILED = \"failed\"\n\n\n# [/DEF:SemanticSourceStatus:Class]\n" }, @@ -56695,11 +58704,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Kind classification for semantic field entries." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FieldKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for semantic field entries.\nclass FieldKind(str, enum.Enum):\n COLUMN = \"column\"\n METRIC = \"metric\"\n FILTER_DIMENSION = \"filter_dimension\"\n PARAMETER = \"parameter\"\n\n\n# [/DEF:FieldKind:Class]\n" }, @@ -56712,11 +58731,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Provenance tracking for semantic field value origin." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FieldProvenance:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance tracking for semantic field value origin.\nclass FieldProvenance(str, enum.Enum):\n DICTIONARY_EXACT = \"dictionary_exact\"\n REFERENCE_IMPORTED = \"reference_imported\"\n FUZZY_INFERRED = \"fuzzy_inferred\"\n AI_GENERATED = \"ai_generated\"\n MANUAL_OVERRIDE = \"manual_override\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FieldProvenance:Class]\n" }, @@ -56729,11 +58758,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Match type classification for semantic candidates." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CandidateMatchType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Match type classification for semantic candidates.\nclass CandidateMatchType(str, enum.Enum):\n EXACT = \"exact\"\n REFERENCE = \"reference\"\n FUZZY = \"fuzzy\"\n GENERATED = \"generated\"\n\n\n# [/DEF:CandidateMatchType:Class]\n" }, @@ -56746,11 +58785,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status for semantic candidate proposals." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:CandidateStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic candidate proposals.\nclass CandidateStatus(str, enum.Enum):\n PROPOSED = \"proposed\"\n ACCEPTED = \"accepted\"\n REJECTED = \"rejected\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:CandidateStatus:Class]\n" }, @@ -56763,11 +58812,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Origin classification for imported filters." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FilterSource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Origin classification for imported filters.\nclass FilterSource(str, enum.Enum):\n SUPERSET_NATIVE = \"superset_native\"\n SUPERSET_URL = \"superset_url\"\n SUPERSET_PERMALINK = \"superset_permalink\"\n SUPERSET_NATIVE_FILTERS_KEY = \"superset_native_filters_key\"\n MANUAL = \"manual\"\n INFERRED = \"inferred\"\n\n\n# [/DEF:FilterSource:Class]\n" }, @@ -56780,11 +58839,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Confidence classification for imported filter values." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FilterConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence classification for imported filter values.\nclass FilterConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FilterConfidenceState:Class]\n" }, @@ -56797,11 +58866,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Recovery quality status for imported filters." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FilterRecoveryStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Recovery quality status for imported filters.\nclass FilterRecoveryStatus(str, enum.Enum):\n RECOVERED = \"recovered\"\n PARTIAL = \"partial\"\n MISSING = \"missing\"\n CONFLICTED = \"conflicted\"\n\n\n# [/DEF:FilterRecoveryStatus:Class]\n" }, @@ -56814,11 +58893,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Kind classification for template variables." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:VariableKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for template variables.\nclass VariableKind(str, enum.Enum):\n NATIVE_FILTER = \"native_filter\"\n PARAMETER = \"parameter\"\n DERIVED = \"derived\"\n UNKNOWN = \"unknown\"\n\n\n# [/DEF:VariableKind:Class]\n" }, @@ -56831,11 +58920,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status for template variable mapping." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MappingStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for template variable mapping.\nclass MappingStatus(str, enum.Enum):\n UNMAPPED = \"unmapped\"\n PROPOSED = \"proposed\"\n APPROVED = \"approved\"\n OVERRIDDEN = \"overridden\"\n INVALID = \"invalid\"\n\n\n# [/DEF:MappingStatus:Class]\n" }, @@ -56848,11 +58947,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Method classification for execution mapping creation." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MappingMethod:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Method classification for execution mapping creation.\nclass MappingMethod(str, enum.Enum):\n DIRECT_MATCH = \"direct_match\"\n HEURISTIC_MATCH = \"heuristic_match\"\n SEMANTIC_MATCH = \"semantic_match\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:MappingMethod:Class]\n" }, @@ -56865,11 +58974,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Warning severity for execution mapping quality indicators." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MappingWarningLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Warning severity for execution mapping quality indicators.\nclass MappingWarningLevel(str, enum.Enum):\n LOW = \"low\"\n MEDIUM = \"medium\"\n HIGH = \"high\"\n\n\n# [/DEF:MappingWarningLevel:Class]\n" }, @@ -56882,11 +59001,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Approval lifecycle for execution mapping gate checks." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ApprovalState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Approval lifecycle for execution mapping gate checks.\nclass ApprovalState(str, enum.Enum):\n PENDING = \"pending\"\n APPROVED = \"approved\"\n REJECTED = \"rejected\"\n NOT_REQUIRED = \"not_required\"\n\n\n# [/DEF:ApprovalState:Class]\n" }, @@ -56899,11 +59028,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status for clarification sessions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ClarificationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for clarification sessions.\nclass ClarificationStatus(str, enum.Enum):\n PENDING = \"pending\"\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:ClarificationStatus:Class]\n" }, @@ -56916,11 +59055,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "State machine for individual clarification questions." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:QuestionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: State machine for individual clarification questions.\nclass QuestionState(str, enum.Enum):\n OPEN = \"open\"\n ANSWERED = \"answered\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:QuestionState:Class]\n" }, @@ -56933,11 +59082,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Classification of clarification answer types." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:AnswerKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of clarification answer types.\nclass AnswerKind(str, enum.Enum):\n SELECTED = \"selected\"\n CUSTOM = \"custom\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:AnswerKind:Class]\n" }, @@ -56950,11 +59109,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Lifecycle status for compiled SQL previews." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:PreviewStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for compiled SQL previews.\nclass PreviewStatus(str, enum.Enum):\n PENDING = \"pending\"\n READY = \"ready\"\n FAILED = \"failed\"\n STALE = \"stale\"\n\n\n# [/DEF:PreviewStatus:Class]\n" }, @@ -56967,11 +59136,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Outcome status for dataset launch handoff." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LaunchStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Outcome status for dataset launch handoff.\nclass LaunchStatus(str, enum.Enum):\n STARTED = \"started\"\n SUCCESS = \"success\"\n FAILED = \"failed\"\n\n\n# [/DEF:LaunchStatus:Class]\n" }, @@ -56984,11 +59163,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Type classification for export artifacts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ArtifactType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Type classification for export artifacts.\nclass ArtifactType(str, enum.Enum):\n DOCUMENTATION = \"documentation\"\n VALIDATION_REPORT = \"validation_report\"\n RUN_SUMMARY = \"run_summary\"\n\n\n# [/DEF:ArtifactType:Class]\n" }, @@ -57001,11 +59190,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Format classification for export artifact output." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ArtifactFormat:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Format classification for export artifact output.\nclass ArtifactFormat(str, enum.Enum):\n JSON = \"json\"\n MARKDOWN = \"markdown\"\n CSV = \"csv\"\n PDF = \"pdf\"\n\n\n# [/DEF:ArtifactFormat:Class]\n" }, @@ -57018,7 +59217,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Compiled preview, run context, session event, and export artifact models for execution and audit." }, @@ -57049,7 +59248,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One compiled SQL preview snapshot with fingerprint for staleness detection." }, "relations": [ @@ -57083,7 +59282,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time." }, "relations": [ @@ -57117,7 +59316,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One persisted audit event for dataset review session mutations." }, "relations": [ @@ -57151,7 +59350,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One persisted export artifact reference for documentation and validation outputs." }, "relations": [ @@ -57185,7 +59384,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Imported filter and template variable models for Superset context recovery and execution mapping." }, @@ -57216,7 +59415,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Recovered Superset filter with confidence and recovery status tracking." }, "relations": [ @@ -57250,7 +59449,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Discovered template variable from dataset SQL with mapping status tracking." }, "relations": [ @@ -57284,7 +59483,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain", "PURPOSE": "Validation finding model for tracking blocking, warning, and informational issues during review." }, @@ -57325,7 +59524,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Structured finding record for dataset review validation issues with resolution tracking." }, "relations": [ @@ -57359,7 +59558,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain", "PURPOSE": "Execution mapping model linking imported filters to template variables with approval gates." }, @@ -57400,7 +59599,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "INVARIANT": "Explicit approval is required before launch when requires_explicit_approval is true.", "PURPOSE": "One filter-to-variable mapping with approval gate, effective value, and transformation metadata." }, @@ -57444,7 +59643,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Domain", "PURPOSE": "Dataset profile model capturing business summary, confidence, and completeness metadata." }, @@ -57485,7 +59684,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness." }, "relations": [ @@ -57519,7 +59718,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Manual overrides are never silently replaced by imported, inferred, or AI-generated values.", "LAYER": "Domain", "PURPOSE": "Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment." @@ -57561,7 +59760,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Registered semantic enrichment source with trust level and application status." }, "relations": [ @@ -57595,7 +59794,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Locked fields preserve their active value regardless of later candidate proposals.", "PURPOSE": "Per-field semantic metadata entry with provenance tracking, lock state, and candidate set." }, @@ -57636,7 +59835,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "One proposed semantic value for a field entry, ranked by match type and confidence." }, "relations": [ @@ -57670,7 +59869,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Session and profile entities are strictly scoped to an authenticated user.", "LAYER": "Domain", "PURPOSE": "Session aggregate root and collaborator models for dataset review orchestration." @@ -57712,7 +59911,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "RBAC collaborator record linking a user to a dataset review session with a specific role." }, "relations": [ @@ -57746,7 +59945,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Optimistic-lock version column prevents lost-update races on concurrent mutations.", "PURPOSE": "Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions." }, @@ -57853,7 +60052,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "LAYER": "Models", "PURPOSE": "Pydantic models for Superset native filter state extraction and restoration.", "SEMANTICS": [ @@ -57874,20 +60073,6 @@ } ], "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Models' is not in allowed enum", - "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Models" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -57909,6 +60094,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -57927,7 +60113,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]", "PURPOSE": "Represents the state of a single native filter." }, @@ -57944,7 +60130,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -57967,7 +60155,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -57992,7 +60181,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -58018,7 +60209,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]", "PURPOSE": "Represents the dataMask containing all native filter states." }, @@ -58035,7 +60226,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -58058,7 +60251,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -58083,7 +60277,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -58109,7 +60305,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]", "PURPOSE": "Result of parsing native filters from permalink or native_filters_key." }, @@ -58126,7 +60322,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -58149,7 +60347,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -58174,7 +60373,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -58200,7 +60401,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]", "PURPOSE": "Result of parsing a complete dashboard URL for filter information." }, @@ -58217,7 +60418,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -58240,7 +60443,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -58265,7 +60469,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -58291,7 +60497,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "DATA_CONTRACT": "Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]", "PURPOSE": "Configuration for merging extraFormData from different sources." }, @@ -58308,7 +60514,9 @@ "Function", "Class", "Component", - "Block" + "Block", + "Skill", + "Agent" ] } }, @@ -58331,7 +60539,8 @@ "Module", "Function", "Class", - "Component" + "Component", + "Agent" ] } }, @@ -58356,7 +60565,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -58383,26 +60594,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:GitModels:Module]\n\nimport enum\nfrom datetime import datetime\nfrom sqlalchemy import Column, String, Integer, DateTime, Enum, ForeignKey, Boolean\nimport uuid\nfrom src.core.database import Base\n\nclass GitProvider(str, enum.Enum):\n GITHUB = \"GITHUB\"\n GITLAB = \"GITLAB\"\n GITEA = \"GITEA\"\n\nclass GitStatus(str, enum.Enum):\n CONNECTED = \"CONNECTED\"\n FAILED = \"FAILED\"\n UNKNOWN = \"UNKNOWN\"\n\nclass SyncStatus(str, enum.Enum):\n CLEAN = \"CLEAN\"\n DIRTY = \"DIRTY\"\n CONFLICT = \"CONFLICT\"\n\n# [DEF:GitServerConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# [/DEF:GitServerConfig:Class]\n\n# [DEF:GitRepository:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# [/DEF:GitRepository:Class]\n\n# [DEF:DeploymentEnvironment:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# [/DEF:DeploymentEnvironment:Class]\n\n# [/DEF:GitModels:Module]\n" }, @@ -58415,11 +60607,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Configuration for a Git server connection." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitServerConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# [/DEF:GitServerConfig:Class]\n" }, @@ -58432,11 +60634,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Tracking for a local Git repository linked to a dashboard." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitRepository:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# [/DEF:GitRepository:Class]\n" }, @@ -58449,11 +60661,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Target Superset environments for dashboard deployment." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DeploymentEnvironment:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# [/DEF:DeploymentEnvironment:Class]\n" }, @@ -58466,7 +60688,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "SQLAlchemy models for LLM provider configuration and validation results.", "SEMANTICS": [ @@ -58497,6 +60719,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "INHERITS_FROM" @@ -58518,7 +60741,17 @@ "PURPOSE": "Defines a scheduled rule for validating a group of dashboards within an execution window." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationPolicy:Class]\n# @PURPOSE: Defines a scheduled rule for validating a group of dashboards within an execution window.\nclass ValidationPolicy(Base):\n __tablename__ = \"validation_policies\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n dashboard_ids = Column(JSON, nullable=False) # Array of dashboard IDs\n schedule_days = Column(JSON, nullable=False) # Array of integers (0-6)\n window_start = Column(Time, nullable=False)\n window_end = Column(Time, nullable=False)\n notify_owners = Column(Boolean, default=True)\n custom_channels = Column(JSON, nullable=True) # List of external channels\n alert_condition = Column(String, default=\"FAIL_ONLY\") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS\n created_at = Column(DateTime, default=datetime.utcnow)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n# [/DEF:ValidationPolicy:Class]\n" }, @@ -58534,7 +60767,17 @@ "PURPOSE": "SQLAlchemy model for LLM provider configuration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:LLMProvider:Class]\n# @PURPOSE: SQLAlchemy model for LLM provider configuration.\nclass LLMProvider(Base):\n __tablename__ = \"llm_providers\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n provider_type = Column(String, nullable=False) # openai, openrouter, kilo\n name = Column(String, nullable=False)\n base_url = Column(String, nullable=False)\n api_key = Column(String, nullable=False) # Should be encrypted\n default_model = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n# [/DEF:LLMProvider:Class]\n" }, @@ -58550,7 +60793,17 @@ "PURPOSE": "SQLAlchemy model for dashboard validation history." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ValidationRecord:Class]\n# @PURPOSE: SQLAlchemy model for dashboard validation history.\nclass ValidationRecord(Base):\n __tablename__ = \"llm_validation_results\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n task_id = Column(String, nullable=True, index=True) # Reference to TaskRecord\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=datetime.utcnow)\n status = Column(String, nullable=False) # PASS, WARN, FAIL, UNKNOWN\n screenshot_path = Column(String, nullable=True)\n issues = Column(JSON, nullable=False)\n summary = Column(Text, nullable=False)\n raw_response = Column(Text, nullable=True)\n# [/DEF:ValidationRecord:Class]\n" }, @@ -58563,7 +60816,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "CONSTRAINT": "source_env_id and target_env_id must be valid environment IDs.", "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", @@ -58614,11 +60867,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Enumeration of possible Superset resource types for ID mapping." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:ResourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible Superset resource types for ID mapping.\nclass ResourceType(str, enum.Enum):\n CHART = \"chart\"\n DATASET = \"dataset\"\n DASHBOARD = \"dashboard\"\n# [/DEF:ResourceType:Class]\n" }, @@ -58631,11 +60894,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Enumeration of possible migration job statuses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:MigrationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible migration job statuses.\nclass MigrationStatus(enum.Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n COMPLETED = \"COMPLETED\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n# [/DEF:MigrationStatus:Class]\n" }, @@ -58648,7 +60921,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": "2", "PURPOSE": "Represents a single migration execution job." }, "relations": [], @@ -58665,7 +60938,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Maps a universal UUID for a resource to its actual ID on a specific environment.", "TEST_DATA": "resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}" }, @@ -58697,7 +60970,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Sensitive Git token is stored encrypted and never returned in plaintext.", "LAYER": "Domain", "PURPOSE": "Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences.", @@ -58748,6 +61021,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "INHERITS_FROM" @@ -58766,7 +61040,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user." }, "relations": [ @@ -58790,7 +61064,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "DATA_CONTRACT": "Model[TaskReport, ReportCollection, ReportDetailView]", "INVARIANT": "Canonical report fields are always present for every report item.", "LAYER": "Domain", @@ -58872,6 +61146,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "[DEPENDS_ON]" @@ -58890,7 +61165,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Must contain valid generic task type mappings.", "PURPOSE": "Supported normalized task report types.", "SEMANTICS": [ @@ -58924,7 +61199,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -58950,7 +61228,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "TaskStatus enum mapping logic holds.", "PURPOSE": "Supported normalized report status values.", "SEMANTICS": [ @@ -58984,7 +61262,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59010,7 +61291,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "The properties accurately describe error state.", "PURPOSE": "Error and recovery context for failed/partial reports.", "SEMANTICS": [ @@ -59038,7 +61319,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59094,7 +61378,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Must represent canonical task record attributes.", "PURPOSE": "Canonical normalized report envelope for one task execution.", "SEMANTICS": [ @@ -59122,7 +61406,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59178,7 +61465,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Time and pagination queries are mutually consistent.", "PURPOSE": "Query object for server-side report filtering, sorting, and pagination.", "SEMANTICS": [ @@ -59206,7 +61493,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59262,7 +61552,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Represents paginated data correctly.", "PURPOSE": "Paginated collection of normalized task reports.", "SEMANTICS": [ @@ -59289,7 +61579,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59345,7 +61638,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Incorporates a report and logs correctly.", "PURPOSE": "Detailed report representation including diagnostics and recovery actions.", "SEMANTICS": [ @@ -59373,7 +61666,10 @@ "detail": { "actual_type": "Class", "allowed_types": [ - "Module" + "Module", + "Block", + "Skill", + "Agent" ] } }, @@ -59430,26 +61726,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:StorageModels:Module]\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Optional\nfrom pydantic import BaseModel, Field\n\n# [DEF:FileCategory:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# [/DEF:FileCategory:Class]\n\n# [DEF:StorageConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# [/DEF:StorageConfig:Class]\n\n# [DEF:StoredFile:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: Optional[str] = Field(None, description=\"MIME type of the file.\")\n# [/DEF:StoredFile:Class]\n\n# [/DEF:StorageModels:Module]\n" }, @@ -59462,11 +61739,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Enumeration of supported file categories in the storage system." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:FileCategory:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# [/DEF:FileCategory:Class]\n" }, @@ -59479,11 +61766,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Configuration model for the storage system, defining paths and naming patterns." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:StorageConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# [/DEF:StorageConfig:Class]\n" }, @@ -59496,11 +61793,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Data model representing metadata for a file stored in the system." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:StoredFile:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: Optional[str] = Field(None, description=\"MIME type of the file.\")\n# [/DEF:StoredFile:Class]\n" }, @@ -59513,7 +61820,7 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", "PURPOSE": "Defines the database schema for task execution records.", @@ -59543,6 +61850,15 @@ "contract_type": "Module" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -59565,11 +61881,21 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1, + "COMPLEXITY": "1", "PURPOSE": "Represents a persistent record of a task execution." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:TaskRecord:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a persistent record of a task execution.\nclass TaskRecord(Base):\n __tablename__ = \"task_records\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # e.g., \"backup\", \"migration\"\n status = Column(String, nullable=False) # Enum: \"PENDING\", \"RUNNING\", \"SUCCESS\", \"FAILED\"\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=True)\n started_at = Column(DateTime(timezone=True), nullable=True)\n finished_at = Column(DateTime(timezone=True), nullable=True)\n logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)\n error = Column(String, nullable=True)\n result = Column(JSON, nullable=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n params = Column(JSON, nullable=True)\n# [/DEF:TaskRecord:Class]\n" }, @@ -59582,7 +61908,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "INVARIANT": "Each log entry belongs to exactly one task.", "PURPOSE": "Represents a single persistent log entry for a task.", "TEST_CONTRACT": "TaskLogCreate ->" @@ -59639,28 +61965,12 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects.", "COMPLEXITY": 2, - "LAYER": "Domain" + "LAYER": "Domain", + "PURPOSE": "SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "BRIEF", - "message": "@BRIEF is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslateModels [C:2] [TYPE Module]\n# @BRIEF SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects.\n# @LAYER Domain\n\nfrom sqlalchemy import (\n Column, String, Boolean, DateTime, JSON, Text,\n ForeignKey, Integer, UniqueConstraint, Index\n)\nfrom sqlalchemy.dialects.postgresql import JSON as PG_JSON\nfrom datetime import datetime, timezone\nimport uuid\nimport hashlib\nfrom .mapping import Base\n\n\n# #region generate_uuid [C:1] [TYPE Function]\ndef generate_uuid():\n return str(uuid.uuid4())\n# #endregion generate_uuid\n\n\n# #region TranslationJob [C:1] [TYPE Class]\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"The column whose values will be translated\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for INSERT via SQL Lab\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationJob\n\n\n# #region TranslationRun [C:1] [TYPE Class]\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationRun\n\n\n# #region TranslationBatch [C:1] [TYPE Class]\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationBatch\n\n\n# #region TranslationRecord [C:1] [TYPE Class]\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row key/context column values for INSERT phase\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n\n\n# #region TranslationEvent [C:1] [TYPE Class]\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationEvent\n\n\n# #region TranslationPreviewSession [C:1] [TYPE Class]\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n\n\n# #region TranslationPreviewRecord [C:1] [TYPE Class]\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row values for key/context columns\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationPreviewRecord\n\n\n# #region TerminologyDictionary [C:1] [TYPE Class]\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TerminologyDictionary\n\n\n# #region DictionaryEntry [C:1] [TYPE Class]\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n\n\n# #region TranslationSchedule [C:1] [TYPE Class]\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationSchedule\n\n\n# #region TranslationJobDictionary [C:1] [TYPE Class]\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n\n\n# #region MetricSnapshot [C:1] [TYPE Class]\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n\n# #endregion TranslateModels\n" }, @@ -59676,17 +61986,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region generate_uuid [C:1] [TYPE Function]\ndef generate_uuid():\n return str(uuid.uuid4())\n# #endregion generate_uuid\n" }, @@ -59702,17 +62002,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationJob [C:1] [TYPE Class]\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"The column whose values will be translated\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for INSERT via SQL Lab\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationJob\n" }, @@ -59728,17 +62018,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationRun [C:1] [TYPE Class]\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationRun\n" }, @@ -59754,17 +62034,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationBatch [C:1] [TYPE Class]\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationBatch\n" }, @@ -59780,17 +62050,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationRecord [C:1] [TYPE Class]\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row key/context column values for INSERT phase\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n" }, @@ -59806,17 +62066,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationEvent [C:1] [TYPE Class]\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationEvent\n" }, @@ -59832,17 +62082,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationPreviewSession [C:1] [TYPE Class]\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n" }, @@ -59858,17 +62098,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationPreviewRecord [C:1] [TYPE Class]\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row values for key/context columns\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationPreviewRecord\n" }, @@ -59884,17 +62114,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TerminologyDictionary [C:1] [TYPE Class]\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TerminologyDictionary\n" }, @@ -59910,17 +62130,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region DictionaryEntry [C:1] [TYPE Class]\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n" }, @@ -59936,17 +62146,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationSchedule [C:1] [TYPE Class]\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationSchedule\n" }, @@ -59962,17 +62162,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region TranslationJobDictionary [C:1] [TYPE Class]\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n" }, @@ -59988,17 +62178,7 @@ "COMPLEXITY": 1 }, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "body": "# #region MetricSnapshot [C:1] [TYPE Class]\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n" }, @@ -60027,7 +62207,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -60091,17 +62273,12 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'App' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "App" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -60125,6 +62302,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60169,17 +62347,12 @@ ], "schema_warnings": [ { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Plugins' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Plugins" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -60203,6 +62376,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60224,7 +62398,17 @@ "PURPOSE": "Plugin for system diagnostics and debugging." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:DebugPlugin:Class]\n# @PURPOSE: Plugin for system diagnostics and debugging.\nclass DebugPlugin(PluginBase):\n \"\"\"\n Plugin for system diagnostics and debugging.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"system-debug\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"system-debug\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"System Debug\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Run system diagnostics and debug Superset API responses.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the debug plugin.\n # @RETURN: str - \"/tools/debug\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/debug\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the debug plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"title\": \"Action\",\n \"enum\": [\"test-db-api\", \"get-dataset-structure\"],\n \"default\": \"test-db-api\"\n },\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (for dataset structure).\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset (for dataset structure).\"\n },\n \"source_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"Source env for DB API test.\"\n },\n \"target_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"Target env for DB API test.\"\n }\n },\n \"required\": [\"action\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the debug logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Debug parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: action must be provided in params.\n # @POST: Debug action is executed and results returned.\n # @RETURN: Dict[str, Any] - Execution results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n action = params.get(\"action\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n debug_log = log.with_source(\"debug\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n debug_log.info(f\"Executing debug action: {action}\")\n \n if action == \"test-db-api\":\n return await self._test_db_api(params, superset_log)\n elif action == \"get-dataset-structure\":\n return await self._get_dataset_structure(params, superset_log)\n else:\n debug_log.error(f\"Unknown action: {action}\")\n raise ValueError(f\"Unknown action: {action}\")\n # [/DEF:execute:Function]\n\n # [DEF:_test_db_api:Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n \n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n \n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n \n return results\n # [/DEF:_test_db_api:Function]\n\n # [DEF:_get_dataset_structure:Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n \n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n \n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n \n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # [/DEF:_get_dataset_structure:Function]\n\n# [/DEF:DebugPlugin:Class]\n" }, @@ -60269,6 +62453,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60320,6 +62513,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60355,7 +62557,9 @@ "Class", "Component", "Block", - "ADR" + "ADR", + "Skill", + "Agent" ] } }, @@ -60381,7 +62585,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "LLM-based extensions for the Git plugin, specifically for commit message generation.", "SEMANTICS": [ @@ -60414,7 +62618,17 @@ "PURPOSE": "Provides LLM capabilities to the Git plugin." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitLLMExtension:Class]\n# @PURPOSE: Provides LLM capabilities to the Git plugin.\nclass GitLLMExtension:\n def __init__(self, client: LLMClient):\n self.client = client\n\n # [DEF:suggest_commit_message:Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n \n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n \n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n \n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n \n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # [/DEF:suggest_commit_message:Function]\n# [/DEF:GitLLMExtension:Class]\n" }, @@ -60439,6 +62653,15 @@ "message": "@PARAM is not defined in axiom_config.yaml tags", "detail": null }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60520,17 +62743,12 @@ } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Plugin' is not in allowed enum", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "allowed": [ - "Domain", - "UI", - "Infra", - "Test" - ], - "value": "Plugin" + "actual_complexity": 1, + "contract_type": "Module" } }, { @@ -60554,6 +62772,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "INHERITS_FROM" @@ -60571,6 +62790,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60588,6 +62808,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60605,6 +62826,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60622,6 +62844,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "USES" @@ -60643,7 +62866,17 @@ "PURPOSE": "Реализация плагина Git Integration для управления версиями дашбордов." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "def", "body": "# [DEF:GitPlugin:Class]\n# @PURPOSE: Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n \n # [DEF:__init__:Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n log.reason(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n \n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n log.reflect(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n log.reflect(f\"GitPlugin initialized with {config_path}\")\n # [/DEF:__init__:Function]\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the plugin identifier.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns 'git-integration'.\n def id(self) -> str:\n with belief_scope(\"GitPlugin.id\"):\n return \"git-integration\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the plugin name.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the human-readable name.\n def name(self) -> str:\n with belief_scope(\"GitPlugin.name\"):\n return \"Git Integration\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns the plugin description.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the plugin's purpose description.\n def description(self) -> str:\n with belief_scope(\"GitPlugin.description\"):\n return \"Version control for Superset dashboards\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the plugin version.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the version string.\n def version(self) -> str:\n with belief_scope(\"GitPlugin.version\"):\n return \"0.1.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the git plugin.\n # @RETURN: str - \"/git\"\n def ui_route(self) -> str:\n with belief_scope(\"GitPlugin.ui_route\"):\n return \"/git\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns a JSON schema dictionary.\n # @RETURN: Dict[str, Any] - Схема параметров.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"operation\": {\"type\": \"string\", \"enum\": [\"sync\", \"deploy\", \"history\"]},\n \"dashboard_id\": {\"type\": \"integer\"},\n \"environment_id\": {\"type\": \"string\"},\n \"source_env_id\": {\"type\": \"string\"}\n },\n \"required\": [\"operation\", \"dashboard_id\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:initialize:Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n log.reason(\"Initializing Git Integration Plugin logic\")\n\n # [DEF:execute:Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.execute\"):\n operation = task_data.get(\"operation\")\n dashboard_id = task_data.get(\"dashboard_id\")\n \n # Use TaskContext logger if available, otherwise fall back to app_logger\n log = context.logger if context else app_logger\n \n # Create sub-loggers for different components\n git_log = log.with_source(\"git\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n log.info(f\"Executing operation: {operation} for dashboard {dashboard_id}\")\n \n if operation == \"sync\":\n source_env_id = task_data.get(\"source_env_id\")\n result = await self._handle_sync(dashboard_id, source_env_id, log, git_log, superset_log)\n elif operation == \"deploy\":\n env_id = task_data.get(\"environment_id\")\n result = await self._handle_deploy(dashboard_id, env_id, log, git_log, superset_log)\n elif operation == \"history\":\n result = {\"status\": \"success\", \"message\": \"History available via API\"}\n else:\n log.error(f\"Unknown operation: {operation}\")\n raise ValueError(f\"Unknown operation: {operation}\")\n \n log.info(f\"Operation {operation} completed.\")\n return result\n # [/DEF:execute:Function]\n\n # [DEF:_handle_sync:Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: Optional[str] = None, log=None, git_log=None, superset_log=None) -> Dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n \n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n \n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n \n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n \n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n \n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n \n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n \n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n log.reason(\"Changes staged in git\")\n except Exception as ge:\n log.explore(\"Failed to stage changes\", error=str(ge))\n\n log.reflect(\"Dashboard synced successfully\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n log.explore(\"Sync failed\", error=str(e))\n raise\n # [/DEF:_handle_sync:Function]\n\n # [DEF:_handle_deploy:Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n \n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n \n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n \n zip_buffer.seek(0)\n \n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n \n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n \n try:\n log.reason(\"Importing dashboard\", payload={\"environment\": env.name})\n result = client.import_dashboard(temp_zip_path)\n log.reflect(\"Deployment successful\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": f\"Dashboard deployed to {env.name}\", \"details\": result}\n finally:\n if temp_zip_path.exists():\n os.remove(temp_zip_path)\n\n except Exception as e:\n log.explore(\"Deployment failed\", error=str(e))\n raise\n # [/DEF:_handle_deploy:Function]\n\n # [DEF:_get_env:Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: Optional[str] = None):\n with belief_scope(\"GitPlugin._get_env\"):\n log.reason(\"Fetching environment\", payload={\"env_id\": env_id})\n \n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n log.reflect(\"Found environment by ID in ConfigManager\", payload={\"name\": env.name})\n return env\n \n # Priority 2: Database (DeploymentEnvironment)\n from src.core.database import SessionLocal\n from src.models.git import DeploymentEnvironment\n \n db = SessionLocal()\n try:\n if env_id:\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.id == env_id).first()\n else:\n # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n log.reflect(\"Found environment in DB\", payload={\"name\": db_env.name})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n \n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n log.reflect(\"Found environment in ConfigManager list\", payload={\"env_id\": env_id})\n return env\n \n if not env_id:\n log.reflect(\"Using first environment from ConfigManager\", payload={\"name\": envs[0].name})\n return envs[0]\n \n log.explore(\"No environments configured\", payload={\"env_id\": env_id}, error=\"No Superset environments found in config or database\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # [/DEF:_get_env:Function]\n\n # [/DEF:initialize:Function]\n# [/DEF:GitPlugin:Class]\n" }, @@ -60679,6 +62912,15 @@ "actual_complexity": 1, "contract_type": "Function" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], "anchor_syntax": "def", @@ -60739,6 +62981,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60816,6 +63067,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60885,6 +63145,15 @@ "contract_type": "Function" } }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, { "code": "unknown_tag", "tag": "RETURN", @@ -60905,26 +63174,7 @@ "complexity": 1, "metadata": {}, "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "PURPOSE", - "message": "@PURPOSE is required for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:backend/src/plugins/llm_analysis/__init__.py:Module]\n\n\"\"\"\nLLM Analysis Plugin for automated dashboard validation and dataset documentation.\n\"\"\"\n\nfrom .plugin import DashboardValidationPlugin, DocumentationPlugin\n\n__all__ = ['DashboardValidationPlugin', 'DocumentationPlugin']\n\n# [/DEF:backend/src/plugins/llm_analysis/__init__.py:Module]\n" }, @@ -60937,7 +63187,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Verify OpenRouter client initialization includes provider-specific headers.", "SEMANTICS": [ "tests", @@ -60955,15 +63205,6 @@ } ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -60976,6 +63217,7 @@ "IMPLEMENTS", "DISPATCHES", "BINDS_TO", + "CALLED_BY", "VERIFIES" ], "predicate": "BELONGS_TO" @@ -61047,7 +63289,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": "3", "PURPOSE": "Protect dashboard screenshot navigation from brittle networkidle waits.", "SEMANTICS": [ "tests", @@ -61064,17 +63306,7 @@ "target_ref": "[src.plugins.llm_analysis.service.ScreenshotService]" } ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "LAYER", - "message": "@LAYER is required for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", "body": "# [DEF:TestScreenshotService:Module]\n# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression\n# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits.\n\nimport pytest\n\nfrom src.plugins.llm_analysis.service import ScreenshotService\n\n\n# [DEF:test_iter_login_roots_includes_child_frames:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Login discovery must search embedded auth frames, not only the main page.\n# @PRE: Page exposes child frames list.\n# @POST: Returned roots include page plus child frames in order.\ndef test_iter_login_roots_includes_child_frames():\n frame_a = object()\n frame_b = object()\n fake_page = type(\"FakePage\", (), {\"frames\": [frame_a, frame_b]})()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n roots = service._iter_login_roots(fake_page)\n\n assert roots == [fake_page, frame_a, frame_b]\n\n\n# [/DEF:test_iter_login_roots_includes_child_frames:Function]\n\n\n# [DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Direct login fallback must reject responses that render the login screen again.\n# @PRE: Response body contains stable login-page markers.\n# @POST: Helper returns True so caller treats fallback as failed authentication.\ndef test_response_looks_like_login_page_detects_login_markup():\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n result = service._response_looks_like_login_page(\n \"\"\"\n \n \n